From 006653ed8ee9ed29c4557f6aaba9327eba5e9fb1 Mon Sep 17 00:00:00 2001 From: lilia Date: Thu, 11 Dec 2014 19:41:40 -0800 Subject: [PATCH] DB/Index Redux This change removes the timestamp field from messages and conversations in favor of multiple semantically named timestamp fields: sent_at, received_at on messages; active_at on conversations. This requires/lets us rethink and improve our indexing scheme thusly: The inbox index on conversations will order entries by the conversation.active_at property, which should only appear on conversations destined for the inbox. The receipt index will use the message.sent_at property, for effecient lookup of outgoing messages by timestamp, for use in processing delivery receipts. The group index on conversation.members is multi-entry, meaning that looking up any phone number in this index will efficiently yield all groups the number belongs to. The conversation index lets us scan messages in a single conversation, in the order they were received (or the reverse order). It is a compound index on [conversationId, received_at]. --- js/database.js | 8 +++--- js/models/conversations.js | 51 +++++++++++++++++++++++++------------- js/models/messages.js | 18 +++++++------- js/views/inbox_view.js | 14 +++++++---- js/views/message_view.js | 2 +- 5 files changed, 58 insertions(+), 35 deletions(-) diff --git a/js/database.js b/js/database.js index b45702a1..b036f9b2 100644 --- a/js/database.js +++ b/js/database.js @@ -24,12 +24,14 @@ { version: "1.0", migrate: function(transaction, next) { - console.log('migratetion 1.0'); + console.log('migration 1.0'); var messages = transaction.db.createObjectStore("messages"); - messages.createIndex("conversation", "conversationId", { unique: false }); + messages.createIndex("conversation", ["conversationId", "received_at"], { unique: false }); + messages.createIndex("receipt", "sent_at", { unique: false }); var conversations = transaction.db.createObjectStore("conversations"); - conversations.createIndex("timestamp", "timestamp", { unique: false }); + conversations.createIndex("inbox", "active_at", { unique: false }); + conversations.createIndex("group", "members", { unique: false, multiEntry: true }); next(); } } diff --git a/js/models/conversations.js b/js/models/conversations.js index a30de0c4..e4a3c511 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -22,12 +22,13 @@ database: Whisper.Database, storeName: 'conversations', defaults: function() { + var timestamp = new Date().getTime(); return { - name: 'New Conversation', - image: '/images/default.png', - unreadCount: 0, - timestamp: new Date().getTime(), - active: true + name : 'New Conversation', + image : '/images/default.png', + unreadCount : 0, + timestamp : timestamp, + active_at : timestamp }; }, @@ -42,18 +43,20 @@ }, sendMessage: function(message, attachments) { - var timestamp = Date.now(); + var now = Date.now(); this.messageCollection.add({ - body : message, - timestamp : timestamp, - conversationId : this.id, - type : 'outgoing', - attachments : attachments, + body : message, + conversationId : this.id, + type : 'outgoing', + attachments : attachments, + sent_at : now, + received_at : now }).save(); - this.save({ timestamp: timestamp, - unreadCount: 0, - active: true}); + this.save({ + unreadCount : 0, + active_at : now + }); if (this.get('type') == 'private') { return textsecure.messaging.sendMessageToNumber(this.get('id'), message, attachments); @@ -85,15 +88,29 @@ fetchMessages: function(options) { options = options || {}; - options.conditions = {conversationId: this.id }; + options.index = { + // 'conversation' index on conversationId + // WHERE conversationId = this.id ORDER received_at DESC + name : 'conversation', + lower : [this.id], + upper : [this.id, Number.MAX_VALUE], + order : 'desc' + }; return this.messageCollection.fetch(options); + // TODO pagination/infinite scroll + // limit: 10, offset: page*10, + }, + + archive: function() { + this.unset('active_at'); }, destroyMessages: function() { var models = this.messageCollection.models; this.messageCollection.reset([]); _.each(models, function(message) { message.destroy(); }); - return this.save({active: false}); + this.archive(); + return this.save(); } }); @@ -110,7 +127,7 @@ var attributes = {}; attributes = { name : name, - numbers : recipients, + members : recipients, type : 'group', }; var conversation = this.add(attributes, {merge: true}); diff --git a/js/models/messages.js b/js/models/messages.js index ca122c36..c425122e 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -19,16 +19,16 @@ window.Whisper = window.Whisper || {}; var Message = Backbone.Model.extend({ - database: Whisper.Database, - storeName: 'messages', - defaults: function() { + database : Whisper.Database, + storeName : 'messages', + defaults : function() { return { timestamp: new Date().getTime(), attachments: [] }; }, validate: function(attributes, options) { - var required = ['timestamp', 'conversationId']; + var required = ['conversationId', 'received_at', 'sent_at']; var missing = _.filter(required, function(attr) { return !attributes[attr]; }); if (missing.length) { console.log("Message missing attributes: " + missing); @@ -37,11 +37,11 @@ }); Whisper.MessageCollection = Backbone.Collection.extend({ - model: Message, - database: Whisper.Database, - storeName: 'messages', - comparator: 'timestamp', - destroyAll: function () { + model : Message, + database : Whisper.Database, + storeName : 'messages', + comparator : 'received_at', + destroyAll : function () { return Promise.all(this.models.map(function(m) { return new Promise(function(resolve, reject) { m.destroy().then(resolve).fail(reject); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 967a6c94..6eb9a08c 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -32,11 +32,15 @@ collection : this.conversations }); - this.conversations.fetch({ reset: true }).then(function() { - this.conversations.reset( - //TODO: Add an index to support this operation at the db level - this.conversations.filter(function(c) { return c.get('active'); }) - ); + this.conversations.fetch({ + index: { + name: 'inbox', // 'inbox' index on active_at + order: 'desc' // ORDER timestamp DESC + }, + reset: true + // TODO pagination/infinite scroll + // limit: 10, offset: page*10, + }).then(function() { if (this.conversations.length) { this.conversations.at(0).trigger('render'); } diff --git a/js/views/message_view.js b/js/views/message_view.js index 2cb94f34..5f26d207 100644 --- a/js/views/message_view.js +++ b/js/views/message_view.js @@ -73,7 +73,7 @@ this.$el.html( Mustache.render(this.template, { message: this.model.get('body'), - timestamp: moment(this.model.get('timestamp')).fromNow(), + timestamp: moment(this.model.get('received_at')).fromNow(), bubble_class: this.model.get('type') === 'outgoing' ? 'sent' : 'incoming', sender: this.model.get('sender') })