mysql-live-select
Advanced tools
Comparing version 0.0.24 to 1.0.0
@@ -25,6 +25,14 @@ /* mysql-live-select, MIT License ben@latenightsketches.com | ||
table: table, | ||
condition: function(row, newRow){ return row.id === id; } | ||
} ]).on('update', function(data){ | ||
condition: function(row, newRow){ | ||
// Only refresh the results when the row matching the specified id is | ||
// changed. | ||
return row.id === id | ||
// On UPDATE queries, newRow must be checked as well | ||
|| (newRow && newRow.id === id); | ||
} | ||
} ]).on('update', function(diff, data){ | ||
// diff contains an object describing the difference since the previous update | ||
// data contains an array of rows of the new result set | ||
console.log(data); | ||
}); | ||
@@ -38,11 +38,8 @@ /* mysql-live-select, MIT License ben@latenightsketches.com | ||
for(var query in self._queryCache){ | ||
if (!self._queryCache.hasOwnProperty(query)){ | ||
continue; | ||
Object.keys(self._queryCache).forEach(function(query) { | ||
var curCache = self._queryCache[query]; | ||
if(curCache.updateTimeout === null && curCache.matchRowEvent(event)){ | ||
curCache.invalidate(); | ||
} | ||
var queryCache = self._queryCache[query]; | ||
if(!queryCache.canSkipRowEvent() && queryCache.matchRowEvent(event)){ | ||
queryCache.invalidate(); | ||
} | ||
} | ||
}); | ||
}) | ||
@@ -153,8 +150,5 @@ | ||
// Update all select statements | ||
for(var query in self._queryCache){ | ||
if (!self._queryCache.hasOwnProperty(query)){ | ||
continue; | ||
} | ||
Object.keys(self._queryCache).forEach(function(query) { | ||
self._queryCache[query].invalidate(); | ||
} | ||
}); | ||
}; | ||
@@ -171,2 +165,5 @@ | ||
// Expose diff apply function statically | ||
LiveMysql.applyDiff = require('./differ').apply; | ||
module.exports = LiveMysql; |
@@ -5,2 +5,3 @@ /* mysql-live-select, MIT License ben@latenightsketches.com | ||
var util = require('util'); | ||
var _ = require('lodash'); | ||
@@ -34,17 +35,12 @@ function LiveMysqlSelect(queryCache, triggers, base){ | ||
self.emit('update', queryCache.data); | ||
// Initial diff is the currently cached data with _index keys as well | ||
var diffAdded = queryCache.data.map(function(row, index) { | ||
var modifiedRow = _.clone(row); | ||
row._index = index + 1; | ||
return row; | ||
}); | ||
if(queryCache.data.length !== 0 && !self.base.settings.skipDiff){ | ||
var diff = queryCache.data.map(function(row, index) { | ||
return [ 'added', row, index ]; | ||
}); | ||
diff.forEach(function(evt){ | ||
self.emit.apply(self, evt); | ||
// New row added to end | ||
self.data[evt[2]] = evt[1]; | ||
}); | ||
// Output all difference events in a single event | ||
self.emit('diff', diff); | ||
} | ||
self.emit('update', | ||
{ removed: null, moved: null, copied: null, added: diffAdded }, | ||
queryCache.data); | ||
}, 50); | ||
@@ -51,0 +47,0 @@ }else{ |
@@ -1,8 +0,15 @@ | ||
/* mysql-live-select, MIT License ben@latenightsketches.com, wj32.64@gmail.com | ||
lib/QueryCache.js - Query Results Cache Class */ | ||
/* mysql-live-select, MIT License ben@latenightsketches.com, wj32.64@gmail.com | ||
lib/QueryCache.js - Query Results Cache Class | ||
// Many LiveMysqlSelect objects can share the same query cache if they have the | ||
// same query string. | ||
Many LiveMysqlSelect objects can share the same query cache if they have the | ||
same query string. | ||
function QueryCache(query, base){ | ||
*/ | ||
var _ = require('lodash'); | ||
var md5 = require('md5'); | ||
var differ = require('./differ'); | ||
function QueryCache(query, base) { | ||
if(!query) | ||
@@ -18,9 +25,13 @@ throw new Error('query required'); | ||
self.data = []; | ||
self.oldHashes = []; | ||
self.selects = []; | ||
self.initialized = false; | ||
self.updateTimeout = null; | ||
} | ||
QueryCache.prototype._setDataOnSelects = function(){ | ||
QueryCache.prototype.setData = function(rows) { | ||
var self = this; | ||
for(var i = 0; i < self.selects.length; i++){ | ||
self.data = rows; | ||
for(var i = 0; i < self.selects.length; i++) { | ||
self.selects[i].data = self.data; | ||
@@ -30,126 +41,88 @@ } | ||
QueryCache.prototype._emitOnSelects = function(name, arg){ | ||
QueryCache.prototype._emitOnSelects = function(/* arguments */) { | ||
var self = this; | ||
for(var i = 0; i < self.selects.length; i++){ | ||
self.selects[i].emit(name, arg); | ||
} | ||
}; | ||
QueryCache.prototype._emitApplyOnSelects = function(evt){ | ||
var self = this; | ||
for(var i = 0; i < self.selects.length; i++){ | ||
var select = self.selects[i]; | ||
select.emit.apply(select, evt); | ||
select.emit.apply(select, arguments); | ||
} | ||
}; | ||
QueryCache.prototype._setRows = function(rows){ | ||
QueryCache.prototype.matchRowEvent = function(event) { | ||
var self = this; | ||
var diff = []; | ||
// Determine what changes before updating cache in order to | ||
// be able to skip all event emissions if no change | ||
// TODO update this algorithm to use less data | ||
rows.forEach(function(row, index){ | ||
if(self.data.length - 1 < index){ | ||
diff.push([ 'added', row, index ]); | ||
}else if(JSON.stringify(self.data[index]) !== JSON.stringify(row)){ | ||
diff.push([ 'changed', self.data[index], row, index ]); | ||
} | ||
}); | ||
if(self.data.length > rows.length){ | ||
for(var i = self.data.length - 1; i >= rows.length; i--){ | ||
diff.push([ 'removed', self.data[i], i ]); | ||
} | ||
for(var i = 0; i < self.selects.length; i++) { | ||
if(self.selects[i].matchRowEvent(event)) return true; | ||
} | ||
if(diff.length !== 0){ | ||
self._emitOnSelects('update', rows); | ||
// Make sure the relevant select objects have the right data array. | ||
self._setDataOnSelects(); | ||
diff.forEach(function(evt){ | ||
if(!self.base.settings.skipDiff){ | ||
self._emitApplyOnSelects(evt); | ||
} | ||
switch(evt[0]){ | ||
case 'added': | ||
// New row added to end | ||
self.data[evt[2]] = evt[1]; | ||
break; | ||
case 'changed': | ||
// Update row data reference | ||
self.data[evt[3]] = evt[2]; | ||
break; | ||
case 'removed': | ||
// Remove extra rows off the end | ||
self.data.splice(evt[2], 1); | ||
break; | ||
} | ||
}); | ||
if(!self.base.settings.skipDiff){ | ||
// Output all difference events in a single event | ||
self._emitOnSelects('diff', diff); | ||
} | ||
}else if(self.initialized === false){ | ||
// If the result set initializes to 0 rows, it still needs to output an | ||
// update event. | ||
self._emitOnSelects('update', rows); | ||
} | ||
self.initialized = true; | ||
self.lastUpdate = Date.now(); | ||
}; | ||
QueryCache.prototype.matchRowEvent = function(event){ | ||
var self = this; | ||
for(var i = 0; i < self.selects.length; i++){ | ||
var select = self.selects[i]; | ||
if (select.matchRowEvent(event)){ | ||
return true; | ||
} | ||
} | ||
return false; | ||
}; | ||
QueryCache.prototype.canSkipRowEvent = function(){ | ||
QueryCache.prototype.invalidate = function() { | ||
var self = this; | ||
return self.base.settings.canSkipRowEvents && self._updateTimeout !== undefined; | ||
}; | ||
QueryCache.prototype.invalidate = function(){ | ||
var self = this; | ||
function update(){ | ||
function update() { | ||
// Refuse to send more than one query out at a time. Note that the code | ||
// below always sets needUpdate to true; when the current query finishes | ||
// below always sets needUpdate to false; when the current query finishes | ||
// running we will check needUpdate again and re-run the query if necessary. | ||
if(self.updating){ | ||
if(self.updating) { | ||
self.needUpdate = true; | ||
return; | ||
} | ||
self.updating = true; | ||
self.needUpdate = false; | ||
self.base.db.query(self.query, function(error, rows){ | ||
// Perform the update | ||
self.base.db.query(self.query, function(error, rows) { | ||
self.updating = false; | ||
if(error){ | ||
self._emitOnSelects('error', error); | ||
}else{ | ||
self._setRows(rows); | ||
if(self.needUpdate){ | ||
update(); | ||
if(error) return self._emitOnSelects('error', error); | ||
if(rows.length === 0 && self.initialized === false) { | ||
// If the result set initializes to 0 rows, it still needs to output an | ||
// update event. | ||
self._emitOnSelects('update', | ||
{ removed: null, moved: null, copied: null, added: [] }, | ||
[] | ||
); | ||
} else { | ||
// Perform deep clone of new data to be modified for the differ | ||
var rowsForDiff = _.clone(rows, true); | ||
var newHashes = rows.map(function(row, index) { | ||
var hash = md5(JSON.stringify(row)); | ||
// Provide the differ with the necessary details | ||
rowsForDiff[index]._hash = hash; | ||
rowsForDiff[index]._index = index + 1; | ||
return hash; | ||
}); | ||
var diff = | ||
filterHashProperties(differ.generate(self.oldHashes, rowsForDiff)); | ||
if(diff !== null) { | ||
self._emitOnSelects('update', diff, rows); | ||
// Now that event has been emitted, the new becomes the old | ||
self.oldHashes = newHashes; | ||
} | ||
} | ||
self.setData(rows); | ||
self.initialized = true; | ||
self.lastUpdate = Date.now(); | ||
if(self.needUpdate) update(); | ||
}); | ||
} | ||
if(self.base.settings.minInterval === undefined){ | ||
if(typeof self.base.settings.minInterval !== 'number') { | ||
update(); | ||
}else if(self.lastUpdate + self.base.settings.minInterval < Date.now()){ | ||
} else if(self.lastUpdate + self.base.settings.minInterval < Date.now()) { | ||
update(); | ||
}else{ // Before minInterval | ||
if(!self._updateTimeout){ | ||
self._updateTimeout = setTimeout(function(){ | ||
delete self._updateTimeout; | ||
} else { // Before minInterval | ||
if(self.updateTimeout === null){ | ||
self.updateTimeout = setTimeout(function(){ | ||
self.updateTimeout = null; | ||
update(); | ||
@@ -162,1 +135,18 @@ }, self.lastUpdate + self.base.settings.minInterval - Date.now()); | ||
module.exports = QueryCache; | ||
function filterHashProperties(diff, alsoIndex) { | ||
if(diff instanceof Array) { | ||
var omitKeys = [ '_hash' ]; | ||
if(alsoIndex) omitKeys.push('_index'); | ||
return diff.map(function(event) { | ||
return _.omit(event, omitKeys) | ||
}); | ||
} | ||
// Otherwise, diff is object with arrays for keys | ||
_.forOwn(diff, function(rows, key) { | ||
diff[key] = filterHashProperties(rows, alsoIndex) | ||
}); | ||
return diff; | ||
} | ||
{ | ||
"name": "mysql-live-select", | ||
"version": "0.0.24", | ||
"version": "1.0.0", | ||
"description": "Live updating MySQL SELECT statements", | ||
@@ -20,2 +20,4 @@ "main": "lib/LiveMysql.js", | ||
"dependencies": { | ||
"lodash": "^3.10.1", | ||
"md5": "^2.0.0", | ||
"mysql": "^2.8.0", | ||
@@ -22,0 +24,0 @@ "zongji": "^0.3.2" |
@@ -33,3 +33,3 @@ # mysql-live-select [![Build Status](https://travis-ci.org/numtel/mysql-live-select.svg?branch=master)](https://travis-ci.org/numtel/mysql-live-select) | ||
```sql | ||
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'user'@'localhost' | ||
GRANT REPLICATION SLAVE, REPLICATION CLIENT, SELECT ON *.* TO 'user'@'localhost' | ||
``` | ||
@@ -55,3 +55,2 @@ | ||
`minInterval` | `integer` | Pass a number of milliseconds to use as the minimum between result set updates. Omit to refresh results on every update. May be changed at runtime. | ||
`skipDiff` | `boolean` | If `true`, the `added`, `changed`, and `removed` events will not be emitted. May be changed at runtime.<br>**Default:** `false` | ||
@@ -71,4 +70,12 @@ ```javascript | ||
table: table, | ||
condition: function(row, newRow){ return row.id === id; } | ||
} ]).on('update', function(data){ | ||
condition: function(row, newRow){ | ||
// Only refresh the results when the row matching the specified id is | ||
// changed. | ||
return row.id === id | ||
// On UPDATE queries, newRow must be checked as well | ||
|| (newRow && newRow.id === id); | ||
} | ||
} ]).on('update', function(diff, data){ | ||
// diff contains an object describing the difference since the previous update | ||
// data contains an array of rows of the new result set | ||
console.log(data); | ||
@@ -128,2 +135,6 @@ }); | ||
### LiveMysql.applyDiff(data, diff) | ||
Exposed statically on the LiveMysql object is a function for applying a `diff` given in an `update` event to an array of rows given in the `data` argument. | ||
## LiveMysqlSelect object | ||
@@ -146,7 +157,3 @@ | ||
-----------|-----------|--------------------------- | ||
`update` | `rows` | Single argument contains complete result set array. Called before `added`, `changed`, and `removed` events. | ||
`added` | `row`, `index` | Row added to result set at index | ||
`changed` | `row`, `newRow`, `index` | Row contents mutated at index | ||
`removed` | `row`, `index` | Row removed at index | ||
`diff` | `diff` | Aggregation of `added`, `changed`, `removed` events for current event into a single array for easier handling of multiple changes | ||
`update` | `diff`, `data` | First argument contains an object describing the difference since the previous `update` event with `added`, `removed`, `moved`, and `copied` rows. Second argument contains complete result set array. | ||
`error` | `error` | Unhandled errors will be thrown | ||
@@ -153,0 +160,0 @@ |
@@ -67,7 +67,18 @@ /* mysql-live-select, MIT License ben@latenightsketches.com | ||
conn.select(query, triggers).on('update', function(data){ | ||
conn.select(query, triggers).on('update', function(diff, data) { | ||
// After initial update | ||
if(data.length > 0 && data[0].col === 10){ | ||
// Second select instance to check resultsBuffer | ||
conn.select(query, triggersSimple).on('update', function(data){ | ||
var secondInstanceInitialized = false; | ||
conn.select(query, triggersSimple).on('update', function(diff, data) { | ||
if(secondInstanceInitialized === false) { | ||
// Check that the diff is correct when initializing from cache | ||
test.deepEqual(diff, | ||
{ removed: null, | ||
moved: null, | ||
copied: null, | ||
added: [ { col: 10, _index: 1 } ] }); | ||
secondInstanceInitialized = true; | ||
} | ||
if(data.length > 0 && data[0].col === 15){ | ||
@@ -81,43 +92,43 @@ // [1] Test in LiveMysqlSelect created later, | ||
} | ||
}).on('added', function(row, index){ | ||
test.equal(index, 0); | ||
test.equal(row.col, 10); | ||
}).on('changed', function(row, newRow, index){ | ||
test.equal(index, 0); | ||
test.equal(row.col, 10); | ||
test.equal(newRow.col, 15); | ||
}).on('removed', function(row, index){ | ||
test.equal(index, 0); | ||
test.equal(row.col, 15); | ||
}).on('diff', function(diff){ | ||
// Only one row will change at once | ||
test.equal(diff.length, 1); | ||
// Index will always be 0, the first item | ||
test.equal(diff[0][diff[0].length - 1], 0); | ||
switch(diff[0][0]){ | ||
case 'added': | ||
test.equal(diff[0][1].col, 10); | ||
break; | ||
case 'changed': | ||
test.equal(diff[0][1].col, 10); | ||
test.equal(diff[0][2].col, 15); | ||
break; | ||
case 'added': | ||
test.equal(diff[0][1].col, 15); | ||
break; | ||
} | ||
}); | ||
} | ||
switch(conditionCheckIndex) { | ||
case 0: | ||
// Initialized as empty | ||
test.equal(data.length, 0); | ||
test.deepEqual(diff, | ||
{ removed: null, moved: null, copied: null, added: [] }); | ||
break; | ||
case 1: | ||
// Row has been inserted | ||
test.equal(data[0].col, 10); | ||
test.equal(data.length, 1); | ||
test.deepEqual(diff, | ||
{ removed: null, | ||
moved: null, | ||
copied: null, | ||
added: [ { col: 10, _index: 1 } ] }); | ||
break; | ||
case 2: | ||
// Row has been updated | ||
test.equal(data[0].col, 15); | ||
test.equal(data.length, 1); | ||
test.deepEqual(diff, | ||
{ removed: [ { _index: 1 } ], | ||
moved: null, | ||
copied: null, | ||
added: [ { col: 15, _index: 1 } ] }); | ||
break; | ||
case 3: | ||
// Row has been deleted | ||
test.equal(data.length, 0); | ||
test.deepEqual(diff, | ||
{ removed: [ { _index: 1 } ], | ||
moved: null, | ||
copied: null, | ||
added: null }); | ||
test.done(); | ||
break; | ||
} | ||
}).on('added', function(row, index){ | ||
test.equal(index, 0); | ||
test.equal(row.col, 10); | ||
}).on('changed', function(row, newRow, index){ | ||
test.equal(index, 0); | ||
test.equal(row.col, 10); | ||
test.equal(newRow.col, 15); | ||
}).on('removed', function(row, index){ | ||
test.equal(index, 0); | ||
test.equal(row.col, 15); | ||
test.done(); | ||
}); | ||
@@ -142,74 +153,2 @@ | ||
}, | ||
skipDiff: function(test){ | ||
var table = 'skip_diff'; | ||
server.on('ready', function(conn, esc, escId, queries){ | ||
querySequence(conn.db, [ | ||
'DROP TABLE IF EXISTS ' + escId(table), | ||
'CREATE TABLE ' + escId(table) + ' (col INT UNSIGNED)', | ||
'INSERT INTO ' + escId(table) + ' (col) VALUES (10)', | ||
], function(results){ | ||
var error = function(){ | ||
throw new Error('diff events should not be called'); | ||
}; | ||
conn.settings.skipDiff = true; | ||
conn.select('SELECT * FROM ' + escId(table), [ { | ||
table: table, | ||
database: server.database | ||
} ]).on('update', function(rows){ | ||
if(rows.length > 0 && rows[0].col === 10){ | ||
test.ok(true); | ||
}else if(rows.length > 0 && rows[0].col === 15){ | ||
conn.db.query('DELETE FROM ' + escId(table)); | ||
}else if(rows.length === 0){ | ||
// Give time, just in case the `removed` event comes in | ||
setTimeout(function(){ | ||
conn.settings.skipDiff = false; | ||
test.done(); | ||
}, 100); | ||
} | ||
}) | ||
.on('added', error) | ||
.on('changed', error) | ||
.on('removed', error) | ||
.on('diff', error); | ||
querySequence(conn.db, [ | ||
'UPDATE ' + escId(table) + | ||
' SET `col` = 15' | ||
], function(results){ | ||
// ... | ||
}); | ||
}); | ||
}); | ||
}, | ||
emptyResults: function(test){ | ||
var waitTime = 500; | ||
var table = 'empty_results'; | ||
server.on('ready', function(conn, esc, escId, queries){ | ||
querySequence(conn.db, [ | ||
'DROP TABLE IF EXISTS ' + escId(table), | ||
'CREATE TABLE ' + escId(table) + ' (col INT UNSIGNED)', | ||
], function(results){ | ||
var pauseTime = Date.now(); | ||
conn.select('SELECT * FROM ' + escId(table), [ { | ||
table: table, | ||
database: server.database | ||
} ]).on('update', function(rows){ | ||
if(rows.length === 0) { | ||
// Initialized with no rows, so add one | ||
querySequence(conn.db, [ | ||
'INSERT INTO ' + escId(table) + ' (col) VALUES (10)' | ||
], function(results){ | ||
// ... | ||
}); | ||
}else if(rows.length > 0 && rows[0].col === 10){ | ||
// Row was added, all done | ||
test.done(); | ||
} | ||
}); | ||
}); | ||
}); | ||
}, | ||
pauseAndResume: function(test){ | ||
@@ -228,3 +167,3 @@ var waitTime = 500; | ||
database: server.database | ||
} ]).on('update', function(rows){ | ||
} ]).on('update', function(diff, rows){ | ||
if(rows.length > 0 && rows[0].col === 10){ | ||
@@ -252,6 +191,6 @@ test.ok(true); | ||
}, | ||
stopAndActive: function(test){ | ||
// Must be last test that uses binlog updates since it calls stop() | ||
stopAndActive: function(test) { | ||
// NOTE: Must be last test that uses binlog updates since it calls stop() | ||
var table = 'stop_active'; | ||
server.on('ready', function(conn, esc, escId, queries){ | ||
server.on('ready', function(conn, esc, escId, queries) { | ||
querySequence(conn.db, [ | ||
@@ -266,10 +205,9 @@ 'DROP TABLE IF EXISTS ' + escId(table), | ||
database: server.database | ||
} ]).on('update', function(rows){ | ||
if(rows.length > 0 && rows[0].col === 10){ | ||
} ]).on('update', function(diff, rows) { | ||
if(rows.length > 0 && rows[0].col === 10) { | ||
test.ok(true); | ||
}else if(rows.length > 0 && rows[0].col === 15){ | ||
}else if(rows.length > 0 && rows[0].col === 15) { | ||
test.ok(this.active()); | ||
this.stop(); | ||
// When all instances of query removed, resultsBuffer removed too | ||
// TODO: Update for queryCache | ||
test.equal(typeof conn._queryCache[query], 'undefined'); | ||
@@ -279,8 +217,8 @@ | ||
conn.db.query('DELETE FROM ' + escId(table)); | ||
setTimeout(function(){ | ||
setTimeout(function() { | ||
test.done(); | ||
}, 100); | ||
}else if(rows.length === 0) { | ||
throw new Error('Select should have been stopped!'); | ||
} | ||
}).on('removed', function(row, index){ | ||
throw new Error('should not be called'); | ||
}); | ||
@@ -287,0 +225,0 @@ |
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
43167
20
821
1
165
4
1
+ Addedlodash@^3.10.1
+ Addedmd5@^2.0.0
+ Addedcharenc@0.0.2(transitive)
+ Addedcrypt@0.0.2(transitive)
+ Addedis-buffer@1.1.6(transitive)
+ Addedlodash@3.10.1(transitive)
+ Addedmd5@2.3.0(transitive)