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:
parent
c46e1a1519
commit
9d84b2f420
7 changed files with 42 additions and 26 deletions
|
@ -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 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
12
ts/types/IndexedDB.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
|
||||||
|
// IndexedDB doesn’t 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;
|
|
@ -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;
|
||||||
}>;
|
}>>;
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue