UNPKG

rodabase

Version:

Transactional document store for Node.js and browsers. Built on LevelDB.

785 lines (670 loc) 20.7 kB
var ginga = require('ginga'), params = ginga.params, H = require('highland'), levelup = require('levelup'), transaction = require('level-transactions'), randomBytes = require('randombytes'), multiplex = require('multiplex'), EventEmitter = require('events').EventEmitter, extend = require('extend'), error = require('./error'), codec = require('./codec'), range = require('./range'), timestamp = require('./timestamp'), section = require('./section'); var MID_LEN = 8; var LOW = codec.encode(null); var defaults = {}; if(process.browser) defaults.db = require('level-js'); module.exports = function(path, opts){ //levelup instance var db = levelup(path, extend({}, defaults, opts, { keyEncoding: 'utf8', valueEncoding: 'json' })); //map of roda var roda = {}; function Roda(name){ name = String(name).trim().replace(/(#|!)/,''); if(!(this instanceof Roda)) return roda[name] || new Roda(name); if(roda[name] && this !== roda[name]) throw new Error('Roda `'+name+'` already exists.'); else roda[name] = this; this.roda = Roda; this._name = name; var pre = '/'+name; this._meta = [pre]; this._state = [pre, 's']; this._read = [pre, 'r']; this._changes = [pre, 'ch']; this._index = [pre, 'i']; this._clock = [pre, 'ck']; this._queue = [pre, 'q']; this._mapper = null; EventEmitter.call(this); //dismiss emitter warnings this.setMaxListeners(0); //replicate worker this._working = false; this._replicated = false; var self = this; var tx = Roda.transaction(); tx.lock(this.name()); tx.defer(function(cb){ self.init(tx, cb); }); tx.commit(function(err){ if(err) throw err; //init must not error replicateWorker.call(self); }); } Roda.fn = ginga(Roda.prototype); extend(Roda.prototype, EventEmitter.prototype); Roda.db = db; Roda.error = error; Roda.transaction = function(opts){ //level-transactions var tx = transaction(db, opts); //dismiss emitter warnings tx.setMaxListeners(0); return tx; }; function xparams(){ var names = Array.prototype.slice.call(arguments); var l = names.length; return function(ctx){ for(var i = 0; i < l; i++) ctx[names[i]] = ctx.args[i]; }; } //Hooks Roda.fn.define('init', xparams('transaction'), null); Roda.fn.define('validate', xparams('result'), null); Roda.fn.define('diff', xparams('current','result','transaction'), null); Roda.fn.define('conflict', xparams('conflict','result','transaction'), null); Roda.fn.use('init', function(ctx){ var self = this; var tx = ctx.transaction; var key = section(this._meta, 'mid'); tx.get(key, function(err, mid){ if(mid) self._mid = mid; }); tx.defer(function(cb){ if(self._mid) return cb(); randomBytes(MID_LEN, function(err, buf){ if(err) return cb(err); self._mid = buf.toString('base64') .replace(/\//g,'_') .replace(/\+/g,'-') .slice(0, MID_LEN); tx.put(key, self._mid, cb); }); }); }); Roda.fn.name = function(cb){ return this._name; }; Roda.fn.define('get', params('id','state:boolean?','tx?'), function(ctx, done){ var id = String(ctx.params.id).trim(); var tx = ctx.params.tx; if(tx && tx.db !== db) return done(error.INVALID_TX); if(tx) tx.lock(this.name()); //lock roda section (tx || db).get(section(this._state, id), function(err, state){ if(err && err.notFound) return done(error.notFound(id)); if(err) return done(err); if(tx){ //lamport timestamp gets-from tx._seq = Math.max(tx._seq || 0, codec.decodeNumber( state.snapshot._rev.slice(MID_LEN) )); } if(ctx.params.state === true){ done(null, state); }else{ if(state.snapshot._deleted) return done(error.notFound(id)); done(null, state.snapshot); } }); }); Roda.fn.define('getBy', params('index:string','key','tx?'), function(ctx, done){ var tx = ctx.params.tx; var key = ctx.params.key; var idx = String(ctx.params.index).trim().replace(/(#|!)/,''); if(tx && tx.db !== db) return done(error.INVALID_TX); if(tx) tx.lock(this.name()); //lock roda section (tx || db).get(section( this._index, idx, codec.encode(key) ), function(err, val){ if(err && err.notFound) return done(error.notFound(key)); if(err) return done(err); if(tx && val._rev){ //lamport timestamp gets-from tx._seq = Math.max(tx._seq || 0, codec.decodeNumber( val._rev.slice(MID_LEN) )); } done(null, val); }); }); Roda.fn.readStream = Roda.fn.createReadStream = function(opts){ opts = opts || {}; if(typeof opts.index === 'string'){ var idx = String(opts.index).trim().replace(/(#|!)/,''); return H(db.createValueStream( section(this._index, idx, range(opts)) )); }else{ return H(db.createValueStream( section(this._read, range(opts)) )); } }; function local(ctx, next, end){ var del = (!ctx.params.result) && ('id' in ctx.params); var tx = ctx.params.tx; ctx.local = true; ctx.result = extend({}, ctx.params.result); //init transaction if(tx){ if(tx.db !== db) return next(error.INVALID_TX); ctx.transaction = tx; }else{ ctx.transaction = Roda.transaction(); } end(function(err){ //err except notFound causes rollback if(err && !err.notFound) ctx.transaction.rollback(err); }); if(del) ctx.result._deleted = true; else ctx.transaction.defer(function(cb){ //trigger validate transaction hook self.validate(ctx.result, function(err){ if(err) return next(err); delete ctx.result._id; delete ctx.result._rev; delete ctx.result._key; delete ctx.result._deleted; delete ctx.result._after; delete ctx.result._from; cb(); }); }); //get roda mid var self = this; ctx.transaction.lock(this.name()); //lock roda section ctx.transaction.defer(function(cb){ if('id' in ctx.params) ctx.result._id = String(ctx.params.id).trim(); else ctx.result._id = codec.encodeNumber(timestamp()) + self._mid; //monotonic timestamp + mid next(); cb(); }); } function indexer(state, tx){ //Index Mapper var self = this; if(!this._mapper) return; //delete current indices var name, i, l, keys; if(state.indexed){ for(name in state.indexed){ keys = state.indexed[name]; for(i = 0, l = keys.length; i < l; i++){ tx.del(section( self._index, name, keys[i].map(codec.encode).join(LOW) )); } } } state.indexed = {}; //clear index when delete if(state.snapshot._deleted) return; var result = extend({}, state.snapshot); var indexed = {}; var async = false; var errorExists = null; var emit = function(name, key, value, unique){ if(async) throw new Error('Index mapper must not be async.'); if(key === null || key === undefined){ errorExists = error.KEY_NULL; return; } //optional value arg if(value === true){ unique = true; value = null; } var idx = [key]; if(unique === true){ tx.get(section( self._index, name, codec.encode(key) ), function(err, val){ if(val) errorExists = error.exists(key); }); }else{ //append unique timestamp for non-unqiue key idx.push(codec.encodeNumber(timestamp(), true)); } tx.put( section(self._index, name, idx.map(codec.encode).join(LOW)), extend({}, value || result, { _id: result._id, _key: key, _rev: result._rev }) ); //record encoded key indexed[name].push(idx); }; for(name in self._mapper){ indexed[name] = []; self._mapper[name](result, emit.bind(null, name)); } //new indexed keys state.indexed = indexed; async = true; tx.defer(function(cb){ if(errorExists) return cb(errorExists); cb(); }); } function diff(ctx, next){ var self = this; var tx = ctx.transaction; ctx.resolveConflict = false; tx.lock(this.name()); //lock roda section this.get(ctx.result._id, true, tx, function(err, state){ //return IO/other errors if(err && !err.notFound) return next(err); //has current ctx.state = state || {}; ctx.current = ctx.state.snapshot; if(ctx.current){ if(!ctx.current._deleted && ctx.errorIfExists === true) return next(error.exists(ctx.result._id)); //delete current change if(!ctx.local){ //remote write //conflict if _from mismatch and is remote write if(ctx.result._from !== ctx.current._rev && ctx.result._rev.indexOf(ctx.current._rev.slice(0, MID_LEN)) !== 0){ if(self.resolver(ctx.result) > self.resolver(ctx.current)){ //current conflict: apply result, do resolve conflict hook ctx.resolveConflict = true; //del conflicted change tx.del(section(self._changes, ctx.current._rev)); }else{ //replicate conflict: change nothing, rollback. return next(error.CONFLICT); } } } } if((!ctx.current || ctx.current._deleted) && ctx.errorIfNotExists === true) return next(error.notFound(ctx.result._id)); tx.defer(function(cb){ //diff transaction hook var current = ctx.current && !ctx.current._deleted ? extend({}, ctx.current) : null; var result = ctx.result && !ctx.result._deleted ? ctx.result : null; self.diff(current, result, tx, cb); }); next(); }); } function rev(ctx){ //lock clock var self = this; var mid = ctx.local ? this._mid : ctx.result._rev.slice(0, MID_LEN); var tx = ctx.transaction; var seq; tx.get(section(this._clock, mid), function(err, curr){ //return IO/other errors if(err && !err.notFound) return; if(ctx.local && ctx.current){ //local write, current exists if(ctx.current._rev.indexOf(self._mid) !== 0){ //read from remote: add _from ctx.result._from = ctx.current._rev; }else{ //read from local: del current change tx.del(section(self._changes, ctx.current._rev)); if(ctx.current._from) ctx.result._from = ctx.current._from; //copy _from over else delete ctx.result._from; } } //given mid i.e. local write or remote merge if(ctx.local){ //generate _rev i.e. lamport timestamp seq = codec.encodeNumber(Math.max( curr ? codec.decodeNumber(curr) : 0, //execution order tx._seq || 0 //gets from ) + 1, true); ctx.result._rev = self._mid + seq; }else{ seq = ctx.result._rev.slice(MID_LEN); } //clock update tx.put(section(self._clock, mid), seq); //put state ctx.state.snapshot = ctx.result; var enId = codec.encode(ctx.result._id); if(ctx.result._deleted){ //store del tx.del(section(self._read, enId)); }else{ //store put tx.put(section(self._read, enId), ctx.result); } //put changes tx.put(section(self._changes, ctx.result._rev), ctx.result); //Index mapper indexer.call(self, ctx.state, tx); //put state tx.defer(function(cb){ tx.put(section(self._state, ctx.result._id), ctx.state, cb); }); //conflict handling comes after diff if(ctx.resolveConflict){ tx.defer(function(cb){ self.conflict(ctx.current, ctx.result, tx, cb); }); } }); } function write(ctx, done){ var self = this; //transaction emitter ctx.transaction.once('end', function(err){ if(!err) self.emit('_write', ctx.result); }); if(ctx.params.tx){ //no need commit for non-root transaction, ctx.transaction.defer(function(cb){ done(null, ctx.result); cb(); }); }else{ ctx.transaction.commit(function(err){ if(err) done(err, null); else done(null, ctx.result); }); } } Roda.fn.define('post', params('result:object','tx?'), local, function(ctx){ ctx.errorIfExists = true; }, diff, rev, write); Roda.fn.define('put', params('id','result:object','tx?'), local, diff, rev, write); Roda.fn.define('del', params('id','tx?'), local, function(ctx){ ctx.errorIfNotExists = true; }, diff, rev, write); //custom function for resolving conflicts Roda.fn.resolver = function(doc){ return codec.seq(doc._rev); }; Roda.fn.registerIndex = function(name, fn){ this._mapper = this._mapper || {}; if(typeof name === 'string' && typeof fn === 'function'){ name = String(name).trim().replace(/(#|!)/,''); if(this._mapper[name]) throw new Error('Index mapper `'+name+'` has already been assigned.'); this._mapper[name] = fn; }else{ throw new Error('Invalid index mapper.'); } return this; }; Roda.fn.define('rebuildIndex', params('tag?'), function(ctx, next, end){ if('tag' in ctx.params){ db.get(section(this._meta, 'rebuild'), { valueEncoding:'utf8' }, function(err, tag){ if(JSON.stringify(ctx.params.tag) === tag) return next(null, true); next(); }); }else{ next(); } }, function(ctx, done){ var self = this; var errors = []; this.readStream().map(H.wrapCallback(function(data, cb){ var tx = Roda.transaction(); self.get(data._id, true, tx, function(err, state){ if(!state) return tx.rollback(); //should not happen indexer.call(self, state, tx); tx.put(section(self._state, data._id), state); }); tx.commit(cb); })) .series() .errors(function(err){ errors.push(err); }) .done(function(){ if(errors.length > 0) return done(errors); if('tag' in ctx.params){ db.put(section(self._meta, 'rebuild'), ctx.params.tag, done); }else{ done(null); } }); }); Roda.fn.liveStream = Roda.fn.createLiveStream = function(){ return H('_write', this).map(function(doc){ return extend({}, doc); }); }; Roda.fn.clocks = Roda.fn.clockStream = Roda.fn.createClockStream = function(){ return H(db.createReadStream( section(this._clock, {}) )).map(function(data){ return section(data.key) + data.value; }); }; function readChangesStream(current, limit){ if(!current) return H(db.createValueStream( section(this._changes, { limit: limit || -1 }) )); var self = this; var count = 0; return H(db.createReadStream(section(this._clock, {}))) .map(section) .reject(function(at){ return ( at.key in current && at.value <= current[at.key] ) || (limit && count >= limit); }) .map(function(at){ return H(db.createValueStream( section(self._changes, { gt: at.key + (current[at.key] || ''), lt: at.key + '~', limit: limit ? limit - count : -1 }) )); }) .series() .map(function(data){ count++; return data; }); } Roda.fn.changesStream = Roda.fn.createChangesStream = function(opts){ opts = opts || {}; var limit = opts.limit && opts.limit > 0 ? opts.limit : null; var live = opts.live === true; var self = this; var current; return H(opts.clocks || []) .collect() .map(function(clocks){ if(clocks.length > 0){ current = {}; for(var i = 0, l = clocks.length; i < l; i++){ var rev = clocks[i]; current[rev.slice(0, MID_LEN)] = rev.slice(MID_LEN); } } var rcs = readChangesStream.call(self, current, limit); current = current || {}; return ( live ? H([rcs, self.liveStream()]).series() : rcs ); }) .series() .filter(function(data){ var mid = data._rev.slice(0, MID_LEN); var seq = data._rev.slice(MID_LEN); //reject duplicates if(mid in current && seq <= current[mid]) return false; data._after = current[mid] || null; current[mid] = seq; return true; }); }; Roda.fn.replicate = Roda.fn.replicateStream = Roda.fn.createReplicateStream = function(opts){ opts = opts || {}; var self = this; var plex = multiplex(function (stream, type) { if(type === 'clock'){ //read changes self.changesStream({ clocks: H(stream).map(JSON.parse), live: true }) .map(JSON.stringify) .pipe(plex.createStream('changes')); }else if(type === 'changes'){ //queue changes H(stream) .map(JSON.parse) .reject(function(data){ //ignore local return data._rev.indexOf(self._mid) === 0; }) .map(H.wrapCallback(function(data, cb){ db.put(section(self._queue, codec.seq(data._rev)), data, cb); })) .series() .each(replicateWorker.bind(self, true)); } }); //emit self clock this.clockStream() .map(JSON.stringify) .pipe(plex.createStream('clock')); return plex; }; function replicateWorker(replicated){ if(this._working){ this._replicated = !!replicated; return; } this._working = true; this._replicated = false; var self = this; H(db.createValueStream(section(this._queue, {}))) .map(H.wrapCallback(function(doc, cb){ self._replicate(doc, function(err){ if(err){ if(err.notReady) cb(err); //time not ready, stop worker else self._conflicted(doc, cb); }else{ cb(null); } }); })) .series() .last() .pull(function(){ self._working = false; if(self._replicated > 0) replicateWorker.call(self); }); } Roda.fn.define('_replicate', params('result'), function(ctx, next, end){ ctx.local = false; ctx.result = extend({}, ctx.params.result); var self = this; var tx = ctx.transaction = Roda.transaction(); var mid = ctx.result._rev.slice(0, MID_LEN); var seq = ctx.result._rev.slice(MID_LEN); tx.lock(this.name()); //lock roda section end(function(err){ tx.rollback(err); }); if(ctx.result._from){ var fromMid = ctx.result._from.slice(0, MID_LEN); var fromSeq = ctx.result._from.slice(MID_LEN); tx.defer(function(cb){ tx.get(section(self._clock, fromMid), function(err, fromCurr){ //must gte _from seq to replicate if(!fromCurr || fromCurr < fromSeq) return next(error.NOT_READY); cb(); }); }); } tx.get(section(self._clock, mid), function(err, curr){ //to replicate must satisify curr === after var after = ctx.result._after; //curr not ready if((!curr && after) || curr < after) return next(error.NOT_READY); //delete waitlist item tx.del(section(self._queue, codec.seq(ctx.result._rev))); //already in store. Delete then next if(curr > after || curr >= seq) return tx.commit(next); delete ctx.result._after; if(!ctx.result._deleted) self.validate(ctx.result, function(err){ if(err) return next(err); next(); }); else{ next(); } }); }, diff, rev, write); Roda.fn.define('_conflicted', params('result'), function(ctx, done){ //delete waitlist, put clock, del merge, keep going var tx = Roda.transaction(); var doc = ctx.params.result; tx.del(section(this._queue, codec.seq(doc._rev))); tx.put( section(this._clock, doc._rev.slice(0, MID_LEN)), doc._rev.slice(MID_LEN) ); tx.commit(done); }); return Roda; };