UNPKG

shared-updated

Version:

Modern fork of shared (Kevin Jones), updated for latest Node.js and MongoDB

1,109 lines (957 loc) 38.4 kB
// Copyright (c) Kevin Jones. All rights reserved. Licensed under the Apache // License, Version 2.0. See LICENSE.txt in the project root for complete // license information. /// <reference path='import.ts' /> /// <reference path='tracker.ts' /> /// <reference path='types.ts' /> /// <reference path='serial.ts' /> /// <reference path='mtxfactory.ts' /> /// <reference path='store.ts' /> module shared { export module store { var util = require('util'); var rsvp = require('rsvp'); var mongo = require('mongodb'); var bson = require('bson'); var MINLOCK: number = 1; var CHECKRAND: number = 100; var MAXLOCK: number = 10000; var lockUID = utils.makeUID('000000000000000000000000'); export class MongoStore implements Store extends mtx.mtxFactory { private _logger: utils.Logger = utils.defaultLogger(); private _host: string; // Configuration private _port: number; private _dbName: string; private _collectionName: string; private _safe: bool; private _db: mongodb.Db; // Database stuff private _collection: mongodb.Collection = null; private _pending: any[] = []; // Outstanding work queue private _root: any = null; // Root object private _cache = new shared.mtx.ObjectCache(); // Cached objects private _lockRand: string; // For checking for lock changes constructor (options?: any = {}) { super(); this._host = options.host || 'localhost'; this._port = options.port || 27017; this._dbName = options.db || 'shared'; this._collectionName = options.collection || 'shared'; this._safe = options.safe || 'false'; this._logger.debug('STORE', '%s: Store created', this.id()); } apply(handler: (store: any) => any, callback?: (error: Error, arg: any) => void = function () { }): void { // Queue this._pending.push({ handler: handler, callback: callback }); // Process queue this.processPending(); } clean(callback?: (error: Error) => void = null ): void { this.apply(function (db) { // Delete all properties var keys = Object.keys(db); for (var k = 0; k < keys.length; k++) { delete db[keys[k]]; } }, function (err) { if (callback !== null) callback(err); }); } close(): void { // Queue close, this._pending.push({ close: true }); // Process queue this.processPending(); } private processPending(recurse: bool = false) { var that = this; // Processing is chained, so only start if only 1 to do if ((recurse && that._pending.length > 0) || (!recurse && that._pending.length === 1)) { var pending = that._pending[0]; if (pending.close) { // Close the db if (that._db) { that._db.close(); that._collection = null; that._db = null; that._logger.debug('STORE', '%s: Database has been closed', that.id()); } that._pending.shift(); } else if (pending.handler) { // An Update that.getRoot().then(function (root) { that.tryHandler(pending.handler).then(function (ret) { // Completed that._logger.debug('STORE', '%s: Invoking user callback', that.id()); pending.callback(null, ret); that._pending.shift(); that.processPending(true); }, function (err) { if (err) { // Some error during processing that._logger.debug('STORE', '%s: Invoking user callback with error %j', that.id(), err); pending.callback(err, null); that._pending.shift(); that.processPending(true); } else { // Needs re-try that.processPending(true); } }); }, function (err) { // No root object that._logger.debug('STORE', '%s: Invoking user callback with error %j', that.id(), err); pending.callback(err, null); that._pending.shift(); that.processPending(true); }); } else { utils.dassert(false); } } } private tryHandler(handler: (store: any) => any, done? = new rsvp.Promise()) { var that = this; try { that._logger.debug('STORE', '%s: Invoking user handler',that.id()); that.markRead(that._root); var ret = handler(that._root) try { that._logger.debug('STORE', '%s: Attempting commit',that.id()); that.commitMtx(that.mtx(that._cache)).then(function () { // It's passed :-) that._logger.debug('STORE', '%s: Update completed successfully',that.id()); that.okMtx(that._cache); done.resolve(ret); }, function (err) { if (utils.isArray(err)) { if (err.length !== 0) { that._logger.debug('STORE', 'Objects need refresh after commit failure'); that.undo(); // Refresh out-of date objects that.locked(function () { return that.refreshSet(err); }).then(function () { that._logger.debug('STORE', 'Starting re-try'); done.reject(null) }, function (err) { done.reject(err) }); } } else { done.reject(err); } }); } catch (e) { that._logger.fatal('Unhandled exception', utils.exceptionInfo(e)); } } catch (e) { that._logger.debug('STORE', 'Exception during try: ', utils.exceptionInfo(e)); // Reset any changes that.undo(); // Cache miss when trying to commit if (e instanceof tracker.UnknownReference) { var unk: tracker.UnknownReference = e; var missing = that._cache.find(unk.missing()); if (missing === null) { // Load the object var curP = that.getCollection(); curP = that.wait(curP, function () { return that.locked(function () { // Get the object var nestedP = that.getObject(unk.missing()); // Assign it if needed nestedP = that.wrap(nestedP, function (obj) { if (unk.id() !== undefined) { var assign: any = that._cache.find(unk.id()); if (assign !== null) { that.disable++; assign[unk.prop()] = obj; that.disable--; // Request retry done.reject(null); } } }); return nestedP; }); }); } else { // Commit available to the prop var to = this._cache.find(unk.id()); that.disable++; to[unk.prop()] = missing; that.disable--; done.reject(null); // A retry request } } else { done.reject(e); } } return done; } private commitMtx(mtx: mtx.MTX): rsvp.Promise { utils.dassert(utils.isValue(mtx)); this._logger.debug('STORE', '%s: commitMtx()', this.id(), mtx.toString()); var that = this; return that.locked(function () { return that.applyMtx(mtx); }); } private applyMtx(mtx: mtx.MTX): rsvp.Promise { var that = this; var curP = new rsvp.Promise(); // Check readset is OK that.checkReadset(mtx.rset).then( function (fails) { var failed = fails.filter(function (v) { return v !== null }) if (failed.length > 0) { that._logger.debug('STORE', '%s: checkReadset failures', that.id(), failed); curP.reject(failed); } else { curP.resolve(); } }, function (err) { that._logger.debug('STORE', '%s: checkReadset failed', that.id()); curP.reject(err); } ); // Make changes return that.wait(curP, function () { return that.makeChanges(mtx); }); } private makeChanges(mtx: mtx.MTX) : rsvp.Promise { var that = this; var curP = new rsvp.Promise(); curP.resolve(); // Ref change & Rev set, for later action // Note: New objects are given a starting ref change of -1 to counter // the + 1 they are given when loaded into mongo. Net effect is that // something must reference them to stop them being deleted at the end // of the pass, which of course the normal case. var rrset = new utils.IdMap(); // Scan nset for cross references var nset = mtx.nset; for (var i = 0; i < nset.size(); i++) { var nentry = nset.at(i); utils.dassert(this._cache.find(nentry.id) === null); // Scan for non-tracked objects var keys = Object.keys(nentry.obj); for (var k = 0 ; k < keys.length; k++) { var v=nentry.obj[keys[k]] if (utils.isObjectOrArray(v) && tracker.getTrackerUnsafe(v)===null) { var vid = this.valueId(v); var rr: RRData = rrset.findOrInsert(vid, { uprev: false, ref: -1, reinit: false }); rr.ref++; } } } // Load up new objects for (var i = 0; i < nset.size(); i++) { var nentry = nset.at(i); utils.dassert(this._cache.find(nentry.id) === null); // Write with 1 ref, compensated in r&r set var writeObjectFn = (function (_id,_obj) { return function () {return that.writeObject(_id, _obj, 0, 1) }; }); curP = that.wait(curP, writeObjectFn(nentry.id, nentry.obj)); var rr: RRData = { uprev: false, ref: -1, reinit: false}; rrset.findOrInsert(nentry.id, { uprev: false, ref: -1, reinit: false }); // Time to start tracking changes new tracker.Tracker(that, nentry.obj, nentry.id, 0); that._cache.insert(nentry.id, nentry.obj); } // Now for the main body of changes var cset = mtx.cset; for (var i = 0; i < cset.size(); i++) { // Pull some basic details about target object var e = cset.at(i); if (e === null) continue; var t = tracker.getTracker(e.obj); var id = t.id(); var tdata: TrackerData = t.getData(); // Record need to up revision, for later var rr: RRData = rrset.findOrInsert(t.id(), { uprev: true, ref: 0, reinit: false }); rr.uprev = true; // Write prop if (e.write !== undefined) { // Deref un-loaded value if (utils.isObjectOrArray(e.last)) { var vid = this.objectID(e.last); var lastrr: RRData = rrset.findOrInsert(vid, { uprev: true, ref: 0, reinit: false }); lastrr.ref--; tdata.rout--; } // Upref if assigning object var val = e.value; if (utils.isObjectOrArray(val)) { var vid = this.objectID(val); val = new serial.Reference(vid); var valrr: RRData = rrset.findOrInsert(vid, { uprev: true, ref: 0, reinit: false }); valrr.ref++; tdata.rout++; } var writePropFn = (function (_id,_write,_val) { return function () {return that.writeProp(_id, _write, _val) }; }); curP = that.wait(curP, writePropFn(id,e.write,val)); } // Delete Prop else if (e.del !== undefined) { // Check for outbound to another object if (tdata.rout > 0) { var readPropFn = (function (_id,_del) { return function () {return that.readProp(_id, _del)}; }); curP = that.wait(curP, readPropFn(t.id(), e.del)); curP = that.wrap(curP, function (value) { if (utils.isObject(value)) { var vkeys = Object.keys(value); if (vkeys.length === 1 && vkeys[0] === '_id') { var valrr: RRData = rrset.findOrInsert(value._id, { uprev: false, ref: 0, reinit: false }); valrr.ref--; tdata.rout--; } } }); } // Remove the prop var deletePropFn = (function (_id,_del) { return function () {return that.deleteProp(_id, _del)}; }); curP = that.wait(curP, deletePropFn(t.id(), e.del)); } else if (e.shift !== undefined) { // Handle front pop if (e.shift === 0 || e.shift === -1) { var count = e.size; var front = (e.shift === 0); var arrayPopFn = (function (_id, _front) { return function () { return that.arrayPop(_id, _front) }; }); while (count--) { curP = that.wait(curP, arrayPopFn(id, front)); } } else { // Need a re-init rr.reinit = true; } } else if (e.unshift !== undefined) { rr.reinit = true; } else if (e.reinit !== undefined) { rr.reinit = true; } else if (e.reverse !== undefined) { rr.reinit = true; } else { this._logger.fatal('%s: cset contains unexpected command', t.id()); } } // Do collected ref & rev changes var rrP = new rsvp.Promise(); curP.then(function () { var done = new rsvp.Promise(); done.resolve(); rrset.apply(function (lid: utils.uid, rr: RRData) { if (rr.reinit) { // Worst case, have to write whole object again var reobj = that._cache.find(lid); var ret = tracker.getTracker(reobj); var ltdata: TrackerData = ret.getData(); var writeObjectFn = (function (_id,_obj,_rev,_ref) { return function () {return that.writeObject(_id, _obj, _rev, _ref) }; }); done = that.wait(done, writeObjectFn(lid, reobj, ret.rev(), ltdata.rin+rr.ref)); } else if (rr.uprev || rr.ref !== 0) { // Just ref & rev changes to do var changeRevAndRefFn = (function (_id,_uprev,_ref) { return function () {return that.changeRevAndRef(_id, _uprev, _ref) }; }); done = that.wait(done, changeRevAndRefFn(lid, rr.uprev, rr.ref)); } }); done.then(function () { rrP.resolve(); }, function (err) { rrP.reject(err); }); }, function (err) { rrP.reject(err); }); return rrP; } private undo() { // Undo current transaction this.undoMtx(this._cache); // Did the root die? var t = tracker.getTracker(this._root); if (t.isDead()) { this._root = null; } } private objectID(obj: any): utils.uid { utils.dassert(utils.isObjectOrArray(obj)); if (obj instanceof serial.Reference) return obj.id(); var t = tracker.getTrackerUnsafe(obj); if (t) return t.id(); return this.valueId(obj); } private fail(promise, fmt: string, ...msgs: any[]) { var msg=utils.format('', fmt, msgs); this._logger.debug('STORE', msg); promise.reject(new Error(msg)); } private updateObject(doc: any, proto?: any) : ObjectData { // Sort out proto if (doc._type === 'Object') { if (!utils.isValue(proto)) { proto = {}; } else { utils.dassert(utils.isObject(proto)); } } else if (doc._type === 'Array') { if (!utils.isValue(proto)) { proto = []; } else { utils.dassert(utils.isArray(proto)); // Prop delete does not work well on arrays so zero proto proto.length = 0; } } else { this._logger.fatal('%s: Unexpected document type: %j', this.id(), doc._type); } this.disable++; // Read props var dkeys = Object.keys(doc._data); var dk = 0; var pkeys = Object.keys(proto); var pk = 0; var out = 0; while (true) { // Run out? if (dk === dkeys.length) break; // Read prop name var prop = dkeys[dk]; // Delete rest of proto props if does not match what is being read if (pk !== -1 && prop != pkeys[pk]) { for (var i = pk; i < pkeys.length; i++) delete proto[pkeys[i]]; pk = -1; } // Check for a Reference var val = doc._data[dkeys[dk]]; if (utils.isObject(val)) { var vkeys = Object.keys(val); if (vkeys.length === 1 && vkeys[0] === '_id') { val = new serial.Reference(val._id); out++; } } // Update proto value proto[prop] = val; dk++; } this.disable--; return { obj: proto, id: doc._id, rev: doc._rev, ref: doc._ref, out: out }; } /* ----------------------------- ASYNC HELPERS ------------------------------- */ private wait(chainP: rsvp.Promise, fn: (args: any[]) => rsvp.Promise): rsvp.Promise { var that = this; var p = new rsvp.Promise(); chainP.then(function () { fn.apply(that,arguments).then(function () { p.resolve.apply(p,arguments); }, function (err) { p.reject(err); }); }, function (err) { p.reject(err); }); return p; } private wrap(chainP: rsvp.Promise, fn: (args: any[]) => any): rsvp.Promise { var that = this; var p = new rsvp.Promise(); chainP.then(function () { p.resolve(fn.apply(that, arguments)); }); return p; } private locked(fn : () => rsvp.Promise): rsvp.Promise { var that = this; var p = new rsvp.Promise(); that.lock().then(function () { fn().then(function () { var args = arguments; that.removeLock().then(function () { p.resolve(args); }, function (err) { p.reject(err); }); }, function (err) { that.removeLock().then(function () { p.reject(err); }, function (err) { p.reject(err); }); }); }, function (err) { p.reject(err); }); return p; } /* ----------------------------- MONGO CODE ----------------------------------- */ private getCollection() : rsvp.Promise { var that = this; var done = new rsvp.Promise(); // Shortcut if we have been here before if (that._collection!==null) { done.resolve(that._collection) return done; } // Open DB that._logger.debug('STORE', '%s: Connecting to database - %s', that.id(), that._dbName); that._db = new mongo.Db(that._dbName, new mongo.Server(that._host, that._port, { poolSize: 1 }), { w: 1 }); that._db.open(function (err, db) { if (err) { that.fail(done, '%s: Unable to open db: %s : %s', that.id(), that._dbName, err.message); } else { // Open Collection that._logger.debug('STORE', '%s: Opening collection - %s', that.id(), that._collectionName); that._db.createCollection(that._collectionName, function (err, collection) { if (err) { that.fail(done, '%s: Unable to open collection: %s : %s', that.id(), that._collectionName, err.message); } else { that._collection = collection; // Init collection var curP = that.ensureExists(lockUID, {locked : false}, null); curP = that.wait(curP, function () { return that.ensureExists(rootUID, { _rev: 0, _ref: 1, _type: 'Object', _data: {} }, collection); }); curP.then(function () { done.resolve(); }, function (err) { done.resolve(err); }); } }); } }); return done; } private lock(timeout?: number=MINLOCK) : rsvp.Promise { var that = this; var p = new rsvp.Promise(); that._logger.debug('STORE', '%s: Trying to acquire lock', that.id()); var oid = utils.toObjectID(lockUID); var rand = new bson.ObjectId().toString(); that._collection.findAndModify({ _id: oid, locked: false }, [], { _id: oid, owner: that.id().toString(), host: utils.hostInfo(), pid: process.pid, rand: rand, locked: true }, { safe: true, upsert: false, remove: false, new: false }, function (err, doc) { if (err) { that.fail(p,'%s: Unable query lock : %s', that.id(), err.message); } else if (!doc) { // Report on current state if (timeout > CHECKRAND) { that._collection.findOne({ _id: oid }, function (err, doc) { if (err) { that.fail(p, '%s: Unable query lock : %s', that.id(), err.message); } else { that._logger.debug('STORE', '%s: Locked by: %s', that.id(), doc.host); // If lock rand is changing, reset timout so we don't kill unecessarily if (doc && doc['rand'] !== undefined && that._lockRand !== doc.rand) { if (that._lockRand) that._logger.debug('STORE', '%s: Lock rand has changed, %s to %s, reseting timeout', that.id(), that._lockRand, doc.rand); that._lockRand = doc.rand; timeout = CHECKRAND; } // We are going to have to break this if (timeout > MAXLOCK) { that._logger.debug('STORE', '%s: Lock owner must be dead, trying to remove', that.id()); that.removeLock().then(function () { that.lock().then(function () { p.resolve(); }, function (err) { p.reject(err); }); }, function (err) { p.resolve(err); }); } else { setTimeout(function () { that.lock(timeout * 2).then(function () { p.resolve(); }, function (err) { p.reject(err); }) }, timeout); } } }); } else { // < CHECK time, just try again setTimeout(function () { that.lock(timeout * 2).then(function () { p.resolve(); }, function (err) { p.reject(err); }) }, timeout); } } else { that._logger.debug('STORE', '%s: Acquired lock', that.id()); p.resolve(); } }); return p; } private removeLock() : rsvp.Promise { var that = this; var done = new rsvp.Promise(); var oid = utils.toObjectID(lockUID); that._collection.update({ _id: oid }, { _id: oid, locked: false }, { safe: that._safe, upsert: true}, function (err, update) { if (err) { that.fail(done,'%s: Unable remove lock : %s', that.id(), err.message); } else { that._logger.debug('STORE', '%s: Released lock', that.id()); that._lockRand = null; done.resolve(); } }); return done; } private getRoot(): rsvp.Promise { var that = this; var done = new rsvp.Promise(); if (that._root !== null) { done.resolve(that._root); } else { var curP = that.getCollection(); curP.then(function () { that.lock().then(function () { that.getObject(rootUID).then(function (obj) { that.removeLock().then(function () { that._root = obj; done.resolve(obj); }, function (err) { done.reject(err); }); }, function (err) { that.removeLock().then(function () {; done.reject(err); }); }); }, function (err) { done.reject(err); }) }, function (err) { done.reject(err); }); } return done; } private getObject(oid: utils.uid) : rsvp.Promise { var done = new rsvp.Promise(); var that = this; that.getCollection().then(function (collection) { that._logger.debug('STORE', '%s: Searching for object: %s', that.id(), oid); collection.findOne({ _id: utils.toObjectID(oid) }, function (err, doc) { if (err) { that.fail(done, '%s: Unable to search collection: %s : %s', that.id(), that._collectionName, err.message); } else { if (doc === null) { that.fail(done, '%s: Object missing in store: %s', that.id(), oid); } else { that._logger.debug('STORE', '%s: Loading object: %s:%d', that.id(), oid, doc._rev); // Load the new object var obj = that._cache.find(oid); var rec = that.updateObject(doc, obj); // Reset tracking var t = tracker.getTrackerUnsafe(rec.obj); if (t === null) { t = new tracker.Tracker(that, rec.obj, rec.id, rec.rev); that._cache.insert(t.id(), rec.obj); } else { t.setRev(rec.rev); t.retrack(rec.obj); } var tdata : TrackerData = { rout: rec.out, rin: doc._ref }; t.setData(tdata); // Catch root update if (t.id().toString() === rootUID.toString()) { that._root = rec.obj; } // Return object done.resolve(rec.obj); } } }); }); return done; } private refreshSet(failed: utils.uid[]): rsvp.Promise { var that = this; var fails = []; for (var i = 0; i < failed.length; i++) { fails.push(that.getObject(failed[i])); } return rsvp.all(fails); } private writeObject(oid:utils.uid, obj:any, rev:number, ref:number) : rsvp.Promise { utils.dassert(utils.isValue(oid) && utils.isObjectOrArray(obj)); var that = this; // Prep a copy for upload var rout = 0; var fake : any = {}; fake._data = utils.clone(obj); var keys = Object.keys(obj); for (var k = 0; k < keys.length; k++) { if (utils.isObjectOrArray(obj[keys[k]])) { var id = that.valueId(obj[keys[k]]); fake._data[keys[k]] = { _id: id.toString() }; rout++; } } fake._id = utils.toObjectID(oid); fake._rev = rev; fake._ref = ref; fake._type = utils.isObject(obj) ? 'Object' : 'Array'; tracker.getTracker(obj).setData({ rout: rout, rin: ref }); // Upload the fake var p = new rsvp.Promise(); that._logger.debug('STORE', '%s: Updating object: %s %j', that.id(), oid, obj); that._collection.update({ _id: fake._id }, fake, { safe: that._safe, upsert: true }, function (err,count) { if (err) { that.fail(p, '%s: Update failed on new object %s=%j error %s', that.id(), id, obj, err.message); } else { if (that._safe && count !== 1) { that.fail(p, '%s: Update failed on new object %s=%j count %d', that.id(), id, obj, count); } else { p.resolve(); } } }); return p; } private ensureExists(oid: utils.uid, proto: any, arg: any) : rsvp.Promise{ var that = this; var p = new rsvp.Promise(); that._logger.debug('STORE', '%s: Checking/inserting for object: %s', that.id(), oid); proto._id = utils.toObjectID(oid); that._collection.findOne({ _id: utils.toObjectID(oid) }, function (err, doc) { if (err) { that.fail(p, '%s: Unable to search collection: %s : %s', that.id(), that._collectionName, err.message); } else if (doc === null) { that._collection.insert(proto, { safe: true }, function (err, inserted) { // Here err maybe be because of a race so we just log it if (err) { that._logger.debug('STORE', '%s: Unable to insert %s (ignoring as maybe race)', that.id(), oid); } else { that._logger.debug('STORE', '%s: Object %s inserted', that.id(), oid); } p.resolve(arg); }); } else { that._logger.debug('STORE', '%s: Object %s already exists', that.id(), oid); p.resolve(arg); } }); return p; } private changeRevAndRef(oid: utils.uid, revchange: bool, refchange: number) : rsvp.Promise { var that = this; var p = new rsvp.Promise(); that._logger.debug('STORE', '%s: Updating object rev & ref: %s uprev: %s, upref %d', that.id(), oid, revchange, refchange); var revinc = 0; if (revchange) revinc = 1; that._collection.findAndModify({ _id: utils.toObjectID(oid) }, [], { $inc: { _rev: revinc, _ref: refchange } }, { safe: true, remove:false, upsert:false, new:true }, function (err,doc) { if (err) { that.fail(p, '%s: Update failed on object ref for %s error %s', that.id(), oid, err.message); } else { if (doc === null) { that.fail(p, '%s: Update failed on object ref for %s empty doc', that.id(), oid); } else { // Delete this object if needed var d; if (doc._ref === 0) { d = that.deleteObject(oid) // Scan object for outgoing links & down ref them var dropRefFn = (function (_id) { return function () { return that.changeRevAndRef(utils.makeUID(_id), false, -1) }; }); var keys = Object.keys(doc._data); for (var k = 0 ; k < keys.length; k++) { var v = doc._data[keys[k]]; if (utils.isObject(v) && utils.isEqual(Object.keys(v), ['_id'])) { var rid = v._id; d = that.wait(d, dropRefFn(rid)); } } } else { d = new rsvp.Promise(); d.resolve(); } d.then(function () { p.resolve(); }, function (err) { p.reject(err); }); } } }); return p; } private deleteObject(oid: utils.uid) : rsvp.Promise { var that = this; var p = new rsvp.Promise(); that._logger.debug('STORE', '%s: Deleting object: %s', that.id(), oid); that._collection.remove({ _id: utils.toObjectID(oid) }, { safe: that._safe }, function (err,count) { if (err) { that.fail(p, '%s: Deleting failed on %s error %s', that.id(), oid, err.message); } else { if (that._safe && count !== 1) { that.fail(p, '%s: Deleting failed on %s count %d', that.id(), oid, count); } else { p.resolve(); } } }); return p; } private readProp(oid: utils.uid, prop:string) : rsvp.Promise { var that = this; var p = new rsvp.Promise(); var fields = {}; fields['_data.'+prop] = true; that._logger.debug('STORE', '%s: Reading prop for object: %s[%s]', that.id(), oid, prop); that._collection.findOne({ _id: utils.toObjectID(oid) }, fields, function (err, doc) { if (err) { that.fail(p, '%s: Unable to search collection: %s : %s', that.id(), that._collectionName, err.message); } else if (doc === null) { that.fail(p, '%s: Object missing in store: %s : %s', that.id(), oid); } else { p.resolve(doc._data[prop]); } }); return p; } private writeProp(oid: utils.uid, prop:string, value:any) : rsvp.Promise { var that = this; var p = new rsvp.Promise(); if (value instanceof serial.Reference) value = { _id: value.id() }; var upd = {}; upd['_data.'+prop] = value; that._logger.debug('STORE', '%s: Updating property: %s[%s] %j', that.id(), oid, prop, value); that._collection.update({ _id: utils.toObjectID(oid) }, { $set: upd }, { safe: that._safe }, function (err,count) { if (err) { that.fail(p, '%s: Update failed on %s[%s] %j error %s', that.id(), oid, prop, value, err.message); } else { if (that._safe && count !== 1) { that.fail(p, '%s: Update failed on %s[%s] %j count %d', that.id(), oid, prop, value, count); } else { p.resolve(); } } }); return p; } private deleteProp(oid: utils.uid, prop:string) : rsvp.Promise { var that = this; var p = new rsvp.Promise(); var upd = {}; upd['_data.'+prop] = ''; that._logger.debug('STORE', '%s: Deleting property: %s[%s]', that.id(), oid, prop); that._collection.update({ _id: utils.toObjectID(oid) }, { $unset: upd }, { safe: that._safe }, function (err,count) { if (err) { that.fail(p, '%s: Deleting failed on %s[%s] error %s', that.id(), oid, prop, err.message); } else { if (that._safe && count !== 1) { that.fail(p, '%s: Deleting failed on %s[%s] count %d', that.id(), oid, prop, count); } else { p.resolve(); } } }); return p; } private arrayPop(oid: utils.uid, front: bool) : rsvp.Promise { var that = this; var p = new rsvp.Promise(); var arg = 1; var name = 'back'; if (front) { arg = -1; name = 'front'; } that._logger.debug('STORE', '%s: Array pop: %s[%s]', that.id(), oid, name); that._collection.update({ _id: utils.toObjectID(oid) }, { $pop: { _data: arg } }, { safe: that._safe }, function (err,count) { if (err) { that.fail(p, '%s: Array pop failed on %s[%s] error %s', that.id(), oid, name, err.message); } else { if (that._safe && count !== 1) { that.fail(p, '%s: Array pop failed on %s[%s] count %d', that.id(), oid, name, count); } else { p.resolve(); } } }); return p; } private checkReadset(rset: mtx.ReadMap) : rsvp.Promise { this._logger.debug('STORE', '%s: checkReadset(%d)', this.id(), rset.size()); var that = this; utils.dassert(rset.size() !== 0); var fails = []; rset.apply(function (oid, rev) { fails.push(that.revisionCheck(oid, rev)); return true; }); return rsvp.all(fails); } private revisionCheck(oid: utils.uid, revision: number) : rsvp.Promise { this._logger.debug('STORE', '%s: revisionCheck(%s,%s)', this.id(), oid, revision); var that = this; var promise = new rsvp.Promise(); that._collection.find({ _id: utils.toObjectID(oid), _rev: revision}).count(function (err, num) { if (err) { promise.reject(err.message); } else { if (num === 1) promise.resolve(null); else promise.resolve(oid); } }); return promise; } } export interface ObjectData { obj: any; id: string; rev: number; ref: number; out: number; } export interface TrackerData { rout: number; rin: number; } export interface RRData { uprev: bool; ref: number; reinit: bool; } } // store } // shared