Move to centralized message/cache data layer
Also, ensure that conversation.messageCollection has nothing in it unless it has an associated ConversationView.pull/1/head
parent
34231168a7
commit
f39a96bc76
@ -0,0 +1,349 @@
|
||||
/* global window */
|
||||
|
||||
const { deferredToPromise } = require('./deferred_to_promise');
|
||||
const MessageType = require('./types/message');
|
||||
|
||||
// calls to search for:
|
||||
// .fetch(
|
||||
// .save(
|
||||
// .destroy(
|
||||
|
||||
async function saveMessage(data, { Message }) {
|
||||
const message = new Message(data);
|
||||
await deferredToPromise(message.save());
|
||||
return message.id;
|
||||
}
|
||||
|
||||
async function removeMessage(id, { Message }) {
|
||||
const message = await getMessageById(id, { Message });
|
||||
// Note: It's important to have a fully database-hydrated model to delete here because
|
||||
// it needs to delete all associated on-disk files along with the database delete.
|
||||
if (message) {
|
||||
await deferredToPromise(message.destroy());
|
||||
}
|
||||
}
|
||||
|
||||
async function getMessageById(id, { Message }) {
|
||||
const message = new Message({ id });
|
||||
try {
|
||||
await deferredToPromise(message.fetch());
|
||||
return message;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllMessageIds({ db, handleDOMException, getMessageKey }) {
|
||||
const lookup = Object.create(null);
|
||||
const storeName = 'messages';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(storeName, 'readwrite');
|
||||
transaction.onerror = () => {
|
||||
handleDOMException(
|
||||
`assembleLookup(${storeName}) transaction error`,
|
||||
transaction.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
transaction.oncomplete = () => {
|
||||
// not really very useful - fires at unexpected times
|
||||
};
|
||||
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.openCursor();
|
||||
request.onerror = () => {
|
||||
handleDOMException(
|
||||
`assembleLookup(${storeName}) request error`,
|
||||
request.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor && cursor.value) {
|
||||
lookup[getMessageKey(cursor.value)] = true;
|
||||
cursor.continue();
|
||||
} else {
|
||||
window.log.info(`Done creating ${storeName} lookup`);
|
||||
resolve(lookup);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getMessageBySender(
|
||||
// eslint-disable-next-line camelcase
|
||||
{ source, sourceDevice, sent_at },
|
||||
{ Message }
|
||||
) {
|
||||
const fetcher = new Message();
|
||||
const options = {
|
||||
index: {
|
||||
name: 'unique',
|
||||
// eslint-disable-next-line camelcase
|
||||
value: [source, sourceDevice, sent_at],
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await deferredToPromise(fetcher.fetch(options));
|
||||
if (fetcher.get('id')) {
|
||||
return fetcher;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getUnreadByConversation(conversationId, { MessageCollection }) {
|
||||
const messages = new MessageCollection();
|
||||
|
||||
await deferredToPromise(
|
||||
messages.fetch({
|
||||
index: {
|
||||
// 'unread' index
|
||||
name: 'unread',
|
||||
lower: [conversationId],
|
||||
upper: [conversationId, Number.MAX_VALUE],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function getMessagesByConversation(
|
||||
conversationId,
|
||||
{ limit = 100, receivedAt = Number.MAX_VALUE, MessageCollection }
|
||||
) {
|
||||
const messages = new MessageCollection();
|
||||
|
||||
const options = {
|
||||
limit,
|
||||
index: {
|
||||
// 'conversation' index on [conversationId, received_at]
|
||||
name: 'conversation',
|
||||
lower: [conversationId],
|
||||
upper: [conversationId, receivedAt],
|
||||
order: 'desc',
|
||||
// SELECT messages WHERE conversationId = this.id ORDER
|
||||
// received_at DESC
|
||||
},
|
||||
};
|
||||
await deferredToPromise(messages.fetch(options));
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function removeAllMessagesInConversation(
|
||||
conversationId,
|
||||
{ MessageCollection }
|
||||
) {
|
||||
const messages = new MessageCollection();
|
||||
|
||||
let loaded;
|
||||
do {
|
||||
// Yes, we really want the await in the loop. We're deleting 100 at a
|
||||
// time so we don't use too much memory.
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await deferredToPromise(
|
||||
messages.fetch({
|
||||
limit: 100,
|
||||
index: {
|
||||
// 'conversation' index on [conversationId, received_at]
|
||||
name: 'conversation',
|
||||
lower: [conversationId],
|
||||
upper: [conversationId, Number.MAX_VALUE],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
loaded = messages.models;
|
||||
messages.reset([]);
|
||||
|
||||
// Note: It's very important that these models are fully hydrated because
|
||||
// we need to delete all associated on-disk files along with the database delete.
|
||||
loaded.map(message => message.destroy());
|
||||
} while (loaded.length > 0);
|
||||
}
|
||||
|
||||
async function getMessagesBySentAt(sentAt, { MessageCollection }) {
|
||||
const messages = new MessageCollection();
|
||||
|
||||
await deferredToPromise(
|
||||
messages.fetch({
|
||||
index: {
|
||||
// 'receipt' index on sent_at
|
||||
name: 'receipt',
|
||||
only: sentAt,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function getExpiredMessages({ MessageCollection }) {
|
||||
window.log.info('Load expired messages');
|
||||
const messages = new MessageCollection();
|
||||
|
||||
await deferredToPromise(
|
||||
messages.fetch({
|
||||
conditions: {
|
||||
expires_at: {
|
||||
$lte: Date.now(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function getNextExpiringMessage({ MessageCollection }) {
|
||||
const messages = new MessageCollection();
|
||||
|
||||
await deferredToPromise(
|
||||
messages.fetch({
|
||||
limit: 1,
|
||||
index: {
|
||||
name: 'expires_at',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function saveUnprocessed(data, { Unprocessed }) {
|
||||
const unprocessed = new Unprocessed(data);
|
||||
return deferredToPromise(unprocessed.save());
|
||||
}
|
||||
|
||||
async function getAllUnprocessed({ UnprocessedCollection }) {
|
||||
const collection = new UnprocessedCollection();
|
||||
await deferredToPromise(collection.fetch());
|
||||
return collection.map(model => model.attributes);
|
||||
}
|
||||
|
||||
async function updateUnprocessed(id, updates, { Unprocessed }) {
|
||||
const unprocessed = new Unprocessed({
|
||||
id,
|
||||
});
|
||||
|
||||
await deferredToPromise(unprocessed.fetch());
|
||||
|
||||
unprocessed.set(updates);
|
||||
await saveUnprocessed(unprocessed.attributes, { Unprocessed });
|
||||
}
|
||||
|
||||
async function removeUnprocessed(id, { Unprocessed }) {
|
||||
const unprocessed = new Unprocessed({
|
||||
id,
|
||||
});
|
||||
|
||||
await deferredToPromise(unprocessed.destroy());
|
||||
}
|
||||
|
||||
async function removeAllUnprocessed() {
|
||||
// erase everything in unprocessed table
|
||||
}
|
||||
|
||||
async function removeAll() {
|
||||
// erase everything in the database
|
||||
}
|
||||
|
||||
async function getMessagesNeedingUpgrade(limit, { MessageCollection }) {
|
||||
const messages = new MessageCollection();
|
||||
|
||||
await deferredToPromise(
|
||||
messages.fetch({
|
||||
limit,
|
||||
index: {
|
||||
name: 'schemaVersion',
|
||||
upper: MessageType.CURRENT_SCHEMA_VERSION,
|
||||
excludeUpper: true,
|
||||
order: 'desc',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const models = messages.models || [];
|
||||
return models.map(model => model.toJSON());
|
||||
}
|
||||
|
||||
async function getMessagesWithVisualMediaAttachments(
|
||||
conversationId,
|
||||
{ limit, MessageCollection }
|
||||
) {
|
||||
const messages = new MessageCollection();
|
||||
const lowerReceivedAt = 0;
|
||||
const upperReceivedAt = Number.MAX_VALUE;
|
||||
|
||||
await deferredToPromise(
|
||||
messages.fetch({
|
||||
limit,
|
||||
index: {
|
||||
name: 'hasVisualMediaAttachments',
|
||||
lower: [conversationId, lowerReceivedAt, 1],
|
||||
upper: [conversationId, upperReceivedAt, 1],
|
||||
order: 'desc',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return messages.models.map(model => model.toJSON());
|
||||
}
|
||||
|
||||
async function getMessagesWithFileAttachments(
|
||||
conversationId,
|
||||
{ limit, MessageCollection }
|
||||
) {
|
||||
const messages = new MessageCollection();
|
||||
const lowerReceivedAt = 0;
|
||||
const upperReceivedAt = Number.MAX_VALUE;
|
||||
|
||||
await deferredToPromise(
|
||||
messages.fetch({
|
||||
limit,
|
||||
index: {
|
||||
name: 'hasFileAttachments',
|
||||
lower: [conversationId, lowerReceivedAt, 1],
|
||||
upper: [conversationId, upperReceivedAt, 1],
|
||||
order: 'desc',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return messages.models.map(model => model.toJSON());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveMessage,
|
||||
removeMessage,
|
||||
getUnreadByConversation,
|
||||
removeAllMessagesInConversation,
|
||||
getMessageBySender,
|
||||
getMessageById,
|
||||
getAllMessageIds,
|
||||
getMessagesBySentAt,
|
||||
getExpiredMessages,
|
||||
getNextExpiringMessage,
|
||||
getMessagesByConversation,
|
||||
|
||||
getAllUnprocessed,
|
||||
saveUnprocessed,
|
||||
updateUnprocessed,
|
||||
removeUnprocessed,
|
||||
removeAllUnprocessed,
|
||||
|
||||
removeAll,
|
||||
|
||||
// Returning plain JSON
|
||||
getMessagesNeedingUpgrade,
|
||||
getMessagesWithVisualMediaAttachments,
|
||||
getMessagesWithFileAttachments,
|
||||
};
|
@ -1,76 +0,0 @@
|
||||
import is from '@sindresorhus/is';
|
||||
|
||||
import { Collection as BackboneCollection } from '../types/backbone/Collection';
|
||||
import { deferredToPromise } from '../../js/modules/deferred_to_promise';
|
||||
import { IndexableBoolean } from '../types/IndexedDB';
|
||||
import { Message } from '../types/Message';
|
||||
|
||||
export const fetchVisualMediaAttachments = async ({
|
||||
conversationId,
|
||||
count,
|
||||
WhisperMessageCollection,
|
||||
}: {
|
||||
conversationId: string;
|
||||
count: number;
|
||||
WhisperMessageCollection: BackboneCollection<Message>;
|
||||
}): Promise<Array<Message>> =>
|
||||
fetchFromAttachmentsIndex({
|
||||
name: 'hasVisualMediaAttachments',
|
||||
conversationId,
|
||||
WhisperMessageCollection,
|
||||
count,
|
||||
});
|
||||
|
||||
export const fetchFileAttachments = async ({
|
||||
conversationId,
|
||||
count,
|
||||
WhisperMessageCollection,
|
||||
}: {
|
||||
conversationId: string;
|
||||
count: number;
|
||||
WhisperMessageCollection: BackboneCollection<Message>;
|
||||
}): Promise<Array<Message>> =>
|
||||
fetchFromAttachmentsIndex({
|
||||
name: 'hasFileAttachments',
|
||||
conversationId,
|
||||
WhisperMessageCollection,
|
||||
count,
|
||||
});
|
||||
|
||||
const fetchFromAttachmentsIndex = async ({
|
||||
name,
|
||||
conversationId,
|
||||
WhisperMessageCollection,
|
||||
count,
|
||||
}: {
|
||||
name: 'hasVisualMediaAttachments' | 'hasFileAttachments';
|
||||
conversationId: string;
|
||||
WhisperMessageCollection: BackboneCollection<Message>;
|
||||
count: number;
|
||||
}): Promise<Array<Message>> => {
|
||||
if (!is.string(conversationId)) {
|
||||
throw new TypeError("'conversationId' is required");
|
||||
}
|
||||
|
||||
if (!is.object(WhisperMessageCollection)) {
|
||||
throw new TypeError("'WhisperMessageCollection' is required");
|
||||
}
|
||||
|
||||
const collection = new WhisperMessageCollection();
|
||||
const lowerReceivedAt = 0;
|
||||
const upperReceivedAt = Number.MAX_VALUE;
|
||||
const condition: IndexableBoolean = 1;
|
||||
await deferredToPromise(
|
||||
collection.fetch({
|
||||
index: {
|
||||
name,
|
||||
lower: [conversationId, lowerReceivedAt, condition],
|
||||
upper: [conversationId, upperReceivedAt, condition],
|
||||
order: 'desc',
|
||||
},
|
||||
limit: count,
|
||||
})
|
||||
);
|
||||
|
||||
return collection.models.map(model => model.toJSON());
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
import * as Conversation from './Conversation';
|
||||
import * as Views from './views';
|
||||
|
||||
export { Conversation, Views };
|
||||
export { Views };
|
||||
|
Loading…
Reference in New Issue