document.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. // Copyright 2015 The MemDB Authors.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12. // implied. See the License for the specific language governing
  13. // permissions and limitations under the License. See the AUTHORS file
  14. // for names of contributors.
  15. 'use strict';
  16. var P = require('bluebird');
  17. var util = require('util');
  18. var utils = require('./utils');
  19. var AsyncLock = require('async-lock');
  20. var EventEmitter = require('events').EventEmitter;
  21. var modifier = require('./modifier');
  22. var logger = require('memdb-logger').getLogger('memdb', __filename);
  23. var DEFAULT_LOCK_TIMEOUT = 10 * 1000;
  24. var Document = function(opts){ //jshint ignore:line
  25. opts = opts || {};
  26. if(!opts.hasOwnProperty('_id')){
  27. throw new Error('_id is not specified');
  28. }
  29. this._id = opts._id;
  30. var doc = opts.doc || null;
  31. if(typeof(doc) !== 'object'){
  32. throw new Error('doc must be object');
  33. }
  34. if(!!doc){
  35. doc._id = this._id;
  36. }
  37. this.commited = doc;
  38. this.changed = undefined; // undefined means no change, while null means removed
  39. this.connId = null; // Connection that hold the document lock
  40. this.locker = opts.locker;
  41. this.lockKey = opts.lockKey;
  42. if(!this.locker){
  43. this.locker = new AsyncLock({
  44. Promise : P,
  45. timeout : opts.lockTimeout || DEFAULT_LOCK_TIMEOUT
  46. });
  47. this.lockKey = '';
  48. }
  49. this.releaseCallback = null;
  50. this.indexes = opts.indexes || {};
  51. this.savedIndexValues = {}; //{indexKey : indexValue}
  52. EventEmitter.call(this);
  53. };
  54. util.inherits(Document, EventEmitter);
  55. var proto = Document.prototype;
  56. proto.find = function(connId, fields){
  57. var doc = this.isLocked(connId) ? this._getChanged() : this.commited;
  58. if(doc === null){
  59. return null;
  60. }
  61. if(!fields){
  62. return doc;
  63. }
  64. var includeFields = [], excludeFields = [];
  65. if(typeof(fields) === 'string'){
  66. includeFields = fields.split(' ');
  67. }
  68. else if(typeof(fields) === 'object'){
  69. for(var field in fields){
  70. if(!!fields[field]){
  71. includeFields.push(field);
  72. }
  73. else{
  74. excludeFields.push(field);
  75. }
  76. }
  77. if(includeFields.length > 0 && excludeFields.length > 0){
  78. throw new Error('Can not specify both include and exclude fields');
  79. }
  80. }
  81. var ret = null;
  82. if(includeFields.length > 0){
  83. ret = {};
  84. includeFields.forEach(function(field){
  85. if(doc.hasOwnProperty(field)){
  86. ret[field] = doc[field];
  87. }
  88. });
  89. ret._id = this._id;
  90. }
  91. else if(excludeFields.length > 0){
  92. ret = {};
  93. for(var key in doc){
  94. ret[key] = doc[key];
  95. }
  96. excludeFields.forEach(function(key){
  97. delete ret[key];
  98. });
  99. }
  100. else{
  101. ret = doc;
  102. }
  103. return ret;
  104. };
  105. proto.exists = function(connId){
  106. return this.isLocked(connId) ? this._getChanged() !== null: this.commited !== null;
  107. };
  108. proto.insert = function(connId, doc){
  109. this.modify(connId, '$insert', doc);
  110. };
  111. proto.remove = function(connId){
  112. this.modify(connId, '$remove');
  113. };
  114. proto.update = function(connId, modifier, opts){
  115. opts = opts || {};
  116. if(!modifier){
  117. throw new Error('modifier is empty');
  118. }
  119. modifier = modifier || {};
  120. var isModify = false;
  121. for(var field in modifier){
  122. isModify = (field[0] === '$');
  123. break;
  124. }
  125. if(!isModify){
  126. this.modify(connId, '$replace', modifier);
  127. }
  128. else{
  129. for(var cmd in modifier){
  130. this.modify(connId, cmd, modifier[cmd]);
  131. }
  132. }
  133. };
  134. proto.modify = function(connId, cmd, param){
  135. this.ensureLocked(connId);
  136. for(var indexKey in this.indexes){
  137. if(!this.savedIndexValues.hasOwnProperty(indexKey)){
  138. this.savedIndexValues[indexKey] = this._getIndexValue(indexKey, this.indexes[indexKey]);
  139. }
  140. }
  141. var modifyFunc = modifier[cmd];
  142. if(typeof(modifyFunc) !== 'function'){
  143. throw new Error('invalid modifier - ' + cmd);
  144. }
  145. if(this.changed === undefined){ //copy on write
  146. this.changed = utils.clone(this.commited);
  147. }
  148. this.changed = modifyFunc(this.changed, param);
  149. // id is immutable
  150. if(!!this.changed){
  151. this.changed._id = this._id;
  152. }
  153. for(indexKey in this.indexes){
  154. var value = this._getIndexValue(indexKey, this.indexes[indexKey]);
  155. if(value !== this.savedIndexValues[indexKey]){
  156. logger.trace('%s.updateIndex(%s, %s, %s)', this._id, indexKey, this.savedIndexValues[indexKey], value);
  157. this.emit('updateIndex', connId, indexKey, this.savedIndexValues[indexKey], value);
  158. this.savedIndexValues[indexKey] = value;
  159. }
  160. }
  161. logger.trace('%s.modify(%s, %j) => %j', this._id, cmd, param, this.changed);
  162. };
  163. proto.lock = function(connId){
  164. if(connId === null || connId === undefined){
  165. throw new Error('connId is null');
  166. }
  167. var deferred = P.defer();
  168. if(this.isLocked(connId)){
  169. deferred.resolve();
  170. }
  171. else{
  172. var self = this;
  173. this.locker.acquire(this.lockKey, function(release){
  174. self.connId = connId;
  175. self.releaseCallback = release;
  176. self.emit('lock');
  177. deferred.resolve();
  178. })
  179. .catch(function(err){
  180. if(!deferred.isResolved()){
  181. deferred.reject(new Error('doc.lock failed - ' + self.lockKey));
  182. }
  183. });
  184. }
  185. return deferred.promise;
  186. };
  187. // Wait existing lock release (not create new lock)
  188. proto._waitUnlock = function(){
  189. var deferred = P.defer();
  190. var self = this;
  191. this.locker.acquire(this.lockKey, function(){
  192. deferred.resolve();
  193. })
  194. .catch(function(err){
  195. deferred.reject(new Error('doc._waitUnlock failed - ' + self.lockKey));
  196. });
  197. return deferred.promise;
  198. };
  199. proto._unlock = function(){
  200. if(this.connId === null){
  201. return;
  202. }
  203. this.connId = null;
  204. var releaseCallback = this.releaseCallback;
  205. this.releaseCallback = null;
  206. releaseCallback();
  207. this.emit('unlock');
  208. };
  209. proto._getChanged = function(){
  210. return this.changed !== undefined ? this.changed : this.commited;
  211. };
  212. proto._getCommited = function(){
  213. return this.commited;
  214. };
  215. proto.commit = function(connId){
  216. this.ensureLocked(connId);
  217. if(this.changed !== undefined){
  218. this.commited = this.changed;
  219. }
  220. this.changed = undefined;
  221. this.emit('commit');
  222. this._unlock();
  223. };
  224. proto.rollback = function(connId){
  225. this.ensureLocked(connId);
  226. this.changed = undefined;
  227. this.savedIndexValues = {};
  228. this.emit('rollback');
  229. this._unlock();
  230. };
  231. proto.ensureLocked = function(connId){
  232. if(!this.isLocked(connId)){
  233. throw new Error('doc not locked by ' + connId);
  234. }
  235. };
  236. proto.isLocked = function(connId){
  237. return this.connId === connId && connId !== null && connId !== undefined;
  238. };
  239. proto.isFree = function(){
  240. return this.connId === null;
  241. };
  242. proto._getIndexValue = function(indexKey, opts){
  243. opts = opts || {};
  244. var self = this;
  245. var indexValue = JSON.parse(indexKey).sort().map(function(key){
  246. var doc = self._getChanged();
  247. var value = !!doc ? doc[key] : undefined;
  248. // null and undefined is not included in index
  249. if(value === null || value === undefined){
  250. return undefined;
  251. }
  252. if(['number', 'string', 'boolean'].indexOf(typeof(value)) === -1){
  253. throw new Error('invalid value for indexed key ' + indexKey);
  254. }
  255. var ignores = opts.valueIgnore ? opts.valueIgnore[key] || [] : [];
  256. if(ignores.indexOf(value) !== -1){
  257. return undefined;
  258. }
  259. return value;
  260. });
  261. // Return null if one of the value is undefined
  262. if(indexValue.indexOf(undefined) !== -1){
  263. return null;
  264. }
  265. return JSON.stringify(indexValue);
  266. };
  267. module.exports = Document;