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}}
{{t 'login.messages.error'}}
{{/if}}
+ {{#if offlineError}}
+ {{t 'login.messages.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
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * 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
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * 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