Index messages with attachments using a boolean

When indexing message attachment metadata using numeric indexes such as:

```javascript
{
  conversationId: '+12223334455',
  received_at: 123,
  attachments: […],
  numAttachments: 2,
},
{
  conversationId: '+12223334455',
  received_at: 456,
  attachments: [],
  numAttachments: 0,
}
{
  conversationId: '+12223334455',
  received_at: 789,
  attachments: [],
  numAttachments: 1,
}
```

It creates an index as follows:

```
[conversationId, received_at, numAttachments]
['+12223334455', 123, 2]
['+12223334455', 456, 0]
['+12223334455', 789, 1]
```

This means a query such as…

```
lowerBound: ['+12223334455', 0,                1               ]
upperBound: ['+12223334455', Number.MAX_VALUE, Number.MAX_VALUE]
```

…will return all three original entries because they span the `received_at`
from `0` through `Number.MAX_VALUE`. One workaround is to index booleans using
`1 | undefined` where `1` is included in the index and `undefined` is not, but
that way we lose the ability to query for the `false` value. Instead, we flip
adjust the index to `[conversationId, hasAttachments, received_at]` and can
then query messages with attachments using

```
[conversationId, 1 /* hasAttachments */, 0                /* received_at */]
[conversationId, 1 /* hasAttachments */, Number.MAX_VALUE /* received_at */]
```
This commit is contained in:
Daniel Gasienica 2018-04-13 21:47:06 -04:00
parent c46e1a1519
commit 9d84b2f420
7 changed files with 42 additions and 26 deletions

View file

@ -2,14 +2,14 @@ exports.run = (transaction) => {
const messagesStore = transaction.objectStore('messages'); const messagesStore = transaction.objectStore('messages');
[ [
'numAttachments', 'hasAttachments',
'numVisualMediaAttachments', 'hasVisualMediaAttachments',
'numFileAttachments', 'hasFileAttachments',
].forEach((name) => { ].forEach((name) => {
console.log(`Create message attachment metadata index: '${name}'`); console.log(`Create message attachment metadata index: '${name}'`);
messagesStore.createIndex( messagesStore.createIndex(
name, name,
['conversationId', 'received_at', name], ['conversationId', name, 'received_at'],
{ unique: false } { unique: false }
); );
}); });

View file

@ -24,9 +24,9 @@ const PRIVATE = 'private';
// - Quotes: Write thumbnail data to disk and store relative path to it. // - Quotes: Write thumbnail data to disk and store relative path to it.
// Version 5 // Version 5
// - Attachments: Track number and kind of attachments for media gallery // - Attachments: Track number and kind of attachments for media gallery
// - `numAttachments: Number` // - `hasAttachments?: 1 | undefined`
// - `numVisualMediaAttachments: Number` (for media gallery Media view) // - `hasVisualMediaAttachments?: 1 | undefined` (for media gallery Media view)
// - `numFileAttachments: Number` (for media gallery Documents view) // - `hasFileAttachments: ?1 | undefined` (for media gallery Documents view)
const INITIAL_SCHEMA_VERSION = 0; const INITIAL_SCHEMA_VERSION = 0;

View file

@ -181,9 +181,9 @@ describe('Message', () => {
fileName: 'test\uFFFDfig.exe', fileName: 'test\uFFFDfig.exe',
size: 1111, size: 1111,
}], }],
numAttachments: 1, hasAttachments: 1,
numVisualMediaAttachments: 0, hasVisualMediaAttachments: undefined,
numFileAttachments: 1, hasFileAttachments: 1,
schemaVersion: Message.CURRENT_SCHEMA_VERSION, schemaVersion: Message.CURRENT_SCHEMA_VERSION,
}; };

View file

@ -38,9 +38,9 @@ describe('Message', () => {
fileName: 'foo.jpg', fileName: 'foo.jpg',
size: 1111, size: 1111,
}], }],
numAttachments: 1, hasAttachments: 1,
numVisualMediaAttachments: 1, hasVisualMediaAttachments: 1,
numFileAttachments: 0, hasFileAttachments: undefined,
}; };
const actual = await Message.initializeAttachmentMetadata(input); const actual = await Message.initializeAttachmentMetadata(input);

12
ts/types/IndexedDB.ts Normal file
View file

@ -0,0 +1,12 @@
/**
* @prettier
*/
// IndexedDB doesnt support boolean indexes so we map `true` to 1 and `false`
// to `0`.
// N.B. Using `undefined` allows excluding an entry from an index. Useful
// when index size is a consideration or one only needs to query for `true`.
export type IndexableBoolean = 1 | 0;
export const toIndexableBoolean = (value: boolean): IndexableBoolean =>
value ? 1 : 0;

View file

@ -2,6 +2,7 @@
* @prettier * @prettier
*/ */
import { Attachment } from './Attachment'; import { Attachment } from './Attachment';
import { IndexableBoolean } from './IndexedDB';
export type Message = IncomingMessage | OutgoingMessage | VerifiedChangeMessage; export type Message = IncomingMessage | OutgoingMessage | VerifiedChangeMessage;
@ -73,8 +74,8 @@ type ExpirationTimerUpdate = Readonly<{
}>; }>;
}>; }>;
type Message4 = Readonly<{ type Message4 = Partial<Readonly<{
numAttachments?: number; hasAttachments: IndexableBoolean;
numVisualMediaAttachments?: number; hasVisualMediaAttachments: IndexableBoolean;
numFileAttachments?: number; hasFileAttachments: IndexableBoolean;
}>; }>>;

View file

@ -4,25 +4,28 @@
import { partition } from 'lodash'; import { partition } from 'lodash';
import * as Attachment from '../Attachment'; import * as Attachment from '../Attachment';
import * as IndexedDB from '../IndexedDB';
import { Message } from '../Message'; import { Message } from '../Message';
export const initializeAttachmentMetadata = async ( export const initializeAttachmentMetadata = async (
message: Message message: Message,
): Promise<Message> => { ): Promise<Message> => {
if (message.type === 'verified-change') { if (message.type === 'verified-change') {
return message; return message;
} }
const numAttachments = message.attachments.length; const hasAttachments = IndexedDB.toIndexableBoolean(message.attachments.length > 0);
const [numVisualMediaAttachments, numFileAttachments] = partition( const [hasVisualMediaAttachments, hasFileAttachments] = partition(
message.attachments, message.attachments,
Attachment.isVisualMedia Attachment.isVisualMedia,
).map(attachments => attachments.length); )
.map((attachments) => attachments.length > 0)
.map(IndexedDB.toIndexableBoolean);
return { return {
...message, ...message,
numAttachments, hasAttachments,
numVisualMediaAttachments, hasVisualMediaAttachments,
numFileAttachments, hasFileAttachments,
}; };
}; };