diff --git a/lib/model/notification.js b/lib/model/notification.js index 13c29f2..2a7de39 100644 --- a/lib/model/notification.js +++ b/lib/model/notification.js @@ -32,6 +32,7 @@ Notification.create = function(opts) { x.version = '1.0.0'; var now = Date.now(); + x.createdOn = Math.floor(now / 1000); x.id = _.padLeft(now, 14, '0') + _.padLeft(opts.ticker || 0, 4, '0'); x.type = opts.type || 'general'; diff --git a/lib/server.js b/lib/server.js index 683069b..bd01531 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1658,6 +1658,24 @@ WalletService.prototype.getNotifications = function(opts, cb) { }); }; +/** + * Retrieves notifications after a specific id or from a given ts (whichever is more recent). + * + * @param {Object} opts + * @param {Object} opts.notificationId (optional) + * @param {Object} opts.minTs (optional) - default now - 60 sec. + * @returns {Notification[]} Notifications + */ +WalletService.prototype.getLatestNotifications = function(opts, cb) { + var self = this; + opts = opts || {}; + + var minTs = _.isNumber(opts.minTs) ? opts.minTs : +Date.now() - (60 * 1000); + self.storage.fetchLatestNotifications(self.walletId, opts.notificationId, minTs, function(err, notifications) { + if (err) return cb(err); + return cb(null, notifications); + }); +}; WalletService.prototype._normalizeTxHistory = function(txs) { var now = Math.floor(Date.now() / 1000); diff --git a/lib/storage.js b/lib/storage.js index 61dd719..82894db 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -315,6 +315,44 @@ Storage.prototype.fetchNotifications = function(walletId, opts, cb) { }); }; +/** + * Retrieves notifications after a specific id or from a given ts (whichever is more recent). + * + * @param {String} notificationId + * @param {Number} minTs + * @returns {Notification[]} Notifications + */ +Storage.prototype.fetchLatestNotifications = function(walletId, notificationId, minTs, cb) { + function makeId(timestamp) { + return _.padLeft(timestamp, 14, '0') + _.repeat('0', 4); + }; + + var self = this; + + var minId = makeId(minTs); + if (notificationId) { + minId = notificationId > minId ? notificationId : minId; + } + + this.db.collection(collections.NOTIFICATIONS) + .find({ + walletId: walletId, + id: { + $gt: minId, + }, + }) + .sort({ + id: 1 + }) + .toArray(function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + var notifications = _.map(result, function(notification) { + return Model.Notification.fromObj(notification); + }); + return cb(null, notifications); + }); +}; // TODO: remove walletId from signature Storage.prototype.storeNotification = function(walletId, notification, cb) { diff --git a/test/integration/server.js b/test/integration/server.js index 41fc629..b8be06b 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3876,72 +3876,78 @@ describe('Wallet service', function() { }); describe('Notifications', function() { + var clock; var server, wallet; beforeEach(function(done) { + clock = sinon.useFakeTimers(10 * 1000, 'Date'); helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; helpers.stubUtxos(server, wallet, _.range(4), function() { var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.01, TestData.copayers[0].privKey_1H_0); async.eachSeries(_.range(3), function(i, next) { + clock.tick(25 * 1000); server.createTx(txOpts, function(err, tx) { should.not.exist(err); next(); }); }, function(err) { + clock.tick(20 * 1000); return done(err); }); }); }); }); + afterEach(function() { + clock.restore(); + }); - it('should pull the last 4 notifications after 3 TXs', function(done) { - server.getNotifications({ - limit: 4, - reverse: true, + it('should pull all notifications', function(done) { + server.getLatestNotifications({ + minTs: 0 }, function(err, notifications) { should.not.exist(err); var types = _.pluck(notifications, 'type'); - types.should.deep.equal(['NewTxProposal', 'NewTxProposal', 'NewTxProposal', 'NewAddress']); + types.should.deep.equal(['NewCopayer', 'NewAddress', 'NewAddress', 'NewTxProposal', 'NewTxProposal', 'NewTxProposal']); var walletIds = _.uniq(_.pluck(notifications, 'walletId')); walletIds.length.should.equal(1); walletIds[0].should.equal(wallet.id); - var creators = _.uniq(_.pluck(notifications, 'creatorId')); + var creators = _.uniq(_.compact(_.pluck(notifications, 'creatorId'))); creators.length.should.equal(1); creators[0].should.equal(wallet.copayers[0].id); done(); }); }); - it('should pull the last 4 notifications, using now', function(done) { - server.getNotifications({ - limit: 4, - reverse: true, - maxTs: Date.now() / 1000, - minTs: Date.now() / 1000 - 1000, - }, function(err, notifications) { + it('should pull notifications in the last 60 seconds', function(done) { + server.getLatestNotifications({}, function(err, notifications) { should.not.exist(err); var types = _.pluck(notifications, 'type'); - types.should.deep.equal(['NewTxProposal', 'NewTxProposal', 'NewTxProposal', 'NewAddress']); + types.should.deep.equal(['NewTxProposal', 'NewTxProposal']); done(); }); }); - it('should pull all notifications after wallet creation', function(done) { - server.getNotifications({ - minTs: 0, + it('should pull notifications after a given notification id', function(done) { + server.getLatestNotifications({ + minTs: 0 }, function(err, notifications) { should.not.exist(err); - var types = _.pluck(notifications, 'type'); - types[0].should.equal('NewCopayer'); - types[types.length - 1].should.equal('NewTxProposal'); - done(); + var from = _.first(_.takeRight(notifications, 2)).id; // second to last + server.getLatestNotifications({ + notificationId: from + }, function(err, res) { + should.not.exist(err); + res.length.should.equal(1); + res[0].id.should.equal(_.first(_.takeRight(notifications)).id); + done(); + }); }); }); it('should contain walletId & creatorId on NewCopayer', function(done) { - server.getNotifications({ + server.getLatestNotifications({ minTs: 0, }, function(err, notifications) { should.not.exist(err); @@ -3962,13 +3968,13 @@ describe('Wallet service', function() { txProposalId: tx.id, signatures: signatures, }, function(err) { - server.getNotifications({ - limit: 3, - reverse: true, + server.getLatestNotifications({ + minTs: Date.now(), }, function(err, notifications) { should.not.exist(err); + notifications.length.should.equal(2); var types = _.pluck(notifications, 'type'); - types.should.deep.equal(['TxProposalFinallyAccepted', 'TxProposalAcceptedBy', 'NewTxProposal']); + types.should.deep.equal(['TxProposalAcceptedBy', 'TxProposalFinallyAccepted']); done(); }); }); @@ -3982,13 +3988,13 @@ describe('Wallet service', function() { txProposalId: tx.id, }, function(err) { should.not.exist(err); - server.getNotifications({ - limit: 2, - reverse: true, + server.getLatestNotifications({ + minTs: Date.now(), }, function(err, notifications) { should.not.exist(err); + notifications.length.should.equal(2); var types = _.pluck(notifications, 'type'); - types.should.deep.equal(['TxProposalFinallyRejected', 'TxProposalRejectedBy']); + types.should.deep.equal(['TxProposalRejectedBy', 'TxProposalFinallyRejected']); done(); }); }); @@ -4010,13 +4016,13 @@ describe('Wallet service', function() { txProposalId: tx.id }, function(err, txp) { should.not.exist(err); - server.getNotifications({ - limit: 3, - reverse: true, + server.getLatestNotifications({ + minTs: Date.now(), }, function(err, notifications) { should.not.exist(err); + notifications.length.should.equal(3); var types = _.pluck(notifications, 'type'); - types.should.deep.equal(['NewOutgoingTx', 'TxProposalFinallyAccepted', 'TxProposalAcceptedBy']); + types.should.deep.equal(['TxProposalAcceptedBy', 'TxProposalFinallyAccepted', 'NewOutgoingTx']); done(); }); }); @@ -4042,13 +4048,13 @@ describe('Wallet service', function() { txProposalId: tx.id }, function(err, txp) { should.not.exist(err); - server.getNotifications({ - limit: 3, - reverse: true, + server.getLatestNotifications({ + minTs: Date.now(), }, function(err, notifications) { should.not.exist(err); + notifications.length.should.equal(3); var types = _.pluck(notifications, 'type'); - types.should.deep.equal(['NewOutgoingTxByThirdParty', 'TxProposalFinallyAccepted', 'TxProposalAcceptedBy']); + types.should.deep.equal(['TxProposalAcceptedBy', 'TxProposalFinallyAccepted', 'NewOutgoingTxByThirdParty']); done(); }); });