Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

first commit

  • Loading branch information...
commit f2e2bfea6613a44dec3adf27cb3d004437afd278 0 parents
authored March 20, 2012
58  README.md
Source Rendered
... ...
@@ -0,0 +1,58 @@
  1
+DeltaQL
  2
+-------
  3
+
  4
+Imagine a database (like MySQL or SQLServer) whose 'result sets' automatically update as the underlying data changes.  Add to this a facility to extend these 'deltas' all the way to the browser.
  5
+
  6
+Welcome to DeltaQL - no more F5, no more polling the DB.
  7
+
  8
+
  9
+Deliverables
  10
+------------
  11
+
  12
+DeltaQL has five deliverables:
  13
+
  14
+1. The Bootstrap App.  A minimal NodeJS web app with just one live page - ideal to build upon.
  15
+
  16
+2. The Server Library.  This is written in Javascript for NodeJS and is used by both your NodeJS web applications and any (optional) separate databases.  DeltaQL can be run both in-process and as separate server process(es).
  17
+
  18
+3. The Browser Library.  This runs on the browser and builds on top of KnockoutJS.
  19
+
  20
+4. The eBook.  Income from this $9 ebook will support the development of DeltaQL.
  21
+
  22
+5. The Blog App.  A working example of a web app built with DeltaQL, possibly demonstrating best practises.
  23
+
  24
+All except the eBook will be available on GitHub under the MIT license.
  25
+
  26
+
  27
+Persistance
  28
+-----------
  29
+
  30
+DeltaQL operates in memory.  It has hooks to add persistence to MySQL, Postgres or any other NodeJS supported database.  Use DeltaQL for all your 'live' clients, yet persist your data in a database which you trust.
  31
+
  32
+The only built-in Persistor is the simple, but inefficient, JSONFilePersistor.  Any others will need to be written (for MySQL, etc.) but it's not hard.
  33
+
  34
+
  35
+Origins
  36
+-------
  37
+
  38
+When I found myself solving the same 'data freshness' and pub/sub issues in NodeJS for the third time, I recognised it as a need and created the DeltaQL project.
  39
+
  40
+
  41
+Built on the Shoulders of Giants
  42
+--------------------------------
  43
+
  44
+DeltaQL uses NodeJS, Express, Socket.IO, Mocha, KnockoutJS and much other uncredited work from the open source community.  Thanks to all of those who've made DeltaQL achievable in only tens-of-hours of design and coding.
  45
+
  46
+
  47
+Jargon
  48
+------
  49
+
  50
+This may be useful if you start diving into the code.
  51
+
  52
+* Lop - a List OPeration.  This is a delta which keeps a two ResultLists in sync.
  53
+* ResultList - an *ordered* list of rows.
  54
+* ResultSet - a set of rows.
  55
+* Row - a JSON object with an 'id' field containing an unique string (often a UUID) and zero or more further fields.
  56
+* Sop - a Set OPeration.  This is a delta which keeps a two ResultSets in sync.
  57
+* LivePage - any web page containing one or more ResultLists.
  58
+
4  deltaql.komodoproject
... ...
@@ -0,0 +1,4 @@
  1
+<?xml version="1.0" encoding="UTF-8"?>
  2
+<!-- Komodo Project File - DO NOT EDIT -->
  3
+<project id="305f0c82-e4a5-40f8-8509-44f8e166dc0e" kpf_version="5" name="deltaql.komodoproject">
  4
+</project>
3  lib/deltaql.js
... ...
@@ -0,0 +1,3 @@
  1
+exports.RSet = require('rset').RSet;
  2
+exports.RList = require('rlist').RList;
  3
+exports.Filter = require('filter').Filter;
96  lib/filter.js
... ...
@@ -0,0 +1,96 @@
  1
+// Filter is a sop->sop class
  2
+
  3
+var util = require('util')
  4
+  , rs = require('./rset')
  5
+  ;
  6
+
  7
+exports.Filter = Filter;
  8
+
  9
+function Filter(parent, fn) {
  10
+  this.fn = fn;
  11
+  this.id = false;
  12
+  this.parent = parent;
  13
+  this.rows = {};
  14
+  
  15
+  var self = this;
  16
+  if (parent) {
  17
+    parent.on('sop', function(sop) { self.processSop(sop) });
  18
+    this.processSop({sop:'state',rows:parent.getRows()});
  19
+  }
  20
+}
  21
+
  22
+util.inherits(Filter, rs.RSet);
  23
+
  24
+// This is copied in from the RSet class, as the constructor needs it before
  25
+// the inheritance chain is setup.
  26
+Filter.prototype.processSop = function(sop) {
  27
+  if (sop.sop === 'state') this.processState(sop);
  28
+  if (sop.sop === 'delete') this.processDelete(sop);
  29
+  if (sop.sop === 'insert') this.processInsert(sop);
  30
+  if (sop.sop === 'update') this.processUpdate(sop);
  31
+}
  32
+
  33
+Filter.prototype.processState = function(sop) {
  34
+  //console.log("processState", sop);
  35
+  this.rows = {};
  36
+  for (var id in sop.rows) {
  37
+    if (this.fn(sop.rows[id])) {
  38
+      //console.log("inc", sop.rows[id]);
  39
+      this.rows[sop.rows[id].id] = sop.rows[id];
  40
+    } else {
  41
+      //console.log("exc", sop.rows[id]);
  42
+    }
  43
+  }
  44
+  this.emit('sop', {sop:"state",rows:this.rows});
  45
+}
  46
+
  47
+Filter.prototype.processDelete = function(sop) {
  48
+  //console.log("processDelete", sop);
  49
+  
  50
+  // checks
  51
+  if (!this.rows[sop.id]) {
  52
+    //console.log("processDelete: not found");
  53
+    return; // don't bother to delete records
  54
+            // which are alreadt filtered out
  55
+  }
  56
+  // action
  57
+  delete this.rows[sop.id];
  58
+  this.emit('sop', {sop:"delete",id:sop.id});
  59
+}
  60
+
  61
+Filter.prototype.processInsert = function(sop) {
  62
+  // checks
  63
+  if (!this.fn(sop.row)) return; // row was filtered out
  64
+  
  65
+  // action
  66
+  this.rows[sop.row.id] = sop.row;
  67
+  this.emit('sop', {sop:"insert",row:sop.row});
  68
+}
  69
+
  70
+Filter.prototype.processUpdate = function(sop) {
  71
+  // an update might cause:
  72
+  // a) an excluded row to be included,
  73
+  // b) an included row to be excluded,
  74
+  // c) an included row to be updated
  75
+  // d) an excluded row to be updated (and ignored)
  76
+  
  77
+  var included = this.rows[sop.row.id];
  78
+  var include = this.fn(sop.row);
  79
+  
  80
+  if (!included && include) {
  81
+    this.rows[sop.row.id] = sop.row;
  82
+    this.emit('sop', {sop:"insert",row:sop.row});
  83
+  } else if (included && !include) {
  84
+    delete this.rows[sop.row.id];
  85
+    this.emit('sop', {sop:"delete",id:sop.row.id});
  86
+  } else if (included && include) {
  87
+    this.rows[sop.row.id] = sop.row
  88
+    this.emit('sop', {sop:"update",id:sop.row});
  89
+  } else if (!included && !include) {
  90
+    // nothing to do
  91
+  } else {
  92
+    // this can never happen
  93
+    throw "unreachable code reached";
  94
+  }
  95
+}
  96
+
72  lib/limit.js
... ...
@@ -0,0 +1,72 @@
  1
+// Limit is a lop->lop class
  2
+
  3
+var util = require('util')
  4
+  , rs = require('rset')
  5
+  ;
  6
+
  7
+function Filter(parent, fn) {
  8
+  rs.RSet(parent);
  9
+  this.fn = fn;
  10
+  
  11
+  var self = this;
  12
+  if (parent) parent.on('sop', function(sop) { self.processSop(sop) });
  13
+}
  14
+
  15
+util.inherits(Filter, rs.RSet);
  16
+
  17
+Filter.prototype.processState = function(sop) {
  18
+  this.rows = {};
  19
+  for (var id in this.result.rows) {
  20
+    if (this.fn(sop.rows[id])) {
  21
+      this.rows[sop.rows[id].id] = sop.rows[id];
  22
+    }
  23
+  }
  24
+  this.emit('sop', {sop:"state",rows:this.rows});
  25
+}
  26
+
  27
+Filter.prototype.processDelete = function(sop) {
  28
+  // checks
  29
+  if (!this.rows[sop.rows[id]]) return; // don't bother to delete records
  30
+                                        // which are alreadt filtered out
  31
+  
  32
+  // action
  33
+  var id = this.rows[sop.row.id];  
  34
+  delete this.result.by_id[id];
  35
+  this.emit('sop', {sop:"delete",id:id});
  36
+}
  37
+
  38
+Filter.prototype.processInsert = function(sop) {
  39
+  // checks
  40
+  if (!this.fn(sop.row)) return; // row was filtered out
  41
+  
  42
+  // action
  43
+  this.rows[sop.row.id] = sop.row;
  44
+  this.emit('sop', {sop:"insert",row:sop.row});
  45
+}
  46
+
  47
+Filter.prototype.processUpdate = function(sop) {
  48
+  // an update might cause:
  49
+  // a) an excluded row to be included,
  50
+  // b) an included row to be excluded,
  51
+  // c) an included row to be updated
  52
+  // d) an excluded row to be updated (and ignored)
  53
+  
  54
+  var included = this.rows[sop.row.id];
  55
+  var include = this.fn(sop.row);
  56
+  
  57
+  if (!included && include) {
  58
+    this.rows[sop.row.id] = sop.row;
  59
+    this.emit('sop', {sop:"insert",row:sop.row});
  60
+  } else if (included && !include) {
  61
+    delete this.rows[sop.row.id];
  62
+    this.emit('sop', {sop:"delete",id:sop.row.id});
  63
+  } else if (included && include) {
  64
+    this.rows[sop.row.id] = sop.row
  65
+    this.emit('sop', {sop:"update",id:sop.row});
  66
+  } else if (!included && !include) {
  67
+    // nothing to do
  68
+  } else {
  69
+    // this can never happen
  70
+    throw "unreachable code reached";
  71
+  }
  72
+}
75  lib/rlist.js
... ...
@@ -0,0 +1,75 @@
  1
+// RList is a lop->lop class
  2
+
  3
+var ev = require('events')
  4
+  , util = require('util')
  5
+  ;
  6
+
  7
+function klone(ob) {
  8
+  return JSON.parse(JSON.stringify(ob));
  9
+}
  10
+
  11
+function RList(parent) {
  12
+  this.id = false;
  13
+  this.parent = parent;
  14
+  this.rows = [];
  15
+  
  16
+  var self = this;
  17
+  if (parent) {
  18
+    parent.on('lop', function(lop) { self.processLop(lop) });
  19
+    this.processLop({lop:'state',rows:parent.getRows()});
  20
+  }
  21
+}
  22
+
  23
+util.inherits(RList, ev.EventEmitter);
  24
+
  25
+RList.prototype.processLop = function(lop) {
  26
+  if (lop.lop === 'state') this.processState(lop);
  27
+  if (lop.lop === 'delete') this.processDelete(lop);
  28
+  if (lop.lop === 'insert') this.processInsert(lop);
  29
+  if (lop.lop === 'replace') this.processReplace(lop);
  30
+  if (lop.lop === 'update') this.processUpdate(lop);
  31
+}
  32
+
  33
+RList.prototype.processState = function(lop) {
  34
+  this.rows = lop.rows;
  35
+  this.emit('lop', {lop:"state",rows:this.rows});
  36
+}
  37
+
  38
+RList.prototype.processDelete = function(lop) {
  39
+  // checks
  40
+  if (!this.rows[lop.pos]) console.log("bad delete", this.rows, lop);
  41
+  
  42
+  // action
  43
+  this.rows.splice([lop.pos], 1);
  44
+  this.emit('lop', {lop:"delete",pos:lop.pos});
  45
+}
  46
+
  47
+RList.prototype.processInsert = function(lop) {
  48
+  // action
  49
+  this.rows.splice(lop.pos, 0, lop.row);
  50
+  this.emit('lop', {lop:"insert",pos:lop.pos,row:lop.row});
  51
+}
  52
+
  53
+RList.prototype.processReplace = function(lop) {
  54
+  // checks
  55
+  if (!this.rows[lop.pos]) throw "bad replace";
  56
+  
  57
+  // action
  58
+  this.result.rows.splice(lop.pos, 1, lop.row);
  59
+  this.emit('lop', {lop:"replace",pos:lop.pos,val:lop.row});
  60
+}
  61
+
  62
+RList.prototype.processUpdate = function(lop) {
  63
+  // checks
  64
+  if (this.rows[lop.pos].id !== lop.row.id) throw "bad update";
  65
+  
  66
+  // action
  67
+  this.rows.splice(pos, 1, lop.val);
  68
+  this.emit('lop', {lop:"update",pos:pos,row:lop.row});
  69
+}
  70
+
  71
+RList.prototype.getRows = function() {
  72
+  return klone(this.rows);
  73
+}
  74
+
  75
+exports.RList = RList;
71  lib/rset.js
... ...
@@ -0,0 +1,71 @@
  1
+// RSet is a sop->sop class
  2
+
  3
+var ev = require('events')
  4
+  , util = require('util')
  5
+  ;
  6
+
  7
+function klone(ob) {
  8
+  return JSON.parse(JSON.stringify(ob));
  9
+}
  10
+
  11
+function RSet(parent) {
  12
+  this.id = false;
  13
+  this.parent = parent;
  14
+  this.rows = {};
  15
+  
  16
+  var self = this;
  17
+  if (parent) {
  18
+    parent.on('sop', function(sop) { self.processSop(sop) });
  19
+    this.processSop({sop:'state',rows:parent.getRows()});
  20
+  }
  21
+}
  22
+
  23
+util.inherits(RSet, ev.EventEmitter);
  24
+
  25
+RSet.prototype.processSop = function(sop) {
  26
+  if (sop.sop === 'state') this.processState(sop);
  27
+  if (sop.sop === 'delete') this.processDelete(sop);
  28
+  if (sop.sop === 'insert') this.processInsert(sop);
  29
+  if (sop.sop === 'update') this.processUpdate(sop);
  30
+}
  31
+
  32
+RSet.prototype.processState = function(sop) {
  33
+  this.rows = sop.rows;
  34
+  this.emit('sop', {sop:"state",rows:this.rows});
  35
+}
  36
+
  37
+RSet.prototype.processDelete = function(sop) {
  38
+  // checks
  39
+  if (!this.rows[sop.id]) {
  40
+    this.emit('error', "bad delete");
  41
+    return;
  42
+  }
  43
+  
  44
+  // action
  45
+  delete this.rows[sop.id];
  46
+  this.emit('sop', {sop:"delete",id:sop.id});
  47
+}
  48
+
  49
+RSet.prototype.processInsert = function(sop) {
  50
+  // checks
  51
+  if (this.rows[sop.id]) throw "bad insert";
  52
+
  53
+  // action
  54
+  this.rows[sop.row.id] = sop.row;
  55
+  this.emit('sop', {sop:"insert",row:sop.row});
  56
+}
  57
+
  58
+RSet.prototype.processUpdate = function(sop) {
  59
+  // checks
  60
+  if (!this.rows[sop.id]) throw "bad update";
  61
+  
  62
+  // action
  63
+  this.rows[osp.row.id] = sop.row;
  64
+  this.emit('sop', {sop:"update",row:sop.row});
  65
+}
  66
+
  67
+RSet.prototype.getRows = function() {
  68
+  return klone(this.rows);
  69
+}
  70
+
  71
+exports.RSet = RSet;
72  lib/sort.js
... ...
@@ -0,0 +1,72 @@
  1
+// Sort is a sop->lop class
  2
+
  3
+var util = require('util')
  4
+  , rs = require('rset')
  5
+  ;
  6
+
  7
+function Filter(parent, fn) {
  8
+  rs.RSet(parent);
  9
+  this.fn = fn;
  10
+  
  11
+  var self = this;
  12
+  if (parent) parent.on('sop', function(sop) { self.processSop(sop) });
  13
+}
  14
+
  15
+util.inherits(Filter, rs.RSet);
  16
+
  17
+Filter.prototype.processState = function(sop) {
  18
+  this.rows = {};
  19
+  for (var id in this.result.rows) {
  20
+    if (this.fn(sop.rows[id])) {
  21
+      this.rows[sop.rows[id].id] = sop.rows[id];
  22
+    }
  23
+  }
  24
+  this.emit('sop', {sop:"state",rows:this.rows});
  25
+}
  26
+
  27
+Filter.prototype.processDelete = function(sop) {
  28
+  // checks
  29
+  if (!this.rows[sop.rows[id]]) return; // don't bother to delete records
  30
+                                        // which are alreadt filtered out
  31
+  
  32
+  // action
  33
+  var id = this.rows[sop.row.id];  
  34
+  delete this.result.by_id[id];
  35
+  this.emit('sop', {sop:"delete",id:id});
  36
+}
  37
+
  38
+Filter.prototype.processInsert = function(sop) {
  39
+  // checks
  40
+  if (!this.fn(sop.row)) return; // row was filtered out
  41
+  
  42
+  // action
  43
+  this.rows[sop.row.id] = sop.row;
  44
+  this.emit('sop', {sop:"insert",row:sop.row});
  45
+}
  46
+
  47
+Filter.prototype.processUpdate = function(sop) {
  48
+  // an update might cause:
  49
+  // a) an excluded row to be included,
  50
+  // b) an included row to be excluded,
  51
+  // c) an included row to be updated
  52
+  // d) an excluded row to be updated (and ignored)
  53
+  
  54
+  var included = this.rows[sop.row.id];
  55
+  var include = this.fn(sop.row);
  56
+  
  57
+  if (!included && include) {
  58
+    this.rows[sop.row.id] = sop.row;
  59
+    this.emit('sop', {sop:"insert",row:sop.row});
  60
+  } else if (included && !include) {
  61
+    delete this.rows[sop.row.id];
  62
+    this.emit('sop', {sop:"delete",id:sop.row.id});
  63
+  } else if (included && include) {
  64
+    this.rows[sop.row.id] = sop.row
  65
+    this.emit('sop', {sop:"update",id:sop.row});
  66
+  } else if (!included && !include) {
  67
+    // nothing to do
  68
+  } else {
  69
+    // this can never happen
  70
+    throw "unreachable code reached";
  71
+  }
  72
+}
23  package.json
... ...
@@ -0,0 +1,23 @@
  1
+{ "name" : "deltaql"
  2
+, "description" : "A NodeJS-based event propagation query language."
  3
+, "version" : "0.0.1"
  4
+, "maintainers" :
  5
+  [ { "name": "Chris Dew"
  6
+    , "email": "cmsdew@gmail.com"
  7
+    }
  8
+  ]
  9
+, "bugs" : { "web" : "http://github.com/chrisdew/deltaql/issues" }
  10
+, "licenses" : [ { "type" : "MIT" } ]
  11
+, "repositories" :
  12
+  [ { "type" : "git"
  13
+    , "url" : "http://github.com/chrisdew/deltaql"
  14
+    }
  15
+  ]
  16
+, "main" : "./lib/deltaql"
  17
+, "engines" : { "node" : ">=0.4.0" }
  18
+, "dependencies" : { "vows" : ">=0.5.2"
  19
+                   , "docco" : ">=0.3.0"
  20
+                   , "node-uuid" : ">=1.1.0"
  21
+                   , "datejs" : ">=0.0.1"
  22
+                   }
  23
+}
50  test/filter-test.js
... ...
@@ -0,0 +1,50 @@
  1
+var ft = require('../lib/filter.js');
  2
+var rs = require('../lib/rset.js');
  3
+var should = require('should');
  4
+
  5
+describe('Filter', function() {
  6
+  var r, f;
  7
+  
  8
+  before(function(){
  9
+    r = new rs.RSet();
  10
+    f = new ft.Filter(r, function(a) { return a.foo > 10; });  // f = r.filter(fn);
  11
+    r.processSop({sop:'state', rows:{'0':{id:'0',foo:12,bar:13},'1':{id:'1',foo:10,bar:14}}});
  12
+  });
  13
+  
  14
+  describe('initial data', function() {
  15
+    it('should return the initial data, filtered', function() {
  16
+      f.getRows().should.eql({'0':{bar:13,id:'0',foo:12}
  17
+                              });
  18
+    });
  19
+  });
  20
+  
  21
+  describe('insert row', function() {
  22
+    it('should emit an sop with the inserted row', function(done) {
  23
+      f.on('sop', function(sop) {
  24
+        sop.should.eql({sop:'insert',row:{id:'2',foo:16,bar:15}});
  25
+        done();
  26
+      });
  27
+      r.processSop({sop:'insert',row:{id:'2',foo:16,bar:15}});
  28
+    });
  29
+  });
  30
+});
  31
+
  32
+describe('Filter', function() {
  33
+  var r, f;
  34
+  
  35
+  before(function(){
  36
+    r = new rs.RSet();
  37
+    f = new ft.Filter(r, function(a) { return a.foo > 10; });  // f = r.filter(fn);
  38
+    r.processSop({sop:'state', rows:{'0':{id:'0',foo:12,bar:13},'1':{id:'1',foo:10,bar:14}}});
  39
+  });
  40
+  
  41
+  describe('delete row', function() {
  42
+    it('should emit an sop with the deleted row id', function(done) {
  43
+      f.on('sop', function(sop) {
  44
+        sop.should.eql({sop:'delete',id:'0'});
  45
+        done();
  46
+      });
  47
+      r.processSop({sop:'delete',id:'0'});
  48
+    });
  49
+  });
  50
+});
49  test/rlist-test.js
... ...
@@ -0,0 +1,49 @@
  1
+var rl = require('../lib/rlist.js');
  2
+var should = require('should');
  3
+
  4
+describe('RList', function() {
  5
+  var r;
  6
+  
  7
+  before(function(){
  8
+    r = new rl.RList();
  9
+    r.processLop({lop:'state', rows:[{id:'0',foo:12,bar:13},{id:'1',foo:10,bar:14}]});
  10
+  });
  11
+  
  12
+  describe('initial data', function() {
  13
+    it('should return the initial data', function() {
  14
+      r.getRows().should.eql([{bar:13,id:'0',foo:12},
  15
+                              {bar:14,id:'1',foo:10}
  16
+                              ]);
  17
+    });
  18
+  });
  19
+  
  20
+  describe('insert row', function() {
  21
+    it('should emit an lop with the inserted row', function(done) {
  22
+      r.on('lop', function(lop) {
  23
+        lop.should.eql({lop:'insert',pos:0,row:{id:'2',foo:6,bar:15}});
  24
+        done();
  25
+      });
  26
+      r.processLop({lop:'insert',pos:0,row:{id:'2',foo:6,bar:15}});
  27
+    });
  28
+  });
  29
+});
  30
+
  31
+describe('RList', function() {
  32
+  var r;
  33
+  
  34
+  before(function(){
  35
+    r = new rl.RList();
  36
+    r.processLop({lop:'state', rows:[{id:'0',foo:12,bar:13},{id:'1',foo:10,bar:14}]});
  37
+  });
  38
+
  39
+  describe('delete row', function() {
  40
+    it('should emit an lop with the deleted row id', function(done) {
  41
+      r.on('lop', function(lop) {
  42
+        lop.should.eql({lop:'delete',pos:0});
  43
+        r.getRows().should.eql([{id:'1',foo:10,bar:14}]);
  44
+        done();
  45
+      });
  46
+      r.processLop({lop:'delete',pos:0});
  47
+    });
  48
+  });
  49
+});
48  test/rset-test.js
... ...
@@ -0,0 +1,48 @@
  1
+var rs = require('../lib/rset.js');
  2
+var should = require('should');
  3
+
  4
+describe('RSet', function() {
  5
+  var r;
  6
+  
  7
+  before(function(){
  8
+    r = new rs.RSet();
  9
+    r.processSop({sop:'state', rows:{'0':{id:'0',foo:12,bar:13},'1':{id:'1',foo:10,bar:14}}});
  10
+  });
  11
+  
  12
+  describe('initial data', function() {
  13
+    it('should return the initial data', function() {
  14
+      r.getRows().should.eql({'0':{bar:13,id:'0',foo:12},
  15
+                              '1':{bar:14,id:'1',foo:10}
  16
+                              });
  17
+    });
  18
+  });
  19
+  
  20
+  describe('insert row', function() {
  21
+    it('should emit an sop with the inserted row', function(done) {
  22
+      r.on('sop', function(sop) {
  23
+        sop.should.eql({sop:'insert',row:{id:'2',foo:6,bar:15}});
  24
+        done();
  25
+      });
  26
+      r.processSop({sop:'insert',row:{id:'2',foo:6,bar:15}});
  27
+    });
  28
+  });
  29
+});
  30
+
  31
+describe('RSet', function() {
  32
+  var r;
  33
+  
  34
+  before(function(){
  35
+    r = new rs.RSet();
  36
+    r.processSop({sop:'state', rows:{'0':{id:'0',foo:12,bar:13},'1':{id:'1',foo:10,bar:14}}});
  37
+  });
  38
+  
  39
+  describe('delete row', function() {
  40
+    it('should emit an sop with the deleted row id', function(done) {
  41
+      r.on('sop', function(sop) {
  42
+        sop.should.eql({sop:'delete',id:'1'});
  43
+        done();
  44
+      });
  45
+      r.processSop({sop:'delete',id:'1'});
  46
+    });
  47
+  });
  48
+});

0 notes on commit f2e2bfe

Please sign in to comment.
Something went wrong with that request. Please try again.