diff --git a/.travis.yml b/.travis.yml index d776f10076..fbfdebaf67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,6 @@ addons: - ubuntu-toolchain-r-test packages: - g++-4.8 - cache: directories: - $HOME/.npm diff --git a/app/adapters/application.js b/app/adapters/application.js index d4713d7435..0342040ebd 100644 --- a/app/adapters/application.js +++ b/app/adapters/application.js @@ -16,10 +16,10 @@ const { } = Ember; export default Adapter.extend(CheckForErrors, { - config: Ember.inject.service(), + ajax: Ember.inject.service(), database: Ember.inject.service(), db: reads('database.mainDB'), - standAlone: reads('config.standAlone'), + usePouchFind: reads('database.usePouchFind'), _specialQueries: [ 'containsValue', @@ -29,66 +29,65 @@ export default Adapter.extend(CheckForErrors, { _esDefaultSize: 25, _executeContainsSearch(store, type, query) { - let standAlone = get(this, 'standAlone'); - if (standAlone) { + let usePouchFind = get(this, 'usePouchFind'); + if (usePouchFind) { return this._executePouchDBFind(store, type, query); } - return new Ember.RSVP.Promise((resolve, reject) => { - let typeName = this.getRecordTypeName(type); - let searchUrl = `/search/hrdb/${typeName}/_search`; - if (query.containsValue && query.containsValue.value) { - let queryString = ''; - query.containsValue.keys.forEach((key) => { - if (!Ember.isEmpty(queryString)) { - queryString = `${queryString} OR `; - } - let queryValue = query.containsValue.value; - switch (key.type) { - case 'contains': { - queryValue = `*${queryValue}*`; - break; - } - case 'fuzzy': { - queryValue = `${queryValue}~`; - break; - } + let typeName = this.getRecordTypeName(type); + let searchUrl = `/search/hrdb/${typeName}/_search`; + if (query.containsValue && query.containsValue.value) { + let queryString = ''; + query.containsValue.keys.forEach((key) => { + if (!Ember.isEmpty(queryString)) { + queryString = `${queryString} OR `; + } + let queryValue = query.containsValue.value; + switch (key.type) { + case 'contains': { + queryValue = `*${queryValue}*`; + break; } - queryString = `${queryString}data.${key.name}:${queryValue}`; - }); - let successFn = (results) => { - if (results && results.hits && results.hits.hits) { - let resultDocs = Ember.A(results.hits.hits).map((hit) => { - let mappedResult = hit._source; - mappedResult.id = hit._id; - return mappedResult; - }); - let response = { - rows: resultDocs - }; - this._handleQueryResponse(response, store, type).then(resolve, reject); - } else if (results.rows) { - this._handleQueryResponse(results, store, type).then(resolve, reject); - } else { - reject('Search results are not valid'); + case 'fuzzy': { + queryValue = `${queryValue}~`; + break; } - }; - - if (Ember.isEmpty(query.size)) { - query.size = this.get('_esDefaultSize'); } - - Ember.$.ajax(searchUrl, { - dataType: 'json', - data: { - q: queryString, - size: this.get('_esDefaultSize') - }, - success: successFn - }); - } else { - reject('invalid query'); + queryString = `${queryString}data.${key.name}:${queryValue}`; + }); + let ajax = get(this, 'ajax'); + if (Ember.isEmpty(query.size)) { + query.size = this.get('_esDefaultSize'); } - }); + + return ajax.request(searchUrl, { + dataType: 'json', + data: { + q: queryString, + size: this.get('_esDefaultSize') + } + }).then((results) => { + if (results && results.hits && results.hits.hits) { + let resultDocs = Ember.A(results.hits.hits).map((hit) => { + let mappedResult = hit._source; + mappedResult.id = hit._id; + return mappedResult; + }); + let response = { + rows: resultDocs + }; + return this._handleQueryResponse(response, store, type); + } else if (results.rows) { + return this._handleQueryResponse(results, store, type); + } else { + throw new Error('Search results are not valid'); + } + }).catch(() => { + // Try pouch db find if ajax fails + return this._executePouchDBFind(store, type, query); + }); + } else { + throw new Error('invalid query'); + } }, _executePouchDBFind(store, type, query) { diff --git a/app/admin/textreplace/route.js b/app/admin/textreplace/route.js index 2705799ea4..ee9407a98b 100644 --- a/app/admin/textreplace/route.js +++ b/app/admin/textreplace/route.js @@ -10,7 +10,6 @@ export default AbstractIndexRoute.extend({ return store.findAll('text-expansion').then((result) => { return result.filter((model) => { let isNew = model.get('isNew'); - console.log(`${model.get('from')} ${isNew}`); return !isNew; }); }); diff --git a/app/authenticators/custom.js b/app/authenticators/custom.js index 66cf99a8b7..9765a63fcd 100644 --- a/app/authenticators/custom.js +++ b/app/authenticators/custom.js @@ -1,6 +1,8 @@ import Ember from 'ember'; import BaseAuthenticator from 'ember-simple-auth/authenticators/base'; import crypto from 'npm:crypto'; +import MapOauthParams from 'hospitalrun/mixins/map-oauth-params'; +import OAuthHeaders from 'hospitalrun/mixins/oauth-headers'; const { computed: { @@ -10,28 +12,19 @@ const { RSVP } = Ember; -export default BaseAuthenticator.extend({ +export default BaseAuthenticator.extend(MapOauthParams, OAuthHeaders, { + ajax: Ember.inject.service(), config: Ember.inject.service(), database: Ember.inject.service(), - serverEndpoint: '/db/_session', - useGoogleAuth: false, + serverEndpoint: '/auth/login', standAlone: alias('config.standAlone'), usersDB: alias('database.usersDB'), - /** - @method absolutizeExpirationTime - @private - */ - _absolutizeExpirationTime(expiresIn) { - if (!Ember.isEmpty(expiresIn)) { - return new Date((new Date().getTime()) + (expiresIn - 5) * 1000).getTime(); - } - }, - - _checkUser(user) { + _checkUser(user, oauthConfigs) { return new RSVP.Promise((resolve, reject) => { - this._makeRequest('POST', { name: user.name }, '/chkuser').then((response) => { + let headers = this.getOAuthHeaders(oauthConfigs); + this._makeRequest({ name: user.name }, '/chkuser', headers).then((response) => { if (response.error) { reject(response); } @@ -39,40 +32,52 @@ export default BaseAuthenticator.extend({ user.role = response.role; user.prefix = response.prefix; resolve(user); - }, () => { + }).catch(() => { // If chkuser fails, user is probably offline; resolve with currently stored credentials resolve(user); }); }); }, - _getPromise(type, data) { - return new RSVP.Promise(function(resolve, reject) { - this._makeRequest(type, data).then(function(response) { - Ember.run(function() { - resolve(response); - }); - }, function(xhr) { - Ember.run(function() { - reject(xhr.responseJSON || xhr.responseText); - }); - }); - }.bind(this)); + _finishAuth(user, oauthConfigs) { + let config = this.get('config'); + let database = this.get('database'); + config.setCurrentUser(user); + return database.setup().then(() => { + user.oauthConfigs = oauthConfigs; + return user; + }); }, - _makeRequest(type, data, url) { + _makeRequest(data, url, headers, method) { if (!url) { url = this.serverEndpoint; } - return Ember.$.ajax({ - url, - type, + let ajax = get(this, 'ajax'); + let params = { + type: 'POST', data, dataType: 'json', contentType: 'application/x-www-form-urlencoded', xhrFields: { withCredentials: true } + }; + if (method) { + params.type = method; + } + if (headers) { + params.headers = headers; + } + + return ajax.request(url, params); + }, + + _saveOAuthConfigs(params) { + let config = get(this, 'config'); + let oauthConfigs = this.mapOauthParams(params); + return config.saveOauthConfigs(oauthConfigs).then(() => { + return oauthConfigs; }); }, @@ -88,51 +93,41 @@ export default BaseAuthenticator.extend({ return this._authenticateStandAlone(credentials); } if (credentials.google_auth) { - this.useGoogleAuth = true; - let sessionCredentials = { - google_auth: true, - consumer_key: credentials.params.k, - consumer_secret: credentials.params.s1, - token: credentials.params.t, - token_secret: credentials.params.s2, - name: credentials.params.i - }; - return new RSVP.Promise((resolve, reject) => { - this._checkUser(sessionCredentials).then((user) => { - resolve(user); - this.get('config').setCurrentUser(user.name); - }, reject); + return this._saveOAuthConfigs(credentials.params).then((oauthConfigs) => { + return this._checkUser({ name: credentials.params.i }, oauthConfigs).then((user) => { + return this._finishAuth(user, oauthConfigs); + }); }); } - return new Ember.RSVP.Promise((resolve, reject) => { - let username = credentials.identification; - if (typeof username === 'string' && username) { - username = username.trim(); + let username = this._getUserName(credentials); + let data = { name: username, password: credentials.password }; + return this._makeRequest(data).then((user) => { + if (user.error) { + throw new Error(user.errorResult || 'Unauthorized user'); } - let data = { name: username, password: credentials.password }; - this._makeRequest('POST', data).then((response) => { - response.name = data.name; - response.expires_at = this._absolutizeExpirationTime(600); - this._checkUser(response).then((user) => { - this.get('config').setCurrentUser(user.name); - let database = this.get('database'); - database.setup({}).then(() => { - resolve(user); - }, reject); - }, reject); - }, function(xhr) { - reject(xhr.responseJSON || xhr.responseText); + let userInfo = { + displayName: user.displayName, + prefix: user.prefix, + role: user.role + }; + userInfo.name = username; + + return this._saveOAuthConfigs(user).then((oauthConfigs) => { + return this._finishAuth(userInfo, oauthConfigs); }); }); }, - invalidate() { + invalidate(data) { let standAlone = get(this, 'standAlone'); if (this.useGoogleAuth || standAlone) { return RSVP.resolve(); } else { - return this._getPromise('DELETE'); + // Ping the remote db to make sure we still have connectivity before logging off. + let headers = this.getOAuthHeaders(data.oauthConfigs); + let remoteDBUrl = get(this, 'database').getRemoteDBUrl(); + return this._makeRequest({}, remoteDBUrl, headers, 'GET'); } }, @@ -140,23 +135,14 @@ export default BaseAuthenticator.extend({ if (window.ELECTRON) { // config service has not been setup yet, so config.standAlone not available yet return RSVP.resolve(data); } - return new RSVP.Promise((resolve, reject) => { - let now = (new Date()).getTime(); - if (!Ember.isEmpty(data.expires_at) && data.expires_at < now) { - reject(); - } else { - if (data.google_auth) { - this.useGoogleAuth = true; - } - this._checkUser(data).then(resolve, reject); - } - }); + return this._checkUser(data, data.oauthConfigs); }, _authenticateStandAlone(credentials) { let usersDB = get(this, 'usersDB'); return new RSVP.Promise((resolve, reject) => { - usersDB.get(`org.couchdb.user:${credentials.identification}`).then((user) => { + let username = this._getUserName(credentials); + usersDB.get(`org.couchdb.user:${username}`).then((user) => { let { salt, iterations, derived_key } = user; let { password } = credentials; this._checkPassword(password, salt, iterations, derived_key, (error, isCorrectPassword) => { @@ -167,7 +153,7 @@ export default BaseAuthenticator.extend({ reject(new Error('UNAUTHORIZED')); } user.role = this._getPrimaryRole(user); - resolve(user); + this._finishAuth(user, {}).then(resolve, reject); }); }, reject); }); @@ -193,6 +179,14 @@ export default BaseAuthenticator.extend({ }); } return primaryRole; + }, + + _getUserName(credentials) { + let username = credentials.identification; + if (typeof username === 'string' && username) { + username = username.trim(); + } + return username; } }); diff --git a/app/controllers/index.js b/app/controllers/index.js index 338e84cbe0..61dc88a16d 100644 --- a/app/controllers/index.js +++ b/app/controllers/index.js @@ -15,11 +15,13 @@ export default Ember.Controller.extend(UserSession, { needsUserSetup: alias('config.needsUserSetup'), // on init, look up the list of users and determine if there's a need for a needsUserSetup msg init() { - get(this, 'database.usersDB').allDocs().then((results) => { - if (results.total_rows <= 1) { - set(this, 'config.needsUserSetup', true); - } - }); + if (get(this, 'standAlone')) { + get(this, 'database.usersDB').allDocs().then((results) => { + if (results.total_rows <= 1) { + set(this, 'config.needsUserSetup', true); + } + }); + } }, actions: { newUser() { diff --git a/app/controllers/login.js b/app/controllers/login.js index ed4ee1c613..d4093b58fe 100644 --- a/app/controllers/login.js +++ b/app/controllers/login.js @@ -1,4 +1,6 @@ import Ember from 'ember'; +import { isAbortError, isTimeoutError } from 'ember-ajax/errors'; + let LoginController = Ember.Controller.extend({ session: Ember.inject.service(), errorMessage: null, @@ -11,8 +13,14 @@ let LoginController = Ember.Controller.extend({ this.get('session').authenticate('authenticator:custom', { identification, password - }).catch(() => { - this.set('errorMessage', true); + }).catch((err) => { + if (isAbortError(err) || isTimeoutError(err)) { + this.set('errorMessage', false); + this.set('offlineError', true); + } else { + this.set('errorMessage', true); + this.set('offlineError', false); + } }); } } diff --git a/app/controllers/navigation.js b/app/controllers/navigation.js index 8b1a4fbfb4..458dcf7f87 100644 --- a/app/controllers/navigation.js +++ b/app/controllers/navigation.js @@ -32,7 +32,12 @@ export default Ember.Controller.extend(HospitalRunVersion, ModalHelper, Progress invalidateSession() { let session = this.get('session'); if (session.get('isAuthenticated')) { - session.invalidate(); + session.invalidate().catch(() => { + let i18n = this.get('i18n'); + let message = i18n.t('navigation.messages.logoutFailed'); + let title = i18n.t('navigation.titles.logoutFailed'); + this.displayAlert(title, message); + }); } }, diff --git a/app/finishgauth/route.js b/app/finishgauth/route.js index 256d4e8bca..f131b6e4a1 100644 --- a/app/finishgauth/route.js +++ b/app/finishgauth/route.js @@ -1,7 +1,8 @@ import Ember from 'ember'; +import MapOauthParams from 'hospitalrun/mixins/map-oauth-params'; import SetupUserRole from 'hospitalrun/mixins/setup-user-role'; -export default Ember.Route.extend(SetupUserRole, { +export default Ember.Route.extend(MapOauthParams, SetupUserRole, { config: Ember.inject.service(), database: Ember.inject.service(), session: Ember.inject.service(), @@ -11,19 +12,6 @@ export default Ember.Route.extend(SetupUserRole, { google_auth: true, params }); - let oauthConfigs = { - config_consumer_key: params.k, - config_consumer_secret: params.s1, - config_oauth_token: params.t, - config_token_secret: params.s2 - }; - return this.get('config').saveOauthConfigs(oauthConfigs) - .then(function() { - oauthConfigs.config_use_google_auth = true; - return this.get('database').setup(oauthConfigs).then(() => { - return this.setupUserRole(); - }); - }.bind(this)); } } }); diff --git a/app/index.html b/app/index.html index 82b0b24c4e..6267af957e 100644 --- a/app/index.html +++ b/app/index.html @@ -1,5 +1,5 @@ - + @@ -29,18 +29,6 @@

Loading

- {{content-for "body"}} diff --git a/app/locales/en/translations.js b/app/locales/en/translations.js index f468749145..ab79434c82 100644 --- a/app/locales/en/translations.js +++ b/app/locales/en/translations.js @@ -922,6 +922,7 @@ export default { }, messages: { error: 'Username or password is incorrect.', + offlineError: 'Cannot login while offline. Please establish a network connection and retry login.', signIn: 'please sign in' } }, @@ -1081,6 +1082,9 @@ export default { inventory: 'Inventory', labs: 'Labs', medication: 'Medication', + messages: { + logoutFailed: 'Could not logout at this time. Logout is not available while offline.' + }, patients: 'Patients', scheduling: 'Scheduling', subnav: { @@ -1123,6 +1127,9 @@ export default { userRoles: 'User Roles', users: 'Users', workflow: 'Workflow' + }, + titles: { + logoutFailed: 'Logout Failed' } }, operationReport: { diff --git a/app/mixins/map-oauth-params.js b/app/mixins/map-oauth-params.js new file mode 100644 index 0000000000..4b5fad664d --- /dev/null +++ b/app/mixins/map-oauth-params.js @@ -0,0 +1,14 @@ +import Ember from 'ember'; + +const { Mixin } = Ember; + +export default Mixin.create({ + mapOauthParams(params) { + return { + config_consumer_key: params.k, + config_consumer_secret: params.s1, + config_oauth_token: params.t, + config_token_secret: params.s2 + }; + } +}); diff --git a/app/mixins/oauth-headers.js b/app/mixins/oauth-headers.js new file mode 100644 index 0000000000..1cb801dcfc --- /dev/null +++ b/app/mixins/oauth-headers.js @@ -0,0 +1,14 @@ +import Ember from 'ember'; + +const { Mixin } = Ember; + +export default Mixin.create({ + getOAuthHeaders(configs) { + return { + 'x-oauth-consumer-secret': configs.config_consumer_secret, + 'x-oauth-consumer-key': configs.config_consumer_key, + 'x-oauth-token-secret': configs.config_token_secret, + 'x-oauth-token': configs.config_oauth_token + }; + } +}); diff --git a/app/mixins/pouch-find-indexes.js b/app/mixins/pouch-find-indexes.js new file mode 100644 index 0000000000..24fdc97a01 --- /dev/null +++ b/app/mixins/pouch-find-indexes.js @@ -0,0 +1,50 @@ +import Ember from 'ember'; + +const { Mixin } = Ember; + +export default Mixin.create({ + buildPouchFindIndexes(db) { + let indexesToBuild = [{ + name: 'inventory', + fields: [ + 'data.crossReference', + 'data.description', + 'data.friendlyId', + 'data.name' + ] + }, { + name: 'invoices', + fields: [ + 'data.externalInvoiceNumber', + 'data.patientInfo' + ] + }, { + name: 'patient', + fields: [ + 'data.externalPatientId', + 'data.firstName', + 'data.friendlyId', + 'data.lastName', + 'data.phone' + ] + }, { + name: 'medication', + fields: [ + 'data.prescription' + ] + }, { + name: 'pricing', + fields: [ + 'data.name' + ] + }]; + indexesToBuild.forEach(function(index) { + db.createIndex({ + index: { + fields: index.fields, + name: index.name + } + }); + }); + } +}); diff --git a/app/routes/application.js b/app/routes/application.js index 898ebfc7f2..06a7a7ce0a 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -82,21 +82,25 @@ let ApplicationRoute = Route.extend(ApplicationRouteMixin, ModalHelper, SetupUse let session = get(this, 'session'); let isAuthenticated = session && get(session, 'isAuthenticated'); let config = get(this, 'config'); - return config.setup().then(function(configs) { + let database = get(this, 'database'); + + return config.setup().then(() => { + let standAlone = config.get('standAlone'); if (transition.targetName !== 'finishgauth' && transition.targetName !== 'login') { - let standAlone = config.get('standAlone'); set(this, 'shouldSetupUserRole', true); if (isAuthenticated || standAlone) { - return get(this, 'database').setup(configs) + return database.setup() .catch(() => { // Error thrown indicates missing auth, so invalidate session. session.invalidate(); }); } + } else if (transition.targetName === 'login' && standAlone) { + return database.createUsersDB(); } else if (transition.targetName === 'finishgauth') { set(this, 'shouldSetupUserRole', false); } - }.bind(this)); + }); }, afterModel() { diff --git a/app/services/config.js b/app/services/config.js index 025c1221d1..f6cc09e9b9 100644 --- a/app/services/config.js +++ b/app/services/config.js @@ -64,6 +64,7 @@ export default Ember.Service.extend({ 'config_consumer_key', 'config_consumer_secret', 'config_disable_offline_sync', + 'config_external_search', 'config_oauth_token', 'config_token_secret', 'config_use_google_auth' diff --git a/app/services/database.js b/app/services/database.js index be435e4c4c..3ae0305a32 100644 --- a/app/services/database.js +++ b/app/services/database.js @@ -1,9 +1,11 @@ -/* global buildPouchFindIndexes */ import Ember from 'ember'; import createPouchViews from 'hospitalrun/utils/pouch-views'; import List from 'npm:pouchdb-list'; +import OAuthHeaders from 'hospitalrun/mixins/oauth-headers'; import PouchAdapterMemory from 'npm:pouchdb-adapter-memory'; +import PouchFindIndexes from 'hospitalrun/mixins/pouch-find-indexes'; import PouchDBUsers from 'npm:pouchdb-users'; +import PouchDBWorker from 'npm:worker-pouch/client'; import UnauthorizedError from 'hospitalrun/utils/unauthorized-error'; const { @@ -18,38 +20,27 @@ const { set } = Ember; -export default Service.extend({ +export default Service.extend(OAuthHeaders, PouchFindIndexes, { mainDB: null, // Server DB oauthHeaders: null, - setMainDB: false, requireLogin: true, + setMainDB: false, + usePouchFind: false, usersDB: null, // local users database for standAlone mode config: inject.service(), standAlone: alias('config.standAlone'), - createDB(configs, pouchOptions) { + createDB(configs) { let standAlone = get(this, 'standAlone'); + if (standAlone || !configs.config_external_search) { + set(this, 'usePouchFind', true); + } if (standAlone) { - return this._createLocalDB('localMainDB', pouchOptions).then((localDb) => { - buildPouchFindIndexes(localDb); - return localDb; - }); + let localDb = this._createLocalDB(); + return RSVP.resolve(localDb); } - return new RSVP.Promise((resolve, reject) => { - let url = `${document.location.protocol}//${document.location.host}/db/main`; - - this._createRemoteDB(url, pouchOptions) - .catch((err) => { - if ((err.status && err.status === 401) || configs.config_disable_offline_sync === true) { - reject(err); - } else { - return this._createLocalDB('localMainDB', pouchOptions); - } - }).then((db) => resolve(db)) - .catch((err) => reject(err)); - - }, 'initialize application db'); + return this._createMainDB(configs); }, getDBInfo() { @@ -67,7 +58,7 @@ export default Service.extend({ resolve(doc); } }); - }); + }, `getDocFromMainDB ${docId}`); }, /** @@ -116,6 +107,10 @@ export default Service.extend({ return get(this, 'mainDB').rel.makeDocID(idInfo); }, + getRemoteDBUrl() { + return `${document.location.protocol}//${document.location.host}/db/main`; + }, + handleErrorResponse(err) { if (!err.status) { if (err.errors && err.errors.length > 0) { @@ -149,7 +144,7 @@ export default Service.extend({ reject(err); }); }, reject); - }); + }, 'loadDBFromDump'); }, queryMainDB(queryParams, mapReduce) { @@ -174,47 +169,215 @@ export default Service.extend({ } }); } - }); + }, 'queryMainDB'); }, - setup(configs) { + setup() { PouchDB.plugin(List); - PouchDB.plugin(PouchDBUsers); - let pouchOptions = this._getOptions(configs); - return this.createDB(configs, pouchOptions).then((db) => { - set(this, 'mainDB', db); - set(this, 'setMainDB', true); - if (get(this, 'standAlone')) { - return this._createUsersDB(); - } + let config = get(this, 'config'); + return config.loadConfig().then((configs) => { + return this.createDB(configs).then((db) => { + set(this, 'mainDB', db); + set(this, 'setMainDB', true); + if (get(this, 'standAlone')) { + return this.createUsersDB(); + } else { + this.setupSubscription(configs); + } + }); }); }, - _createRemoteDB(remoteUrl, pouchOptions) { - return new RSVP.Promise(function(resolve, reject) { - let remoteDB = new PouchDB(remoteUrl, pouchOptions); - // remote db lazy created, check if db created correctly - remoteDB.info().then(()=> { - createPouchViews(remoteDB); - resolve(remoteDB); - }).catch((err) => { - console.log('error with remote db:', JSON.stringify(err, null, 2)); - reject(err); + setupSubscription(configs) { + if (!configs.config_disable_offline_sync && navigator.serviceWorker) { + let config = get(this, 'config'); + let localDB = this._createLocalDB(); + return config.getConfigValue('push_subscription').then((pushSub) => { + if (isEmpty(pushSub)) { + return localDB.id().then((dbId) => { + let dbInfo = { + id: dbId, + remoteSeq: 0 + }; + return this._getPermissionAndSubscribe(dbInfo); + }).then(() => { + return this._requestSync(); + }); + } else { + return this._requestSync(); + } }); - }); + } + }, + + _askPermission() { + return new RSVP.Promise((resolve, reject) => { + let permissionResult = Notification.requestPermission((result) => { + resolve(result); + }); + + if (permissionResult) { + permissionResult.then(resolve, reject); + } + }) + .then((permissionResult) => { + if (permissionResult !== 'granted') { + throw new Error('We weren\'t granted permission.'); + } + return permissionResult; + }, 'Ask for notification permisson'); + }, + + _createLocalDB(pouchOptions) { + let localDB = new PouchDB('localMainDB', pouchOptions); + createPouchViews(localDB); + this.buildPouchFindIndexes(localDB); + return localDB; + }, + + _createMainDB(configs) { + this._setOAuthHeaders(configs); + if (!configs.config_disable_offline_sync && navigator.serviceWorker) { + // Use pouch-worker to run the DB in the service worker + return navigator.serviceWorker.ready.then(() => { + if (navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage) { + PouchDB.adapter('worker', PouchDBWorker); + let localDB = this._createLocalDB({ + adapter: 'worker', + worker: () => navigator.serviceWorker + }); + return localDB; + } else { + return this._createRemoteDB(configs); + } + }); + } else { + return this._createRemoteDB(configs); + } }, - _createLocalDB(localDBName, pouchOptions) { - return new RSVP.Promise(function(resolve, reject) { - let localDB = new PouchDB(localDBName, pouchOptions); - localDB.info().then(() => { - createPouchViews(localDB); - resolve(localDB); - }).catch((err) => reject(err)); + _createRemoteDB(configs) { + let remoteUrl = this.getRemoteDBUrl(); + let pouchOptions = this._getOptions(configs); + let remoteDB = new PouchDB(remoteUrl, pouchOptions); + return remoteDB.info().then(()=> { + createPouchViews(remoteDB); + return remoteDB; + }).catch((err) => { + console.log('error with remote db:', JSON.stringify(err, null, 2)); + throw err; }); }, - _createUsersDB() { + _getNotificationPermissionState() { + if (navigator.permissions) { + return navigator.permissions.query({ name: 'notifications' }) + .then((result) => { + return result.state; + }); + } + return RSVP.resolve(Notification.permission); + }, + + _getPermissionAndSubscribe(dbInfo) { + return new RSVP.Promise((resolve, reject) => { + navigator.serviceWorker.ready.then((registration) => { + return this._getNotificationPermissionState().then((permission) => { + if (permission !== 'granted') { + return this._askPermission().then(() => { + return this._subscribeUserToPush(registration, dbInfo).then(resolve, reject); + }); + } else { + return this._subscribeUserToPush(registration, dbInfo).then(resolve, reject); + } + }); + }); + }, 'Get notification permission and subscribe to push'); + }, + + _urlBase64ToUint8Array(base64String) { + let padding = '='.repeat((4 - base64String.length % 4) % 4); + let base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + let rawData = window.atob(base64); + let outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; + }, + + _sendSubscriptionToServer(subscription, dbInfo) { + return new RSVP.Promise((resolve, reject) => { + return fetch('/save-subscription/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + dbInfo, + subscription + }) + }).then((response) => { + if (!response.ok) { + throw new Error('Bad status code from server.'); + } + return response.json(); + }).then((responseData) => { + if (responseData.ok !== true) { + throw new Error('There was a bad response from server.', JSON.stringify(responseData, null, 2)); + } + resolve(responseData); + }).catch(reject); + }, 'Send push subscription to server'); + }, + + _subscribeUserToPush(registration, dbInfo) { + let config = get(this, 'config'); + return config.getConfigValue('push_public_key').then((serverKey) => { + if (!serverKey) { + return; + } + let subscribeOptions = { + userVisibleOnly: true, + applicationServerKey: this._urlBase64ToUint8Array(serverKey) + }; + return new RSVP.Promise((resolve, reject) => { + return registration.pushManager.subscribe(subscribeOptions) + .then((pushSubscription) => { + let subInfo = JSON.stringify(pushSubscription); + subInfo = JSON.parse(subInfo); + return this._sendSubscriptionToServer(subInfo, dbInfo); + }).then((savedSubscription) => { + let configDB = config.getConfigDB(); + return configDB.put({ + _id: 'config_push_subscription', + value: savedSubscription.id + }).then(resolve, reject); + }).catch(reject); + }); + }, 'Subscribe user to push service.'); + }, + + _requestSync() { + return new RSVP.Promise((resolve, reject) => { + let messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = function(event) { + if (event.data.error) { + reject(event.data.error); + } else { + resolve(event.data); + } + }; + navigator.serviceWorker.controller.postMessage('remotesync', [messageChannel.port2]); + }, 'Request offline sync'); + }, + + createUsersDB() { + PouchDB.plugin(PouchDBUsers); let usersDB = new PouchDB('_users'); return usersDB.installUsersBehavior().then(() => { set(this, 'usersDB', usersDB); @@ -239,7 +402,7 @@ export default Service.extend({ _getOptions(configs) { let pouchOptions = {}; - if (configs && configs.config_use_google_auth) { + if (configs) { pouchOptions.ajax = { timeout: 30000 }; @@ -250,13 +413,7 @@ export default Service.extend({ || isEmpty(configs.config_token_secret)) { throw Error('login required'); } else { - let headers = { - 'x-oauth-consumer-secret': configs.config_consumer_secret, - 'x-oauth-consumer-key': configs.config_consumer_key, - 'x-oauth-token-secret': configs.config_token_secret, - 'x-oauth-token': configs.config_oauth_token - }; - set(this, 'oauthHeaders', headers); + let headers = get(this, 'oauthHeaders'); pouchOptions.ajax.headers = headers; } } @@ -279,6 +436,11 @@ export default Service.extend({ }); } return mappedRows; + }, + + _setOAuthHeaders(configs) { + let headers = this.getOAuthHeaders(configs); + set(this, 'oauthHeaders', headers); } }); diff --git a/app/serviceworkers/pouchdb-sync.js b/app/serviceworkers/pouchdb-sync.js index 6cc9c0c452..a0d2bea141 100644 --- a/app/serviceworkers/pouchdb-sync.js +++ b/app/serviceworkers/pouchdb-sync.js @@ -1,50 +1,324 @@ - +let allChanges = {}; let configs = false; let syncingRemote = false; let configDB = new PouchDB('config'); let localMainDB = new PouchDB('localMainDB'); +let lastServerSeq; + +function PouchError(opts) { + Error.call(opts.reason); + this.status = opts.status; + this.name = opts.error; + this.message = opts.reason; + this.error = true; +} + +function createError(err) { + let status = err.status || 500; -toolbox.router.get('/db/main/', function(request, values, options) { - logDebug('request for main info:', request.url); - return couchDBResponse(request, values, options, function() { - return localMainDB.info(); + // last argument is optional + if (err.name && err.message) { + if (err.name === 'Error' || err.name === 'TypeError') { + if (err.message.indexOf('Bad special document member') !== -1) { + err.name = 'doc_validation'; + // add more clauses here if the error name is too general + } else { + err.name = 'bad_request'; + } + } + err = { + error: err.name, + name: err.name, + reason: err.message, + message: err.message, + status + }; + } + return err; +} + +function safeEval(str) { + let target = {}; + /* jshint evil: true */ + eval(`target.target = (${str});`); + return target.target; +} + +function decodeArgs(args) { + let funcArgs = ['filter', 'map', 'reduce']; + args.forEach(function(arg) { + if (typeof arg === 'object' && arg !== null && !Array.isArray(arg)) { + funcArgs.forEach(function(funcArg) { + if (!(funcArg in arg) || arg[funcArg] === null) { + delete arg[funcArg]; + } else if (arg[funcArg].type === 'func' && arg[funcArg].func) { + arg[funcArg] = safeEval(arg[funcArg].func); + } + }); + } }); -}); + return args; +} + +function postMessage(msg, event) { + event.ports[0].postMessage(msg); +} + +function sendError(clientId, messageId, data, event) { + logDebug(' -> sendError', clientId, messageId, data); + postMessage({ + type: 'error', + id: clientId, + messageId, + content: createError(data) + }, event); +} + +function sendSuccess(clientId, messageId, data, event) { + logDebug(' -> sendSuccess', clientId, messageId); + postMessage({ + type: 'success', + id: clientId, + messageId, + content: data + }, event); +} + +function sendUpdate(clientId, messageId, data, event) { + logDebug(' -> sendUpdate', clientId, messageId); + postMessage({ + type: 'update', + id: clientId, + messageId, + content: data + }, event); +} + +function getCurrentDB(clientId) { + switch (clientId) { + case 'localMainDB': { + return Promise.resolve(localMainDB); + } + case 'hospitalrun-test-database': { + return Promise.resolve(new PouchDB('hospitalrun-test-database', { + adapter: 'memory' + })); + } + default: { + return getRemoteDB(); + } + } +} -toolbox.router.get('/db/main/_all_docs', function(request, values, options) { - logDebug('request for all docs:', request.url); - return couchDBResponse(request, values, options, function(request) { - let options = getDBOptions(request.url); - logDebug('allDocs PouchDB:', options); - return localMainDB.allDocs(options); +function dbMethod(clientId, methodName, messageId, args, event) { + let dbAdapter; + return getCurrentDB(clientId).then((db) => { + if (!db) { + return sendError(clientId, messageId, { error: 'db not found' }, event); + } + dbAdapter = db.adapter; + return db[methodName](...args); + }).then(function(res) { + sendSuccess(clientId, messageId, res, event); + switch (methodName) { + case 'put': + case 'bulkDocs': + case 'post': + case 'remove': + case 'removeAttachment': + case 'putAttachment': + remoteSync(); + } + }).catch(function(err) { + if (dbAdapter === 'http') { + // If the failure was on http, retry with local db. + return dbMethod('localMainDB', methodName, messageId, args, event); + } else { + sendError(clientId, messageId, err, event); + } }); -}); -toolbox.router.get('/db/main/_design/:design_doc/_view/:view', function(request, values, options) { - logDebug('request for view:', request.url); - return couchDBResponse(request, values, options, function(request) { - let options = getDBOptions(request.url); - let mapReduce = `${values.design_doc}/${values.view}`; - logDebug('queryPouchDB:', mapReduce, options); - return localMainDB.query(mapReduce, options); +} + +function changes(clientId, messageId, args, event) { + let [opts] = args; + if (opts && typeof opts === 'object') { + // just send all the docs anyway because we need to emit change events + // TODO: be smarter about emitting changes without building up an array + opts.returnDocs = true; + opts.return_docs = true; + } + dbMethod(clientId, 'changes', messageId, args, event); +} + +function createDatabase(clientId, messageId, args, event) { + return sendSuccess(clientId, messageId, { ok: true, exists: true }, event); +} + +function getAttachment(clientId, messageId, args, event) { + return getCurrentDB(clientId).then((db) => { + if (!db) { + return sendError(clientId, messageId, { error: 'db not found' }, event); + } + let [docId, attId, opts] = args; + if (typeof opts !== 'object') { + opts = {}; + } + return db.get(docId, opts).then(function(doc) { + if (!doc._attachments || !doc._attachments[attId]) { + throw new PouchError({ + status: 404, + error: 'not_found', + reason: 'missing' + }); + } + return db.getAttachment(...args).then(function(buff) { + sendSuccess(clientId, messageId, buff, event); + }); + }); + }).catch(function(err) { + sendError(clientId, messageId, err, event); }); -}); +} + +function destroy(clientId, messageId, args, event) { + if (clientId === 'hospitalrun-test-database') { + getCurrentDB(clientId).then((db) => { + if (!db) { + return sendError(clientId, messageId, { error: 'db not found' }, event); + } + Promise.resolve().then(() => { + return db.destroy(...args); + }).then((res) => { + sendSuccess(clientId, messageId, res, event); + }).catch((err) => { + sendError(clientId, messageId, err, event); + }); + }); + } else { + return sendError(clientId, messageId, { error: 'permission denied' }, event); + } +} -toolbox.router.post('/db/main/_bulk_docs', function(request, values, options) { - logDebug('request for bulk docs:', request.url); - let pouchRequest = request.clone(); - return couchDBResponse(request, values, options, function() { - logDebug('couch failed, trying pouch request:', request.url); - return pouchRequest.json().then(function(jsonRequest) { - logDebug('got bulk docs, jsonRequest is:', jsonRequest); - return localMainDB.bulkDocs(jsonRequest); - }).catch(function(err) { - logDebug('err getting json: ', err); +function liveChanges(clientId, messageId, args, event) { + getCurrentDB(clientId).then((db) => { + if (!db) { + return sendError(clientId, messageId, { error: 'db not found' }, event); + } + let changes = db.changes(args[0]); + allChanges[messageId] = changes; + changes.on('change', function(change) { + sendUpdate(clientId, messageId, change, event); + }).on('complete', function(change) { + changes.removeAllListeners(); + delete allChanges[messageId]; + sendSuccess(clientId, messageId, change, event); + }).on('error', function(change) { + changes.removeAllListeners(); + delete allChanges[messageId]; + sendError(clientId, messageId, change, event); }); }); +} + +function cancelChanges(messageId) { + let changes = allChanges[messageId]; + if (changes) { + changes.cancel(); + } +} + +function onReceiveMessage(clientId, type, messageId, args, event) { + switch (type) { + case 'createDatabase': + return createDatabase(clientId, messageId, args, event); + case 'id': + sendSuccess(clientId, messageId, clientId, event); + return; + case 'info': + case 'put': + case 'allDocs': + case 'bulkDocs': + case 'post': + case 'get': + case 'remove': + case 'revsDiff': + case 'compact': + case 'viewCleanup': + case 'removeAttachment': + case 'putAttachment': + case 'query': + return dbMethod(clientId, type, messageId, args, event); + case 'changes': + return changes(clientId, messageId, args, event); + case 'getAttachment': + return getAttachment(clientId, messageId, args, event); + case 'liveChanges': + return liveChanges(clientId, messageId, args, event); + case 'cancelChanges': + return cancelChanges(messageId); + case 'destroy': + return destroy(clientId, messageId, args, event); + default: + return sendError(clientId, messageId, { error: `unknown API method: ${type}` }, event); + } +} + +function handleMessage(message, clientId, event) { + let { type, messageId } = message; + let args = decodeArgs(message.args); + onReceiveMessage(clientId, type, messageId, args, event); +} + +self.addEventListener('push', function(event) { + if (event.data) { + let pushData = event.data.json(); + if (pushData.type === 'couchDBChange') { + logDebug(`Got couchDBChange pushed, attempting to sync to: ${pushData.seq}`); + event.waitUntil( + remoteSync(pushData.seq).then((resp) => { + logDebug(`Response from sync ${JSON.stringify(resp, null, 2)}`); + }) + ); + } else { + logDebug('Unknown push event has data and here it is: ', pushData); + } + } }); -function setupRemoteSync() { - if (!syncingRemote && configs.config_disable_offline_sync !== true) { +self.addEventListener('message', function(event) { + logDebug('got message', event); + if (event.data === 'remotesync') { + remoteSync(); + return; + } + if (!event.data || !event.data.id || !event.data.args + || !event.data.type || !event.data.messageId) { + // assume this is not a message from worker-pouch + // (e.g. the user is using the custom API instead) + return; + } + let clientId = event.data.id; + if (event.data.type === 'close') { + // logDebug('closing worker', clientId); + } else { + handleMessage(event.data, clientId, event); + } +}); + +self.addEventListener('sync', function(event) { + if (event.tag === 'remoteSync') { + event.waitUntil(remoteSync(null, true).catch((err) =>{ + if (event.lastChance) { + logDebug('Sync failed for the last time, so give up for now.', err); + } else { + logDebug('Sync failed, will try again later', err); + } + })); + } +}); + +function getRemoteDB() { + return setupConfigs().then(() => { let pouchOptions = { ajax: { headers: {}, @@ -59,27 +333,53 @@ function setupRemoteSync() { pouchOptions.ajax.headers['x-oauth-token'] = configs.config_oauth_token; } let remoteURL = `${self.location.protocol}//${self.location.host}/db/main`; - let remoteDB = new PouchDB(remoteURL, pouchOptions); - syncingRemote = localMainDB.sync(remoteDB, { - live: true, - retry: true - }).on('change', function(info) { - logDebug('local sync change', info); - }).on('paused', function() { - logDebug('local sync paused'); - // replication paused (e.g. user went offline) - }).on('active', function() { - logDebug('local sync active'); - // replicate resumed (e.g. user went back online) - }).on('denied', function(info) { - logDebug('local sync denied:', info); - // a document failed to replicate, e.g. due to permissions - }).on('complete', function(info) { - logDebug('local sync complete:', info); + return new PouchDB(remoteURL, pouchOptions); + }); +} + +function remoteSync(remoteSequence, retryingSync) { + lastServerSeq = remoteSequence; + if (!syncingRemote && configs.config_disable_offline_sync !== true) { + logDebug(`Synching local db to remoteSequence: ${remoteSequence} at: ${new Date()}`); + syncingRemote = true; + return getRemoteDB().then((remoteDB) => { + return localMainDB.sync(remoteDB); + }).then((info) => { + syncingRemote = false; + logDebug('local sync complete:', info, configs); + + // Update push subscription with latest sync info + fetch('/update-subscription/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + subscriptionId: configs.config_push_subscription, + remoteSeq: info.pull.last_seq + }) + }); // handle complete - }).on('error', function(err) { - logDebug('local sync error:', err); + if (info.pull.last_seq < lastServerSeq) { + return remoteSync(lastServerSeq); + } else { + + return true; + } + }).catch((err) => { + syncingRemote = false; + logDebug(`local sync error, register remote sync: ${new Date()}`, err); + if (retryingSync) { + throw err; + } else { + self.registration.sync.register('remoteSync'); + } }); + } else { + if (syncingRemote) { + logDebug(`Skipping sync to: ${remoteSequence} because sync is in process`); + } + return Promise.resolve(false); } } @@ -100,71 +400,3 @@ function setupConfigs() { } }); } - -function couchDBResponse(request, values, options, pouchDBFn) { - setupConfigs().then(setupRemoteSync).catch(function(err) { - logDebug('Error setting up remote sync', JSON.stringify(err, null, 2)); - }); - logDebug('Looking for couchdb response for:', request.url); - return new Promise(function(resolve, reject) { - let startTime = performance.now(); - toolbox.networkOnly(request, values, options).then(function(response) { - if (response) { - let elapsedTime = performance.now() - startTime; - resolve(response); - logPerformance(elapsedTime, request.url); - } else { - logDebug('Network first returned no response, get data from local pouch db.'); - runPouchFn(pouchDBFn, request, resolve, reject); - } - }).catch(function(err) { - logDebug('Network first returned err, get data from local pouch db:', err); - runPouchFn(pouchDBFn, request, resolve, reject); - }); - }); -} - -function convertPouchToResponse(pouchResponse) { - return new Response(JSON.stringify(pouchResponse), { - status: 200, - statusText: 'OK' - }); -} - -function getDBOptions(url) { - let returnParams = {}; - if (url.indexOf('?') > 0) { - let urlParams = url.split('?'); - let params = decodeURIComponent(urlParams[1]).split('&'); - for (let i = 0; i < params.length; i++) { - let paramParts = params[i].split('='); - returnParams[paramParts[0]] = JSON.parse(paramParts[1]); - } - } - return returnParams; -} - -function logPerformance(elapsedTime, requestUrl) { - if (configs.config_log_metrics && configs.current_user) { - let now = Date.now(); - let timingId = `timing_${configs.current_user.toLowerCase()}_${now}`; - localMainDB.put({ - _id: timingId, - elapsed: elapsedTime, - url: requestUrl - }); - } -} - -function runPouchFn(pouchDBFn, request, resolve, reject) { - if (configs.disable_offline_sync) { - reject('Offline access has been disabled.'); - } else { - pouchDBFn(request).then(function(response) { - resolve(convertPouchToResponse(response)); - }).catch(function(err) { - logDebug('POUCH error is:', err); - reject(err); - }); - } -} diff --git a/app/templates/login.hbs b/app/templates/login.hbs index a3c394589c..e49f75917d 100644 --- a/app/templates/login.hbs +++ b/app/templates/login.hbs @@ -9,6 +9,9 @@ {{#if errorMessage}} {{/if}} + {{#if offlineError}} + + {{/if}}
{{input id='identification' value=identification placeholder=(t 'login.labels.username') class='form-control'}}
diff --git a/config/environment.js b/config/environment.js index a1f09b6918..8e296545dd 100644 --- a/config/environment.js +++ b/config/environment.js @@ -59,22 +59,22 @@ module.exports = function(environment) { ENV.serviceWorker = { enabled: false, includeRegistration: false - } + }; } else { ENV.serviceWorker = { enabled: true, debug: true, excludePaths: ['manifest.appcache'], swIncludeFiles: [ - 'node_modules/pouchdb/dist/pouchdb.js' + 'vendor/pouchdb-for-sw.js' ] }; if (environment === 'production') { ENV.serviceWorker.debug = false; } } - if (environment === 'test') { - ENV.serviceWorker.includeRegistration = false; + if (environment === 'test' && !process.env.EMBER_CLI_ELECTRON) { + ENV.serviceWorker.enabled = true; } ENV.emberFullCalendar = { diff --git a/ember-cli-build.js b/ember-cli-build.js index 9c0a3052b8..8b22c0962b 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -34,7 +34,6 @@ module.exports = function(defaults) { app.import('vendor/octicons/octicons/octicons.css'); app.import('bower_components/pouchdb-load/dist/pouchdb.load.js'); app.import('bower_components/webrtc-adapter/release/adapter.js'); - app.import('vendor/pouch-find-indexes.js'); if (EmberApp.env() !== 'production') { app.import('bower_components/timekeeper/lib/timekeeper.js', { type: 'test' }); diff --git a/package.json b/package.json index b2a8e4e833..ad17361bad 100644 --- a/package.json +++ b/package.json @@ -40,16 +40,14 @@ "body-parser": "^1.14.2", "broccoli-asset-rev": "^2.4.5", "broccoli-export-text": "0.0.2", - "broccoli-funnel": "^1.0.7", - "broccoli-manifest": "0.0.7", - "broccoli-serviceworker": "0.1.4", + "broccoli-serviceworker": "0.1.6", "crypto": "0.0.3", "csv-parse": "^1.2.0", "devtron": "1.4.0", "electron-prebuilt-compile": "1.6.2", "electron-protocol-serve": "1.3.0", "electron-rebuild": "^1.5.7", - "ember-ajax": "2.5.4", + "ember-ajax": "^3.0.0", "ember-browserify": "^1.1.12", "ember-cli": "2.10.0", "ember-cli-active-link-wrapper": "0.3.2", @@ -72,7 +70,7 @@ "ember-cli-template-lint": "0.4.12", "ember-cli-test-loader": "^1.1.0", "ember-cli-uglify": "^1.2.0", - "ember-concurrency": "0.8.1", + "ember-concurrency": "0.8.3", "ember-concurrency-test-waiter": "0.2.0", "ember-data": "2.10.0", "ember-electron": "2.1.0", @@ -94,21 +92,21 @@ "eslint-plugin-ember-suave": "^1.0.0", "express": "^4.8.5", "glob": "^7.1.0", - "hospitalrun-dblisteners": "0.9.6", - "hospitalrun-server-routes": "0.9.11", + "hospitalrun-dblisteners": "1.0.0-beta", + "hospitalrun-server-routes": "1.0.0-beta", "loader.js": "^4.0.11", "nano": "6.2.0", - "pouchdb": "6.1.2", - "pouchdb-adapter-memory": "6.1.2", + "pouchdb": "6.2.0", + "pouchdb-adapter-memory": "6.2.0", "pouchdb-list": "^1.1.0", "pouchdb-users": "^1.0.3", - "pbkdf2": "3.0.9", "stylelint": "~7.7.1", "stylelint-config-concentric": "1.0.7", "stylelint-declaration-use-variable": "1.6.0", "stylelint-scss": "1.4.1", "tosource-polyfill": "^0.3.1", - "uuid": "^3.0.0" + "uuid": "^3.0.0", + "worker-pouch": "git+https://github.com/jkleinsc/worker-pouch.git" }, "dependencies": { "electron-compile": "^6.3.0", diff --git a/server/config-example.js b/server/config-example.js index 4ad07b0271..8e4e51bdf7 100644 --- a/server/config-example.js +++ b/server/config-example.js @@ -14,6 +14,16 @@ var config = { useSSL: false, imagesdir: '/tmp/patientimages', attachmentsDir: 'tmp/attachments', + disableOfflineSync: false, //Set to true to disable offline usage + /* The following settings are used to enable offline usage of HospitalRun + You will need to install web-push to generate the keys: + 1. npm install web-push -g + 2. web-push generate-vapid-keys + 3. You will need to set pushContactInfo to a valid email address + pushPublicKey: false, + pushPrivateKey: false, + pushContactInfo: 'mailto:YOUR ADMIN EMAIL HERE' + */ }; config.couchCredentials = function() { diff --git a/tests/acceptance/login-test.js b/tests/acceptance/login-test.js index 91c819a081..800f794f2b 100644 --- a/tests/acceptance/login-test.js +++ b/tests/acceptance/login-test.js @@ -43,7 +43,7 @@ test('incorrect credentials shows an error message on the screen', function(asse let errorMessage = 'Username or password is incorrect.'; - stubRequest('post', '/db/_session', function(request) { + stubRequest('post', '/auth/login', function(request) { assert.equal(request.requestBody, 'name=hradmin&password=tset', 'credential are sent to the server'); request.error({ 'error': 'unauthorized', 'reason': errorMessage }); }); @@ -62,21 +62,16 @@ test('incorrect credentials shows an error message on the screen', function(asse function login(assert, spaceAroundUsername) { if (!window.ELECTRON) { - assert.expect(3); + assert.expect(2); } runWithPouchDump('default', function() { visit('/login'); - stubRequest('post', '/db/_session', function(request) { + stubRequest('post', '/auth/login', function(request) { assert.equal(request.requestBody, 'name=hradmin&password=test', !spaceAroundUsername ? 'credential are sent to the server' : 'username trimmed and credential are sent to the server'); request.ok({ 'ok': true, 'name': 'hradmin', 'roles': ['System Administrator', 'admin', 'user'] }); }); - stubRequest('post', '/chkuser', function(request) { - assert.equal(request.requestBody, 'name=hradmin', !spaceAroundUsername ? 'username is sent to /chkuser' : 'trimmed username is sent to /chkuser'); - request.ok({ 'prefix': 'p1', 'role': 'System Administrator' }); - }); - andThen(function() { assert.equal(currentURL(), '/login'); }); @@ -84,5 +79,8 @@ function login(assert, spaceAroundUsername) { fillIn('#identification', !spaceAroundUsername ? 'hradmin' : ' hradmin'); fillIn('#password', 'test'); click('button:contains(Sign in)'); + andThen(() => { + waitToAppear('.sidebar-nav-logo'); + }); }); } diff --git a/tests/helpers/run-with-pouch-dump.js b/tests/helpers/run-with-pouch-dump.js index d35ab93995..a02b6b1cad 100644 --- a/tests/helpers/run-with-pouch-dump.js +++ b/tests/helpers/run-with-pouch-dump.js @@ -6,8 +6,10 @@ import PouchAdapterMemory from 'npm:pouchdb-adapter-memory'; import PouchDBUsers from 'npm:pouchdb-users'; import DatabaseService from 'hospitalrun/services/database'; import ConfigService from 'hospitalrun/services/config'; +import PouchDBWorker from 'npm:worker-pouch/client'; const { + get, set } = Ember; @@ -39,6 +41,7 @@ function destroyDatabases(dbs) { function runWithPouchDumpAsyncHelper(app, dumpName, functionToRun) { PouchDB.plugin(PouchAdapterMemory); PouchDB.plugin(PouchDBUsers); + let db = new PouchDB('hospitalrun-test-database', { adapter: 'memory' }); @@ -55,12 +58,36 @@ function runWithPouchDumpAsyncHelper(app, dumpName, functionToRun) { let promise = db.load(dump); let InMemoryDatabaseService = DatabaseService.extend({ - createDB() { - return promise.then(function() { - return db; - }); + + createDB(configs) { + let standAlone = get(this, 'standAlone'); + if (standAlone || !configs.config_external_search) { + set(this, 'usePouchFind', true); + } + if (standAlone) { + return promise.then(() => db); + } + if (!window.ELECTRON && navigator.serviceWorker) { + // Use pouch-worker to run the DB in the service worker + return navigator.serviceWorker.ready.then(() => { + if (navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage) { + PouchDB.adapter('worker', PouchDBWorker); + db = new PouchDB('hospitalrun-test-database', { + adapter: 'worker', + worker: () => navigator.serviceWorker + }); + return db.load(dump).then(() => { + return db; + }); + } else { + return promise.then(() => db); + } + }); + } else { + return promise.then(() => db); + } }, - _createUsersDB() { + createUsersDB() { return usersDB.installUsersBehavior().then(() => { set(this, 'usersDB', usersDB); return usersDB.put({ diff --git a/tests/index.html b/tests/index.html index 33ad9ff0de..e873a91285 100644 --- a/tests/index.html +++ b/tests/index.html @@ -29,5 +29,16 @@ {{content-for "body-footer"}} {{content-for "test-body-footer"}} + diff --git a/vendor/pouch-find-indexes.js b/vendor/pouch-find-indexes.js deleted file mode 100644 index 62242153b4..0000000000 --- a/vendor/pouch-find-indexes.js +++ /dev/null @@ -1,45 +0,0 @@ - -function buildPouchFindIndexes(db) { - var indexesToBuild = [{ - name: 'inventory', - fields: [ - 'data.crossReference', - 'data.description', - 'data.friendlyId', - 'data.name' - ] - }, { - name: 'invoices', - fields: [ - 'data.externalInvoiceNumber', - 'data.patientInfo' - ] - }, { - name: 'patient', - fields: [ - 'data.externalPatientId', - 'data.firstName', - 'data.friendlyId', - 'data.lastName', - 'data.phone' - ] - }, { - name: 'medication', - fields: [ - 'data.prescription' - ] - }, { - name: 'pricing', - fields: [ - 'data.name' - ] - }]; - indexesToBuild.forEach(function(index) { - db.createIndex({ - index: { - fields: index.fields, - name: index.name - } - }); - }); -} diff --git a/vendor/pouchdb-for-sw-src.js b/vendor/pouchdb-for-sw-src.js new file mode 100644 index 0000000000..2367ec50cd --- /dev/null +++ b/vendor/pouchdb-for-sw-src.js @@ -0,0 +1,7 @@ +self.PouchDB = require('pouchdb-core') + .plugin(require('pouchdb-adapter-idb')) + .plugin(require('pouchdb-adapter-http')) + .plugin(require('pouchdb-mapreduce')) + .plugin(require('pouchdb-replication')) + .plugin(require('pouchdb-adapter-memory')) + .plugin(require('pouchdb-find')) diff --git a/vendor/pouchdb-for-sw.js b/vendor/pouchdb-for-sw.js new file mode 100644 index 0000000000..fa6630214a --- /dev/null +++ b/vendor/pouchdb-for-sw.js @@ -0,0 +1,33984 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o>>= 0 + + var maxLength = obj.byteLength - byteOffset + + if (maxLength < 0) { + throw new RangeError("'offset' is out of bounds") + } + + if (length === undefined) { + length = maxLength + } else { + length >>>= 0 + + if (length > maxLength) { + throw new RangeError("'length' is out of bounds") + } + } + + return isModern + ? Buffer.from(obj.slice(byteOffset, byteOffset + length)) + : new Buffer(new Uint8Array(obj.slice(byteOffset, byteOffset + length))) +} + +function fromString (string, encoding) { + if (typeof encoding !== 'string' || encoding === '') { + encoding = 'utf8' + } + + if (!Buffer.isEncoding(encoding)) { + throw new TypeError('"encoding" must be a valid string encoding') + } + + return isModern + ? Buffer.from(string, encoding) + : new Buffer(string, encoding) +} + +function bufferFrom (value, encodingOrOffset, length) { + if (typeof value === 'number') { + throw new TypeError('"value" argument must not be a number') + } + + if (isArrayBuffer(value)) { + return fromArrayBuffer(value, encodingOrOffset, length) + } + + if (typeof value === 'string') { + return fromString(value, encodingOrOffset) + } + + return isModern + ? Buffer.from(value) + : new Buffer(value) +} + +module.exports = bufferFrom + +}).call(this,require("buffer").Buffer) +},{"buffer":162,"is-array-buffer-x":22}],3:[function(require,module,exports){ +(function (global){ +'use strict'; + +var buffer = require('buffer'); +var Buffer = buffer.Buffer; +var SlowBuffer = buffer.SlowBuffer; +var MAX_LEN = buffer.kMaxLength || 2147483647; +exports.alloc = function alloc(size, fill, encoding) { + if (typeof Buffer.alloc === 'function') { + return Buffer.alloc(size, fill, encoding); + } + if (typeof encoding === 'number') { + throw new TypeError('encoding must not be number'); + } + if (typeof size !== 'number') { + throw new TypeError('size must be a number'); + } + if (size > MAX_LEN) { + throw new RangeError('size is too large'); + } + var enc = encoding; + var _fill = fill; + if (_fill === undefined) { + enc = undefined; + _fill = 0; + } + var buf = new Buffer(size); + if (typeof _fill === 'string') { + var fillBuf = new Buffer(_fill, enc); + var flen = fillBuf.length; + var i = -1; + while (++i < size) { + buf[i] = fillBuf[i % flen]; + } + } else { + buf.fill(_fill); + } + return buf; +} +exports.allocUnsafe = function allocUnsafe(size) { + if (typeof Buffer.allocUnsafe === 'function') { + return Buffer.allocUnsafe(size); + } + if (typeof size !== 'number') { + throw new TypeError('size must be a number'); + } + if (size > MAX_LEN) { + throw new RangeError('size is too large'); + } + return new Buffer(size); +} +exports.from = function from(value, encodingOrOffset, length) { + if (typeof Buffer.from === 'function' && (!global.Uint8Array || Uint8Array.from !== Buffer.from)) { + return Buffer.from(value, encodingOrOffset, length); + } + if (typeof value === 'number') { + throw new TypeError('"value" argument must not be a number'); + } + if (typeof value === 'string') { + return new Buffer(value, encodingOrOffset); + } + if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) { + var offset = encodingOrOffset; + if (arguments.length === 1) { + return new Buffer(value); + } + if (typeof offset === 'undefined') { + offset = 0; + } + var len = length; + if (typeof len === 'undefined') { + len = value.byteLength - offset; + } + if (offset >= value.byteLength) { + throw new RangeError('\'offset\' is out of bounds'); + } + if (len > value.byteLength - offset) { + throw new RangeError('\'length\' is out of bounds'); + } + return new Buffer(value.slice(offset, offset + len)); + } + if (Buffer.isBuffer(value)) { + var out = new Buffer(value.length); + value.copy(out, 0, 0, value.length); + return out; + } + if (value) { + if (Array.isArray(value) || (typeof ArrayBuffer !== 'undefined' && value.buffer instanceof ArrayBuffer) || 'length' in value) { + return new Buffer(value); + } + if (value.type === 'Buffer' && Array.isArray(value.data)) { + return new Buffer(value.data); + } + } + + throw new TypeError('First argument must be a string, Buffer, ' + 'ArrayBuffer, Array, or array-like object.'); +} +exports.allocUnsafeSlow = function allocUnsafeSlow(size) { + if (typeof Buffer.allocUnsafeSlow === 'function') { + return Buffer.allocUnsafeSlow(size); + } + if (typeof size !== 'number') { + throw new TypeError('size must be a number'); + } + if (size >= MAX_LEN) { + throw new RangeError('size is too large'); + } + return new SlowBuffer(size); +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"buffer":162}],4:[function(require,module,exports){ +(function (Buffer){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// NOTE: These type checking functions intentionally don't use `instanceof` +// because it is fragile and can be easily faked with `Object.create()`. + +function isArray(arg) { + if (Array.isArray) { + return Array.isArray(arg); + } + return objectToString(arg) === '[object Array]'; +} +exports.isArray = isArray; + +function isBoolean(arg) { + return typeof arg === 'boolean'; +} +exports.isBoolean = isBoolean; + +function isNull(arg) { + return arg === null; +} +exports.isNull = isNull; + +function isNullOrUndefined(arg) { + return arg == null; +} +exports.isNullOrUndefined = isNullOrUndefined; + +function isNumber(arg) { + return typeof arg === 'number'; +} +exports.isNumber = isNumber; + +function isString(arg) { + return typeof arg === 'string'; +} +exports.isString = isString; + +function isSymbol(arg) { + return typeof arg === 'symbol'; +} +exports.isSymbol = isSymbol; + +function isUndefined(arg) { + return arg === void 0; +} +exports.isUndefined = isUndefined; + +function isRegExp(re) { + return objectToString(re) === '[object RegExp]'; +} +exports.isRegExp = isRegExp; + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} +exports.isObject = isObject; + +function isDate(d) { + return objectToString(d) === '[object Date]'; +} +exports.isDate = isDate; + +function isError(e) { + return (objectToString(e) === '[object Error]' || e instanceof Error); +} +exports.isError = isError; + +function isFunction(arg) { + return typeof arg === 'function'; +} +exports.isFunction = isFunction; + +function isPrimitive(arg) { + return arg === null || + typeof arg === 'boolean' || + typeof arg === 'number' || + typeof arg === 'string' || + typeof arg === 'symbol' || // ES6 symbol + typeof arg === 'undefined'; +} +exports.isPrimitive = isPrimitive; + +exports.isBuffer = Buffer.isBuffer; + +function objectToString(o) { + return Object.prototype.toString.call(o); +} + +}).call(this,{"isBuffer":require("../../../../../../../../usr/local/lib/node_modules/browserify/node_modules/is-buffer/index.js")}) +},{"../../../../../../../../usr/local/lib/node_modules/browserify/node_modules/is-buffer/index.js":167}],5:[function(require,module,exports){ +(function (process){ +/** + * This is the web browser implementation of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = require('./debug'); +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; +exports.storage = 'undefined' != typeof chrome + && 'undefined' != typeof chrome.storage + ? chrome.storage.local + : localstorage(); + +/** + * Colors. + */ + +exports.colors = [ + 'lightseagreen', + 'forestgreen', + 'goldenrod', + 'dodgerblue', + 'darkorchid', + 'crimson' +]; + +/** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window && typeof window.process !== 'undefined' && window.process.type === 'renderer') { + return true; + } + + // is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document && 'WebkitAppearance' in document.documentElement.style) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window && window.console && (console.firebug || (console.exception && console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); +} + +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + +exports.formatters.j = function(v) { + try { + return JSON.stringify(v); + } catch (err) { + return '[UnexpectedJSONParseError]: ' + err.message; + } +}; + + +/** + * Colorize log arguments if enabled. + * + * @api public + */ + +function formatArgs(args) { + var useColors = this.useColors; + + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); + + if (!useColors) return; + + var c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit') + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); +} + +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; +} + +/** + * Enable namespaces listed in `localStorage.debug` initially. + */ + +exports.enable(load()); + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + +function localstorage() { + try { + return window.localStorage; + } catch (e) {} +} + +}).call(this,require('_process')) +},{"./debug":6,"_process":170}],6:[function(require,module,exports){ + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = require('ms'); + +/** + * The currently active debug mode names, and names to skip. + */ + +exports.names = []; +exports.skips = []; + +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + +exports.formatters = {}; + +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * @param {String} namespace + * @return {Number} + * @api private + */ + +function selectColor(namespace) { + var hash = 0, i; + + for (i in namespace) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return exports.colors[Math.abs(hash) % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function createDebug(namespace) { + + function debug() { + // disabled? + if (!debug.enabled) return; + + var self = debug; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // turn the `arguments` into a proper Array + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %O + args.unshift('%O'); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // apply env-specific formatting (colors, etc.) + exports.formatArgs.call(self, args); + + var logFn = debug.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.enabled = exports.enabled(namespace); + debug.useColors = exports.useColors(); + debug.color = selectColor(namespace); + + // env-specific initialization logic for debug instances + if ('function' === typeof exports.init) { + exports.init(debug); + } + + return debug; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + exports.names = []; + exports.skips = []; + + var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + +},{"ms":55}],7:[function(require,module,exports){ +var util = require('util') + , AbstractIterator = require('abstract-leveldown').AbstractIterator + + +function DeferredIterator (options) { + AbstractIterator.call(this, options) + + this._options = options + this._iterator = null + this._operations = [] +} + +util.inherits(DeferredIterator, AbstractIterator) + +DeferredIterator.prototype.setDb = function (db) { + var it = this._iterator = db.iterator(this._options) + this._operations.forEach(function (op) { + it[op.method].apply(it, op.args) + }) +} + +DeferredIterator.prototype._operation = function (method, args) { + if (this._iterator) + return this._iterator[method].apply(this._iterator, args) + this._operations.push({ method: method, args: args }) +} + +'next end'.split(' ').forEach(function (m) { + DeferredIterator.prototype['_' + m] = function () { + this._operation(m, arguments) + } +}) + +module.exports = DeferredIterator; + +},{"abstract-leveldown":12,"util":188}],8:[function(require,module,exports){ +(function (Buffer,process){ +var util = require('util') + , AbstractLevelDOWN = require('abstract-leveldown').AbstractLevelDOWN + , DeferredIterator = require('./deferred-iterator') + +function DeferredLevelDOWN (location) { + AbstractLevelDOWN.call(this, typeof location == 'string' ? location : '') // optional location, who cares? + this._db = undefined + this._operations = [] + this._iterators = [] +} + +util.inherits(DeferredLevelDOWN, AbstractLevelDOWN) + +// called by LevelUP when we have a real DB to take its place +DeferredLevelDOWN.prototype.setDb = function (db) { + this._db = db + this._operations.forEach(function (op) { + db[op.method].apply(db, op.args) + }) + this._iterators.forEach(function (it) { + it.setDb(db) + }) +} + +DeferredLevelDOWN.prototype._open = function (options, callback) { + return process.nextTick(callback) +} + +// queue a new deferred operation +DeferredLevelDOWN.prototype._operation = function (method, args) { + if (this._db) + return this._db[method].apply(this._db, args) + this._operations.push({ method: method, args: args }) +} + +// deferrables +'put get del batch approximateSize'.split(' ').forEach(function (m) { + DeferredLevelDOWN.prototype['_' + m] = function () { + this._operation(m, arguments) + } +}) + +DeferredLevelDOWN.prototype._isBuffer = function (obj) { + return Buffer.isBuffer(obj) +} + +DeferredLevelDOWN.prototype._iterator = function (options) { + if (this._db) + return this._db.iterator.apply(this._db, arguments) + var it = new DeferredIterator(options) + this._iterators.push(it) + return it +} + +module.exports = DeferredLevelDOWN +module.exports.DeferredIterator = DeferredIterator + +}).call(this,{"isBuffer":require("../../../../../../../usr/local/lib/node_modules/browserify/node_modules/is-buffer/index.js")},require('_process')) +},{"../../../../../../../usr/local/lib/node_modules/browserify/node_modules/is-buffer/index.js":167,"./deferred-iterator":7,"_process":170,"abstract-leveldown":12,"util":188}],9:[function(require,module,exports){ +(function (process){ +/* Copyright (c) 2013 Rod Vagg, MIT License */ + +function AbstractChainedBatch (db) { + this._db = db + this._operations = [] + this._written = false +} + +AbstractChainedBatch.prototype._checkWritten = function () { + if (this._written) + throw new Error('write() already called on this batch') +} + +AbstractChainedBatch.prototype.put = function (key, value) { + this._checkWritten() + + var err = this._db._checkKey(key, 'key', this._db._isBuffer) + if (err) + throw err + + if (!this._db._isBuffer(key)) key = String(key) + if (!this._db._isBuffer(value)) value = String(value) + + if (typeof this._put == 'function' ) + this._put(key, value) + else + this._operations.push({ type: 'put', key: key, value: value }) + + return this +} + +AbstractChainedBatch.prototype.del = function (key) { + this._checkWritten() + + var err = this._db._checkKey(key, 'key', this._db._isBuffer) + if (err) throw err + + if (!this._db._isBuffer(key)) key = String(key) + + if (typeof this._del == 'function' ) + this._del(key) + else + this._operations.push({ type: 'del', key: key }) + + return this +} + +AbstractChainedBatch.prototype.clear = function () { + this._checkWritten() + + this._operations = [] + + if (typeof this._clear == 'function' ) + this._clear() + + return this +} + +AbstractChainedBatch.prototype.write = function (options, callback) { + this._checkWritten() + + if (typeof options == 'function') + callback = options + if (typeof callback != 'function') + throw new Error('write() requires a callback argument') + if (typeof options != 'object') + options = {} + + this._written = true + + if (typeof this._write == 'function' ) + return this._write(callback) + + if (typeof this._db._batch == 'function') + return this._db._batch(this._operations, options, callback) + + process.nextTick(callback) +} + +module.exports = AbstractChainedBatch +}).call(this,require('_process')) +},{"_process":170}],10:[function(require,module,exports){ +(function (process){ +/* Copyright (c) 2013 Rod Vagg, MIT License */ + +function AbstractIterator (db) { + this.db = db + this._ended = false + this._nexting = false +} + +AbstractIterator.prototype.next = function (callback) { + var self = this + + if (typeof callback != 'function') + throw new Error('next() requires a callback argument') + + if (self._ended) + return callback(new Error('cannot call next() after end()')) + if (self._nexting) + return callback(new Error('cannot call next() before previous next() has completed')) + + self._nexting = true + if (typeof self._next == 'function') { + return self._next(function () { + self._nexting = false + callback.apply(null, arguments) + }) + } + + process.nextTick(function () { + self._nexting = false + callback() + }) +} + +AbstractIterator.prototype.end = function (callback) { + if (typeof callback != 'function') + throw new Error('end() requires a callback argument') + + if (this._ended) + return callback(new Error('end() already called on iterator')) + + this._ended = true + + if (typeof this._end == 'function') + return this._end(callback) + + process.nextTick(callback) +} + +module.exports = AbstractIterator + +}).call(this,require('_process')) +},{"_process":170}],11:[function(require,module,exports){ +(function (Buffer,process){ +/* Copyright (c) 2013 Rod Vagg, MIT License */ + +var xtend = require('xtend') + , AbstractIterator = require('./abstract-iterator') + , AbstractChainedBatch = require('./abstract-chained-batch') + +function AbstractLevelDOWN (location) { + if (!arguments.length || location === undefined) + throw new Error('constructor requires at least a location argument') + + if (typeof location != 'string') + throw new Error('constructor requires a location string argument') + + this.location = location + this.status = 'new' +} + +AbstractLevelDOWN.prototype.open = function (options, callback) { + var self = this + , oldStatus = this.status + + if (typeof options == 'function') + callback = options + + if (typeof callback != 'function') + throw new Error('open() requires a callback argument') + + if (typeof options != 'object') + options = {} + + options.createIfMissing = options.createIfMissing != false + options.errorIfExists = !!options.errorIfExists + + if (typeof this._open == 'function') { + this.status = 'opening' + this._open(options, function (err) { + if (err) { + self.status = oldStatus + return callback(err) + } + self.status = 'open' + callback() + }) + } else { + this.status = 'open' + process.nextTick(callback) + } +} + +AbstractLevelDOWN.prototype.close = function (callback) { + var self = this + , oldStatus = this.status + + if (typeof callback != 'function') + throw new Error('close() requires a callback argument') + + if (typeof this._close == 'function') { + this.status = 'closing' + this._close(function (err) { + if (err) { + self.status = oldStatus + return callback(err) + } + self.status = 'closed' + callback() + }) + } else { + this.status = 'closed' + process.nextTick(callback) + } +} + +AbstractLevelDOWN.prototype.get = function (key, options, callback) { + var err + + if (typeof options == 'function') + callback = options + + if (typeof callback != 'function') + throw new Error('get() requires a callback argument') + + if (err = this._checkKey(key, 'key', this._isBuffer)) + return callback(err) + + if (!this._isBuffer(key)) + key = String(key) + + if (typeof options != 'object') + options = {} + + options.asBuffer = options.asBuffer != false + + if (typeof this._get == 'function') + return this._get(key, options, callback) + + process.nextTick(function () { callback(new Error('NotFound')) }) +} + +AbstractLevelDOWN.prototype.put = function (key, value, options, callback) { + var err + + if (typeof options == 'function') + callback = options + + if (typeof callback != 'function') + throw new Error('put() requires a callback argument') + + if (err = this._checkKey(key, 'key', this._isBuffer)) + return callback(err) + + if (!this._isBuffer(key)) + key = String(key) + + // coerce value to string in node, don't touch it in browser + // (indexeddb can store any JS type) + if (value != null && !this._isBuffer(value) && !process.browser) + value = String(value) + + if (typeof options != 'object') + options = {} + + if (typeof this._put == 'function') + return this._put(key, value, options, callback) + + process.nextTick(callback) +} + +AbstractLevelDOWN.prototype.del = function (key, options, callback) { + var err + + if (typeof options == 'function') + callback = options + + if (typeof callback != 'function') + throw new Error('del() requires a callback argument') + + if (err = this._checkKey(key, 'key', this._isBuffer)) + return callback(err) + + if (!this._isBuffer(key)) + key = String(key) + + if (typeof options != 'object') + options = {} + + if (typeof this._del == 'function') + return this._del(key, options, callback) + + process.nextTick(callback) +} + +AbstractLevelDOWN.prototype.batch = function (array, options, callback) { + if (!arguments.length) + return this._chainedBatch() + + if (typeof options == 'function') + callback = options + + if (typeof array == 'function') + callback = array + + if (typeof callback != 'function') + throw new Error('batch(array) requires a callback argument') + + if (!Array.isArray(array)) + return callback(new Error('batch(array) requires an array argument')) + + if (!options || typeof options != 'object') + options = {} + + var i = 0 + , l = array.length + , e + , err + + for (; i < l; i++) { + e = array[i] + if (typeof e != 'object') + continue + + if (err = this._checkKey(e.type, 'type', this._isBuffer)) + return callback(err) + + if (err = this._checkKey(e.key, 'key', this._isBuffer)) + return callback(err) + } + + if (typeof this._batch == 'function') + return this._batch(array, options, callback) + + process.nextTick(callback) +} + +//TODO: remove from here, not a necessary primitive +AbstractLevelDOWN.prototype.approximateSize = function (start, end, callback) { + if ( start == null + || end == null + || typeof start == 'function' + || typeof end == 'function') { + throw new Error('approximateSize() requires valid `start`, `end` and `callback` arguments') + } + + if (typeof callback != 'function') + throw new Error('approximateSize() requires a callback argument') + + if (!this._isBuffer(start)) + start = String(start) + + if (!this._isBuffer(end)) + end = String(end) + + if (typeof this._approximateSize == 'function') + return this._approximateSize(start, end, callback) + + process.nextTick(function () { + callback(null, 0) + }) +} + +AbstractLevelDOWN.prototype._setupIteratorOptions = function (options) { + var self = this + + options = xtend(options) + + ;[ 'start', 'end', 'gt', 'gte', 'lt', 'lte' ].forEach(function (o) { + if (options[o] && self._isBuffer(options[o]) && options[o].length === 0) + delete options[o] + }) + + options.reverse = !!options.reverse + options.keys = options.keys != false + options.values = options.values != false + options.limit = 'limit' in options ? options.limit : -1 + options.keyAsBuffer = options.keyAsBuffer != false + options.valueAsBuffer = options.valueAsBuffer != false + + return options +} + +AbstractLevelDOWN.prototype.iterator = function (options) { + if (typeof options != 'object') + options = {} + + options = this._setupIteratorOptions(options) + + if (typeof this._iterator == 'function') + return this._iterator(options) + + return new AbstractIterator(this) +} + +AbstractLevelDOWN.prototype._chainedBatch = function () { + return new AbstractChainedBatch(this) +} + +AbstractLevelDOWN.prototype._isBuffer = function (obj) { + return Buffer.isBuffer(obj) +} + +AbstractLevelDOWN.prototype._checkKey = function (obj, type) { + + if (obj === null || obj === undefined) + return new Error(type + ' cannot be `null` or `undefined`') + + if (this._isBuffer(obj)) { + if (obj.length === 0) + return new Error(type + ' cannot be an empty Buffer') + } else if (String(obj) === '') + return new Error(type + ' cannot be an empty String') +} + +module.exports = AbstractLevelDOWN + +}).call(this,{"isBuffer":require("../../../../../../../../../usr/local/lib/node_modules/browserify/node_modules/is-buffer/index.js")},require('_process')) +},{"../../../../../../../../../usr/local/lib/node_modules/browserify/node_modules/is-buffer/index.js":167,"./abstract-chained-batch":9,"./abstract-iterator":10,"_process":170,"xtend":157}],12:[function(require,module,exports){ +exports.AbstractLevelDOWN = require('./abstract-leveldown') +exports.AbstractIterator = require('./abstract-iterator') +exports.AbstractChainedBatch = require('./abstract-chained-batch') +exports.isLevelDOWN = require('./is-leveldown') + +},{"./abstract-chained-batch":9,"./abstract-iterator":10,"./abstract-leveldown":11,"./is-leveldown":13}],13:[function(require,module,exports){ +var AbstractLevelDOWN = require('./abstract-leveldown') + +function isLevelDOWN (db) { + if (!db || typeof db !== 'object') + return false + return Object.keys(AbstractLevelDOWN.prototype).filter(function (name) { + // TODO remove approximateSize check when method is gone + return name[0] != '_' && name != 'approximateSize' + }).every(function (name) { + return typeof db[name] == 'function' + }) +} + +module.exports = isLevelDOWN + +},{"./abstract-leveldown":11}],14:[function(require,module,exports){ +/** + * Copyright (c) 2013 Petka Antonov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions:

+ * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +"use strict"; +function Deque(capacity) { + this._capacity = getCapacity(capacity); + this._length = 0; + this._front = 0; + if (isArray(capacity)) { + var len = capacity.length; + for (var i = 0; i < len; ++i) { + this[i] = capacity[i]; + } + this._length = len; + } +} + +Deque.prototype.toArray = function Deque$toArray() { + var len = this._length; + var ret = new Array(len); + var front = this._front; + var capacity = this._capacity; + for (var j = 0; j < len; ++j) { + ret[j] = this[(front + j) & (capacity - 1)]; + } + return ret; +}; + +Deque.prototype.push = function Deque$push(item) { + var argsLength = arguments.length; + var length = this._length; + if (argsLength > 1) { + var capacity = this._capacity; + if (length + argsLength > capacity) { + for (var i = 0; i < argsLength; ++i) { + this._checkCapacity(length + 1); + var j = (this._front + length) & (this._capacity - 1); + this[j] = arguments[i]; + length++; + this._length = length; + } + return length; + } + else { + var j = this._front; + for (var i = 0; i < argsLength; ++i) { + this[(j + length) & (capacity - 1)] = arguments[i]; + j++; + } + this._length = length + argsLength; + return length + argsLength; + } + + } + + if (argsLength === 0) return length; + + this._checkCapacity(length + 1); + var i = (this._front + length) & (this._capacity - 1); + this[i] = item; + this._length = length + 1; + return length + 1; +}; + +Deque.prototype.pop = function Deque$pop() { + var length = this._length; + if (length === 0) { + return void 0; + } + var i = (this._front + length - 1) & (this._capacity - 1); + var ret = this[i]; + this[i] = void 0; + this._length = length - 1; + return ret; +}; + +Deque.prototype.shift = function Deque$shift() { + var length = this._length; + if (length === 0) { + return void 0; + } + var front = this._front; + var ret = this[front]; + this[front] = void 0; + this._front = (front + 1) & (this._capacity - 1); + this._length = length - 1; + return ret; +}; + +Deque.prototype.unshift = function Deque$unshift(item) { + var length = this._length; + var argsLength = arguments.length; + + + if (argsLength > 1) { + var capacity = this._capacity; + if (length + argsLength > capacity) { + for (var i = argsLength - 1; i >= 0; i--) { + this._checkCapacity(length + 1); + var capacity = this._capacity; + var j = (((( this._front - 1 ) & + ( capacity - 1) ) ^ capacity ) - capacity ); + this[j] = arguments[i]; + length++; + this._length = length; + this._front = j; + } + return length; + } + else { + var front = this._front; + for (var i = argsLength - 1; i >= 0; i--) { + var j = (((( front - 1 ) & + ( capacity - 1) ) ^ capacity ) - capacity ); + this[j] = arguments[i]; + front = j; + } + this._front = front; + this._length = length + argsLength; + return length + argsLength; + } + } + + if (argsLength === 0) return length; + + this._checkCapacity(length + 1); + var capacity = this._capacity; + var i = (((( this._front - 1 ) & + ( capacity - 1) ) ^ capacity ) - capacity ); + this[i] = item; + this._length = length + 1; + this._front = i; + return length + 1; +}; + +Deque.prototype.peekBack = function Deque$peekBack() { + var length = this._length; + if (length === 0) { + return void 0; + } + var index = (this._front + length - 1) & (this._capacity - 1); + return this[index]; +}; + +Deque.prototype.peekFront = function Deque$peekFront() { + if (this._length === 0) { + return void 0; + } + return this[this._front]; +}; + +Deque.prototype.get = function Deque$get(index) { + var i = index; + if ((i !== (i | 0))) { + return void 0; + } + var len = this._length; + if (i < 0) { + i = i + len; + } + if (i < 0 || i >= len) { + return void 0; + } + return this[(this._front + i) & (this._capacity - 1)]; +}; + +Deque.prototype.isEmpty = function Deque$isEmpty() { + return this._length === 0; +}; + +Deque.prototype.clear = function Deque$clear() { + var len = this._length; + var front = this._front; + var capacity = this._capacity; + for (var j = 0; j < len; ++j) { + this[(front + j) & (capacity - 1)] = void 0; + } + this._length = 0; + this._front = 0; +}; + +Deque.prototype.toString = function Deque$toString() { + return this.toArray().toString(); +}; + +Deque.prototype.valueOf = Deque.prototype.toString; +Deque.prototype.removeFront = Deque.prototype.shift; +Deque.prototype.removeBack = Deque.prototype.pop; +Deque.prototype.insertFront = Deque.prototype.unshift; +Deque.prototype.insertBack = Deque.prototype.push; +Deque.prototype.enqueue = Deque.prototype.push; +Deque.prototype.dequeue = Deque.prototype.shift; +Deque.prototype.toJSON = Deque.prototype.toArray; + +Object.defineProperty(Deque.prototype, "length", { + get: function() { + return this._length; + }, + set: function() { + throw new RangeError(""); + } +}); + +Deque.prototype._checkCapacity = function Deque$_checkCapacity(size) { + if (this._capacity < size) { + this._resizeTo(getCapacity(this._capacity * 1.5 + 16)); + } +}; + +Deque.prototype._resizeTo = function Deque$_resizeTo(capacity) { + var oldCapacity = this._capacity; + this._capacity = capacity; + var front = this._front; + var length = this._length; + if (front + length > oldCapacity) { + var moveItemsCount = (front + length) & (oldCapacity - 1); + arrayMove(this, 0, this, oldCapacity, moveItemsCount); + } +}; + + +var isArray = Array.isArray; + +function arrayMove(src, srcIndex, dst, dstIndex, len) { + for (var j = 0; j < len; ++j) { + dst[j + dstIndex] = src[j + srcIndex]; + src[j + srcIndex] = void 0; + } +} + +function pow2AtLeast(n) { + n = n >>> 0; + n = n - 1; + n = n | (n >> 1); + n = n | (n >> 2); + n = n | (n >> 4); + n = n | (n >> 8); + n = n | (n >> 16); + return n + 1; +} + +function getCapacity(capacity) { + if (typeof capacity !== "number") { + if (isArray(capacity)) { + capacity = capacity.length; + } + else { + return 16; + } + } + return pow2AtLeast( + Math.min( + Math.max(16, capacity), 1073741824) + ); +} + +module.exports = Deque; + +},{}],15:[function(require,module,exports){ +var prr = require('prr') + +function init (type, message, cause) { + prr(this, { + type : type + , name : type + // can be passed just a 'cause' + , cause : typeof message != 'string' ? message : cause + , message : !!message && typeof message != 'string' ? message.message : message + + }, 'ewr') +} + +// generic prototype, not intended to be actually used - helpful for `instanceof` +function CustomError (message, cause) { + Error.call(this) + if (Error.captureStackTrace) + Error.captureStackTrace(this, arguments.callee) + init.call(this, 'CustomError', message, cause) +} + +CustomError.prototype = new Error() + +function createError (errno, type, proto) { + var err = function (message, cause) { + init.call(this, type, message, cause) + //TODO: the specificity here is stupid, errno should be available everywhere + if (type == 'FilesystemError') { + this.code = this.cause.code + this.path = this.cause.path + this.errno = this.cause.errno + this.message = + (errno.errno[this.cause.errno] + ? errno.errno[this.cause.errno].description + : this.cause.message) + + (this.cause.path ? ' [' + this.cause.path + ']' : '') + } + Error.call(this) + if (Error.captureStackTrace) + Error.captureStackTrace(this, arguments.callee) + } + err.prototype = !!proto ? new proto() : new CustomError() + return err +} + +module.exports = function (errno) { + var ce = function (type, proto) { + return createError(errno, type, proto) + } + return { + CustomError : CustomError + , FilesystemError : ce('FilesystemError') + , createError : ce + } +} + +},{"prr":135}],16:[function(require,module,exports){ +var all = module.exports.all = [ + { + errno: -2, + code: 'ENOENT', + description: 'no such file or directory' + }, + { + errno: -1, + code: 'UNKNOWN', + description: 'unknown error' + }, + { + errno: 0, + code: 'OK', + description: 'success' + }, + { + errno: 1, + code: 'EOF', + description: 'end of file' + }, + { + errno: 2, + code: 'EADDRINFO', + description: 'getaddrinfo error' + }, + { + errno: 3, + code: 'EACCES', + description: 'permission denied' + }, + { + errno: 4, + code: 'EAGAIN', + description: 'resource temporarily unavailable' + }, + { + errno: 5, + code: 'EADDRINUSE', + description: 'address already in use' + }, + { + errno: 6, + code: 'EADDRNOTAVAIL', + description: 'address not available' + }, + { + errno: 7, + code: 'EAFNOSUPPORT', + description: 'address family not supported' + }, + { + errno: 8, + code: 'EALREADY', + description: 'connection already in progress' + }, + { + errno: 9, + code: 'EBADF', + description: 'bad file descriptor' + }, + { + errno: 10, + code: 'EBUSY', + description: 'resource busy or locked' + }, + { + errno: 11, + code: 'ECONNABORTED', + description: 'software caused connection abort' + }, + { + errno: 12, + code: 'ECONNREFUSED', + description: 'connection refused' + }, + { + errno: 13, + code: 'ECONNRESET', + description: 'connection reset by peer' + }, + { + errno: 14, + code: 'EDESTADDRREQ', + description: 'destination address required' + }, + { + errno: 15, + code: 'EFAULT', + description: 'bad address in system call argument' + }, + { + errno: 16, + code: 'EHOSTUNREACH', + description: 'host is unreachable' + }, + { + errno: 17, + code: 'EINTR', + description: 'interrupted system call' + }, + { + errno: 18, + code: 'EINVAL', + description: 'invalid argument' + }, + { + errno: 19, + code: 'EISCONN', + description: 'socket is already connected' + }, + { + errno: 20, + code: 'EMFILE', + description: 'too many open files' + }, + { + errno: 21, + code: 'EMSGSIZE', + description: 'message too long' + }, + { + errno: 22, + code: 'ENETDOWN', + description: 'network is down' + }, + { + errno: 23, + code: 'ENETUNREACH', + description: 'network is unreachable' + }, + { + errno: 24, + code: 'ENFILE', + description: 'file table overflow' + }, + { + errno: 25, + code: 'ENOBUFS', + description: 'no buffer space available' + }, + { + errno: 26, + code: 'ENOMEM', + description: 'not enough memory' + }, + { + errno: 27, + code: 'ENOTDIR', + description: 'not a directory' + }, + { + errno: 28, + code: 'EISDIR', + description: 'illegal operation on a directory' + }, + { + errno: 29, + code: 'ENONET', + description: 'machine is not on the network' + }, + { + errno: 31, + code: 'ENOTCONN', + description: 'socket is not connected' + }, + { + errno: 32, + code: 'ENOTSOCK', + description: 'socket operation on non-socket' + }, + { + errno: 33, + code: 'ENOTSUP', + description: 'operation not supported on socket' + }, + { + errno: 34, + code: 'ENOENT', + description: 'no such file or directory' + }, + { + errno: 35, + code: 'ENOSYS', + description: 'function not implemented' + }, + { + errno: 36, + code: 'EPIPE', + description: 'broken pipe' + }, + { + errno: 37, + code: 'EPROTO', + description: 'protocol error' + }, + { + errno: 38, + code: 'EPROTONOSUPPORT', + description: 'protocol not supported' + }, + { + errno: 39, + code: 'EPROTOTYPE', + description: 'protocol wrong type for socket' + }, + { + errno: 40, + code: 'ETIMEDOUT', + description: 'connection timed out' + }, + { + errno: 41, + code: 'ECHARSET', + description: 'invalid Unicode character' + }, + { + errno: 42, + code: 'EAIFAMNOSUPPORT', + description: 'address family for hostname not supported' + }, + { + errno: 44, + code: 'EAISERVICE', + description: 'servname not supported for ai_socktype' + }, + { + errno: 45, + code: 'EAISOCKTYPE', + description: 'ai_socktype not supported' + }, + { + errno: 46, + code: 'ESHUTDOWN', + description: 'cannot send after transport endpoint shutdown' + }, + { + errno: 47, + code: 'EEXIST', + description: 'file already exists' + }, + { + errno: 48, + code: 'ESRCH', + description: 'no such process' + }, + { + errno: 49, + code: 'ENAMETOOLONG', + description: 'name too long' + }, + { + errno: 50, + code: 'EPERM', + description: 'operation not permitted' + }, + { + errno: 51, + code: 'ELOOP', + description: 'too many symbolic links encountered' + }, + { + errno: 52, + code: 'EXDEV', + description: 'cross-device link not permitted' + }, + { + errno: 53, + code: 'ENOTEMPTY', + description: 'directory not empty' + }, + { + errno: 54, + code: 'ENOSPC', + description: 'no space left on device' + }, + { + errno: 55, + code: 'EIO', + description: 'i/o error' + }, + { + errno: 56, + code: 'EROFS', + description: 'read-only file system' + }, + { + errno: 57, + code: 'ENODEV', + description: 'no such device' + }, + { + errno: 58, + code: 'ESPIPE', + description: 'invalid seek' + }, + { + errno: 59, + code: 'ECANCELED', + description: 'operation canceled' + } +] + +module.exports.errno = {} +module.exports.code = {} + +all.forEach(function (error) { + module.exports.errno[error.errno] = error + module.exports.code[error.code] = error +}) + +module.exports.custom = require('./custom')(module.exports) +module.exports.create = module.exports.custom.createError + +},{"./custom":15}],17:[function(require,module,exports){ +"use strict" + +module.exports = createRBTree + +var RED = 0 +var BLACK = 1 + +function RBNode(color, key, value, left, right, count) { + this._color = color + this.key = key + this.value = value + this.left = left + this.right = right + this._count = count +} + +function cloneNode(node) { + return new RBNode(node._color, node.key, node.value, node.left, node.right, node._count) +} + +function repaint(color, node) { + return new RBNode(color, node.key, node.value, node.left, node.right, node._count) +} + +function recount(node) { + node._count = 1 + (node.left ? node.left._count : 0) + (node.right ? node.right._count : 0) +} + +function RedBlackTree(compare, root) { + this._compare = compare + this.root = root +} + +var proto = RedBlackTree.prototype + +Object.defineProperty(proto, "keys", { + get: function() { + var result = [] + this.forEach(function(k,v) { + result.push(k) + }) + return result + } +}) + +Object.defineProperty(proto, "values", { + get: function() { + var result = [] + this.forEach(function(k,v) { + result.push(v) + }) + return result + } +}) + +//Returns the number of nodes in the tree +Object.defineProperty(proto, "length", { + get: function() { + if(this.root) { + return this.root._count + } + return 0 + } +}) + +//Insert a new item into the tree +proto.insert = function(key, value) { + var cmp = this._compare + //Find point to insert new node at + var n = this.root + var n_stack = [] + var d_stack = [] + while(n) { + var d = cmp(key, n.key) + n_stack.push(n) + d_stack.push(d) + if(d <= 0) { + n = n.left + } else { + n = n.right + } + } + //Rebuild path to leaf node + n_stack.push(new RBNode(RED, key, value, null, null, 1)) + for(var s=n_stack.length-2; s>=0; --s) { + var n = n_stack[s] + if(d_stack[s] <= 0) { + n_stack[s] = new RBNode(n._color, n.key, n.value, n_stack[s+1], n.right, n._count+1) + } else { + n_stack[s] = new RBNode(n._color, n.key, n.value, n.left, n_stack[s+1], n._count+1) + } + } + //Rebalance tree using rotations + //console.log("start insert", key, d_stack) + for(var s=n_stack.length-1; s>1; --s) { + var p = n_stack[s-1] + var n = n_stack[s] + if(p._color === BLACK || n._color === BLACK) { + break + } + var pp = n_stack[s-2] + if(pp.left === p) { + if(p.left === n) { + var y = pp.right + if(y && y._color === RED) { + //console.log("LLr") + p._color = BLACK + pp.right = repaint(BLACK, y) + pp._color = RED + s -= 1 + } else { + //console.log("LLb") + pp._color = RED + pp.left = p.right + p._color = BLACK + p.right = pp + n_stack[s-2] = p + n_stack[s-1] = n + recount(pp) + recount(p) + if(s >= 3) { + var ppp = n_stack[s-3] + if(ppp.left === pp) { + ppp.left = p + } else { + ppp.right = p + } + } + break + } + } else { + var y = pp.right + if(y && y._color === RED) { + //console.log("LRr") + p._color = BLACK + pp.right = repaint(BLACK, y) + pp._color = RED + s -= 1 + } else { + //console.log("LRb") + p.right = n.left + pp._color = RED + pp.left = n.right + n._color = BLACK + n.left = p + n.right = pp + n_stack[s-2] = n + n_stack[s-1] = p + recount(pp) + recount(p) + recount(n) + if(s >= 3) { + var ppp = n_stack[s-3] + if(ppp.left === pp) { + ppp.left = n + } else { + ppp.right = n + } + } + break + } + } + } else { + if(p.right === n) { + var y = pp.left + if(y && y._color === RED) { + //console.log("RRr", y.key) + p._color = BLACK + pp.left = repaint(BLACK, y) + pp._color = RED + s -= 1 + } else { + //console.log("RRb") + pp._color = RED + pp.right = p.left + p._color = BLACK + p.left = pp + n_stack[s-2] = p + n_stack[s-1] = n + recount(pp) + recount(p) + if(s >= 3) { + var ppp = n_stack[s-3] + if(ppp.right === pp) { + ppp.right = p + } else { + ppp.left = p + } + } + break + } + } else { + var y = pp.left + if(y && y._color === RED) { + //console.log("RLr") + p._color = BLACK + pp.left = repaint(BLACK, y) + pp._color = RED + s -= 1 + } else { + //console.log("RLb") + p.left = n.right + pp._color = RED + pp.right = n.left + n._color = BLACK + n.right = p + n.left = pp + n_stack[s-2] = n + n_stack[s-1] = p + recount(pp) + recount(p) + recount(n) + if(s >= 3) { + var ppp = n_stack[s-3] + if(ppp.right === pp) { + ppp.right = n + } else { + ppp.left = n + } + } + break + } + } + } + } + //Return new tree + n_stack[0]._color = BLACK + return new RedBlackTree(cmp, n_stack[0]) +} + + +//Visit all nodes inorder +function doVisitFull(visit, node) { + if(node.left) { + var v = doVisitFull(visit, node.left) + if(v) { return v } + } + var v = visit(node.key, node.value) + if(v) { return v } + if(node.right) { + return doVisitFull(visit, node.right) + } +} + +//Visit half nodes in order +function doVisitHalf(lo, compare, visit, node) { + var l = compare(lo, node.key) + if(l <= 0) { + if(node.left) { + var v = doVisitHalf(lo, compare, visit, node.left) + if(v) { return v } + } + var v = visit(node.key, node.value) + if(v) { return v } + } + if(node.right) { + return doVisitHalf(lo, compare, visit, node.right) + } +} + +//Visit all nodes within a range +function doVisit(lo, hi, compare, visit, node) { + var l = compare(lo, node.key) + var h = compare(hi, node.key) + var v + if(l <= 0) { + if(node.left) { + v = doVisit(lo, hi, compare, visit, node.left) + if(v) { return v } + } + if(h > 0) { + v = visit(node.key, node.value) + if(v) { return v } + } + } + if(h > 0 && node.right) { + return doVisit(lo, hi, compare, visit, node.right) + } +} + + +proto.forEach = function rbTreeForEach(visit, lo, hi) { + if(!this.root) { + return + } + switch(arguments.length) { + case 1: + return doVisitFull(visit, this.root) + break + + case 2: + return doVisitHalf(lo, this._compare, visit, this.root) + break + + case 3: + if(this._compare(lo, hi) >= 0) { + return + } + return doVisit(lo, hi, this._compare, visit, this.root) + break + } +} + +//First item in list +Object.defineProperty(proto, "begin", { + get: function() { + var stack = [] + var n = this.root + while(n) { + stack.push(n) + n = n.left + } + return new RedBlackTreeIterator(this, stack) + } +}) + +//Last item in list +Object.defineProperty(proto, "end", { + get: function() { + var stack = [] + var n = this.root + while(n) { + stack.push(n) + n = n.right + } + return new RedBlackTreeIterator(this, stack) + } +}) + +//Find the ith item in the tree +proto.at = function(idx) { + if(idx < 0) { + return new RedBlackTreeIterator(this, []) + } + var n = this.root + var stack = [] + while(true) { + stack.push(n) + if(n.left) { + if(idx < n.left._count) { + n = n.left + continue + } + idx -= n.left._count + } + if(!idx) { + return new RedBlackTreeIterator(this, stack) + } + idx -= 1 + if(n.right) { + if(idx >= n.right._count) { + break + } + n = n.right + } else { + break + } + } + return new RedBlackTreeIterator(this, []) +} + +proto.ge = function(key) { + var cmp = this._compare + var n = this.root + var stack = [] + var last_ptr = 0 + while(n) { + var d = cmp(key, n.key) + stack.push(n) + if(d <= 0) { + last_ptr = stack.length + } + if(d <= 0) { + n = n.left + } else { + n = n.right + } + } + stack.length = last_ptr + return new RedBlackTreeIterator(this, stack) +} + +proto.gt = function(key) { + var cmp = this._compare + var n = this.root + var stack = [] + var last_ptr = 0 + while(n) { + var d = cmp(key, n.key) + stack.push(n) + if(d < 0) { + last_ptr = stack.length + } + if(d < 0) { + n = n.left + } else { + n = n.right + } + } + stack.length = last_ptr + return new RedBlackTreeIterator(this, stack) +} + +proto.lt = function(key) { + var cmp = this._compare + var n = this.root + var stack = [] + var last_ptr = 0 + while(n) { + var d = cmp(key, n.key) + stack.push(n) + if(d > 0) { + last_ptr = stack.length + } + if(d <= 0) { + n = n.left + } else { + n = n.right + } + } + stack.length = last_ptr + return new RedBlackTreeIterator(this, stack) +} + +proto.le = function(key) { + var cmp = this._compare + var n = this.root + var stack = [] + var last_ptr = 0 + while(n) { + var d = cmp(key, n.key) + stack.push(n) + if(d >= 0) { + last_ptr = stack.length + } + if(d < 0) { + n = n.left + } else { + n = n.right + } + } + stack.length = last_ptr + return new RedBlackTreeIterator(this, stack) +} + +//Finds the item with key if it exists +proto.find = function(key) { + var cmp = this._compare + var n = this.root + var stack = [] + while(n) { + var d = cmp(key, n.key) + stack.push(n) + if(d === 0) { + return new RedBlackTreeIterator(this, stack) + } + if(d <= 0) { + n = n.left + } else { + n = n.right + } + } + return new RedBlackTreeIterator(this, []) +} + +//Removes item with key from tree +proto.remove = function(key) { + var iter = this.find(key) + if(iter) { + return iter.remove() + } + return this +} + +//Returns the item at `key` +proto.get = function(key) { + var cmp = this._compare + var n = this.root + while(n) { + var d = cmp(key, n.key) + if(d === 0) { + return n.value + } + if(d <= 0) { + n = n.left + } else { + n = n.right + } + } + return +} + +//Iterator for red black tree +function RedBlackTreeIterator(tree, stack) { + this.tree = tree + this._stack = stack +} + +var iproto = RedBlackTreeIterator.prototype + +//Test if iterator is valid +Object.defineProperty(iproto, "valid", { + get: function() { + return this._stack.length > 0 + } +}) + +//Node of the iterator +Object.defineProperty(iproto, "node", { + get: function() { + if(this._stack.length > 0) { + return this._stack[this._stack.length-1] + } + return null + }, + enumerable: true +}) + +//Makes a copy of an iterator +iproto.clone = function() { + return new RedBlackTreeIterator(this.tree, this._stack.slice()) +} + +//Swaps two nodes +function swapNode(n, v) { + n.key = v.key + n.value = v.value + n.left = v.left + n.right = v.right + n._color = v._color + n._count = v._count +} + +//Fix up a double black node in a tree +function fixDoubleBlack(stack) { + var n, p, s, z + for(var i=stack.length-1; i>=0; --i) { + n = stack[i] + if(i === 0) { + n._color = BLACK + return + } + //console.log("visit node:", n.key, i, stack[i].key, stack[i-1].key) + p = stack[i-1] + if(p.left === n) { + //console.log("left child") + s = p.right + if(s.right && s.right._color === RED) { + //console.log("case 1: right sibling child red") + s = p.right = cloneNode(s) + z = s.right = cloneNode(s.right) + p.right = s.left + s.left = p + s.right = z + s._color = p._color + n._color = BLACK + p._color = BLACK + z._color = BLACK + recount(p) + recount(s) + if(i > 1) { + var pp = stack[i-2] + if(pp.left === p) { + pp.left = s + } else { + pp.right = s + } + } + stack[i-1] = s + return + } else if(s.left && s.left._color === RED) { + //console.log("case 1: left sibling child red") + s = p.right = cloneNode(s) + z = s.left = cloneNode(s.left) + p.right = z.left + s.left = z.right + z.left = p + z.right = s + z._color = p._color + p._color = BLACK + s._color = BLACK + n._color = BLACK + recount(p) + recount(s) + recount(z) + if(i > 1) { + var pp = stack[i-2] + if(pp.left === p) { + pp.left = z + } else { + pp.right = z + } + } + stack[i-1] = z + return + } + if(s._color === BLACK) { + if(p._color === RED) { + //console.log("case 2: black sibling, red parent", p.right.value) + p._color = BLACK + p.right = repaint(RED, s) + return + } else { + //console.log("case 2: black sibling, black parent", p.right.value) + p.right = repaint(RED, s) + continue + } + } else { + //console.log("case 3: red sibling") + s = cloneNode(s) + p.right = s.left + s.left = p + s._color = p._color + p._color = RED + recount(p) + recount(s) + if(i > 1) { + var pp = stack[i-2] + if(pp.left === p) { + pp.left = s + } else { + pp.right = s + } + } + stack[i-1] = s + stack[i] = p + if(i+1 < stack.length) { + stack[i+1] = n + } else { + stack.push(n) + } + i = i+2 + } + } else { + //console.log("right child") + s = p.left + if(s.left && s.left._color === RED) { + //console.log("case 1: left sibling child red", p.value, p._color) + s = p.left = cloneNode(s) + z = s.left = cloneNode(s.left) + p.left = s.right + s.right = p + s.left = z + s._color = p._color + n._color = BLACK + p._color = BLACK + z._color = BLACK + recount(p) + recount(s) + if(i > 1) { + var pp = stack[i-2] + if(pp.right === p) { + pp.right = s + } else { + pp.left = s + } + } + stack[i-1] = s + return + } else if(s.right && s.right._color === RED) { + //console.log("case 1: right sibling child red") + s = p.left = cloneNode(s) + z = s.right = cloneNode(s.right) + p.left = z.right + s.right = z.left + z.right = p + z.left = s + z._color = p._color + p._color = BLACK + s._color = BLACK + n._color = BLACK + recount(p) + recount(s) + recount(z) + if(i > 1) { + var pp = stack[i-2] + if(pp.right === p) { + pp.right = z + } else { + pp.left = z + } + } + stack[i-1] = z + return + } + if(s._color === BLACK) { + if(p._color === RED) { + //console.log("case 2: black sibling, red parent") + p._color = BLACK + p.left = repaint(RED, s) + return + } else { + //console.log("case 2: black sibling, black parent") + p.left = repaint(RED, s) + continue + } + } else { + //console.log("case 3: red sibling") + s = cloneNode(s) + p.left = s.right + s.right = p + s._color = p._color + p._color = RED + recount(p) + recount(s) + if(i > 1) { + var pp = stack[i-2] + if(pp.right === p) { + pp.right = s + } else { + pp.left = s + } + } + stack[i-1] = s + stack[i] = p + if(i+1 < stack.length) { + stack[i+1] = n + } else { + stack.push(n) + } + i = i+2 + } + } + } +} + +//Removes item at iterator from tree +iproto.remove = function() { + var stack = this._stack + if(stack.length === 0) { + return this.tree + } + //First copy path to node + var cstack = new Array(stack.length) + var n = stack[stack.length-1] + cstack[cstack.length-1] = new RBNode(n._color, n.key, n.value, n.left, n.right, n._count) + for(var i=stack.length-2; i>=0; --i) { + var n = stack[i] + if(n.left === stack[i+1]) { + cstack[i] = new RBNode(n._color, n.key, n.value, cstack[i+1], n.right, n._count) + } else { + cstack[i] = new RBNode(n._color, n.key, n.value, n.left, cstack[i+1], n._count) + } + } + + //Get node + n = cstack[cstack.length-1] + //console.log("start remove: ", n.value) + + //If not leaf, then swap with previous node + if(n.left && n.right) { + //console.log("moving to leaf") + + //First walk to previous leaf + var split = cstack.length + n = n.left + while(n.right) { + cstack.push(n) + n = n.right + } + //Copy path to leaf + var v = cstack[split-1] + cstack.push(new RBNode(n._color, v.key, v.value, n.left, n.right, n._count)) + cstack[split-1].key = n.key + cstack[split-1].value = n.value + + //Fix up stack + for(var i=cstack.length-2; i>=split; --i) { + n = cstack[i] + cstack[i] = new RBNode(n._color, n.key, n.value, n.left, cstack[i+1], n._count) + } + cstack[split-1].left = cstack[split] + } + //console.log("stack=", cstack.map(function(v) { return v.value })) + + //Remove leaf node + n = cstack[cstack.length-1] + if(n._color === RED) { + //Easy case: removing red leaf + //console.log("RED leaf") + var p = cstack[cstack.length-2] + if(p.left === n) { + p.left = null + } else if(p.right === n) { + p.right = null + } + cstack.pop() + for(var i=0; i 0) { + return this._stack[this._stack.length-1].key + } + return + }, + enumerable: true +}) + +//Returns value +Object.defineProperty(iproto, "value", { + get: function() { + if(this._stack.length > 0) { + return this._stack[this._stack.length-1].value + } + return + }, + enumerable: true +}) + + +//Returns the position of this iterator in the sorted list +Object.defineProperty(iproto, "index", { + get: function() { + var idx = 0 + var stack = this._stack + if(stack.length === 0) { + var r = this.tree.root + if(r) { + return r._count + } + return 0 + } else if(stack[stack.length-1].left) { + idx = stack[stack.length-1].left._count + } + for(var s=stack.length-2; s>=0; --s) { + if(stack[s+1] === stack[s].right) { + ++idx + if(stack[s].left) { + idx += stack[s].left._count + } + } + } + return idx + }, + enumerable: true +}) + +//Advances iterator to next element in list +iproto.next = function() { + var stack = this._stack + if(stack.length === 0) { + return + } + var n = stack[stack.length-1] + if(n.right) { + n = n.right + while(n) { + stack.push(n) + n = n.left + } + } else { + stack.pop() + while(stack.length > 0 && stack[stack.length-1].right === n) { + n = stack[stack.length-1] + stack.pop() + } + } +} + +//Checks if iterator is at end of tree +Object.defineProperty(iproto, "hasNext", { + get: function() { + var stack = this._stack + if(stack.length === 0) { + return false + } + if(stack[stack.length-1].right) { + return true + } + for(var s=stack.length-1; s>0; --s) { + if(stack[s-1].left === stack[s]) { + return true + } + } + return false + } +}) + +//Update value +iproto.update = function(value) { + var stack = this._stack + if(stack.length === 0) { + throw new Error("Can't update empty node!") + } + var cstack = new Array(stack.length) + var n = stack[stack.length-1] + cstack[cstack.length-1] = new RBNode(n._color, n.key, value, n.left, n.right, n._count) + for(var i=stack.length-2; i>=0; --i) { + n = stack[i] + if(n.left === stack[i+1]) { + cstack[i] = new RBNode(n._color, n.key, n.value, cstack[i+1], n.right, n._count) + } else { + cstack[i] = new RBNode(n._color, n.key, n.value, n.left, cstack[i+1], n._count) + } + } + return new RedBlackTree(this.tree._compare, cstack[0]) +} + +//Moves iterator backward one element +iproto.prev = function() { + var stack = this._stack + if(stack.length === 0) { + return + } + var n = stack[stack.length-1] + if(n.left) { + n = n.left + while(n) { + stack.push(n) + n = n.right + } + } else { + stack.pop() + while(stack.length > 0 && stack[stack.length-1].left === n) { + n = stack[stack.length-1] + stack.pop() + } + } +} + +//Checks if iterator is at start of tree +Object.defineProperty(iproto, "hasPrev", { + get: function() { + var stack = this._stack + if(stack.length === 0) { + return false + } + if(stack[stack.length-1].left) { + return true + } + for(var s=stack.length-1; s>0; --s) { + if(stack[s-1].right === stack[s]) { + return true + } + } + return false + } +}) + +//Default comparison function +function defaultCompare(a, b) { + if(a < b) { + return -1 + } + if(a > b) { + return 1 + } + return 0 +} + +//Build a tree +function createRBTree(compare) { + return new RedBlackTree(compare || defaultCompare, null) +} +},{}],18:[function(require,module,exports){ +/** + * @file + * + * Travis status + * + * + * Dependency status + * + * + * devDependency status + * + * + * npm version + * + * + * Tests if `Symbol` exists and creates the correct type. + * + * Requires ES3 or above. + * + * @version 1.2.0 + * @author Xotic750 + * @copyright Xotic750 + * @license {@link MIT} + * @module has-symbol-support-x + */ + +/* eslint strict: 1, symbol-description: 1 */ + +/* global module */ + +;(function () { // eslint-disable-line no-extra-semi + + 'use strict'; + + /** + * Indicates if `Symbol`exists and creates the correct type. + * `true`, if it exists and creates the correct type, otherwise `false`. + * + * @type boolean + */ + module.exports = typeof Symbol === 'function' && typeof Symbol() === 'symbol'; +}()); + +},{}],19:[function(require,module,exports){ +/** + * @file + * + * Travis status + * + * + * Dependency status + * + * + * devDependency status + * + * + * npm version + * + * + * Tests if ES6 @@toStringTag is supported. + * + * Requires ES3 or above. + * + * @see {@link http://www.ecma-international.org/ecma-262/6.0/#sec-@@tostringtag|26.3.1 @@toStringTag} + * + * @version 1.2.0 + * @author Xotic750 + * @copyright Xotic750 + * @license {@link MIT} + * @module has-to-string-tag-x + */ + +/* eslint strict: 1 */ + +/* global module */ + +;(function () { // eslint-disable-line no-extra-semi + + 'use strict'; + + /** + * Indicates if `Symbol.toStringTag`exists and is the correct type. + * `true`, if it exists and is the correct type, otherwise `false`. + * + * @type boolean + */ + module.exports = require('has-symbol-support-x') && typeof Symbol.toStringTag === 'symbol'; +}()); + +},{"has-symbol-support-x":18}],20:[function(require,module,exports){ +(function (global){ +'use strict'; +var Mutation = global.MutationObserver || global.WebKitMutationObserver; + +var scheduleDrain; + +{ + if (Mutation) { + var called = 0; + var observer = new Mutation(nextTick); + var element = global.document.createTextNode(''); + observer.observe(element, { + characterData: true + }); + scheduleDrain = function () { + element.data = (called = ++called % 2); + }; + } else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined') { + var channel = new global.MessageChannel(); + channel.port1.onmessage = nextTick; + scheduleDrain = function () { + channel.port2.postMessage(0); + }; + } else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) { + scheduleDrain = function () { + + // Create a