diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 48e35177340d..8d4b87da6345 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -498,12 +498,18 @@ function migrateSchemaVersion(db: Database): void { setUserVersion(db, newUserVersion); } -function openAndMigrateDatabase(filePath: string, key: string) { +function openAndMigrateDatabase( + filePath: string, + key: string, + readonly: boolean +) { let db: Database | undefined; // First, we try to open the database without any cipher changes try { - db = new SQL(filePath); + db = new SQL(filePath, { + readonly, + }); keyDatabase(db, key); switchToWAL(db); migrateSchemaVersion(db); @@ -538,13 +544,16 @@ function openAndMigrateDatabase(filePath: string, key: string) { } const INVALID_KEY = /[^0-9A-Fa-f]/; -function openAndSetUpSQLCipher(filePath: string, { key }: { key: string }) { +function openAndSetUpSQLCipher( + filePath: string, + { key, readonly }: { key: string; readonly: boolean } +) { const match = INVALID_KEY.exec(key); if (match) { throw new Error(`setupSQLCipher: key '${key}' is not valid`); } - const db = openAndMigrateDatabase(filePath, key); + const db = openAndMigrateDatabase(filePath, key, readonly); // Because foreign key support is not enabled by default! db.pragma('foreign_keys = ON'); @@ -552,7 +561,8 @@ function openAndSetUpSQLCipher(filePath: string, { key }: { key: string }) { return db; } -let globalInstance: Database | undefined; +let globalWritableInstance: Database | undefined; +let globalReadonlyInstance: Database | undefined; let logger = consoleLogger; let databaseFilePath: string | undefined; let indexedDBPath: string | undefined; @@ -570,7 +580,7 @@ async function initialize({ key: string; logger: LoggerType; }): Promise { - if (globalInstance) { + if (globalWritableInstance || globalReadonlyInstance) { throw new Error('Cannot initialize more than once!'); } @@ -590,26 +600,32 @@ async function initialize({ databaseFilePath = join(dbDir, 'db.sqlite'); - let db: Database | undefined; + let writable: Database | undefined; + let readonly: Database | undefined; try { - db = openAndSetUpSQLCipher(databaseFilePath, { key }); + writable = openAndSetUpSQLCipher(databaseFilePath, { + key, + readonly: false, + }); // For profiling use: // db.pragma('cipher_profile=\'sqlcipher.log\''); - updateSchema(db, logger); + updateSchema(writable, logger); + + readonly = openAndSetUpSQLCipher(databaseFilePath, { key, readonly: true }); // At this point we can allow general access to the database - globalInstance = db; + globalWritableInstance = writable; + globalReadonlyInstance = readonly; // test database getMessageCountSync(); } catch (error) { logger.error('Database startup error:', error.stack); - if (db) { - db.close(); - } + readonly?.close(); + writable?.close(); throw error; } } @@ -617,20 +633,30 @@ async function initialize({ async function close(): Promise { // SQLLite documentation suggests that we run `PRAGMA optimize` right // before closing the database connection. - globalInstance?.pragma('optimize'); + globalWritableInstance?.pragma('optimize'); - globalInstance?.close(); - globalInstance = undefined; + globalWritableInstance?.close(); + globalWritableInstance = undefined; + globalReadonlyInstance?.close(); + globalReadonlyInstance = undefined; } async function removeDB(): Promise { - if (globalInstance) { + if (globalWritableInstance) { try { - globalInstance.close(); + globalWritableInstance.close(); } catch (error) { logger.error('removeDB: Failed to close database:', error.stack); } - globalInstance = undefined; + globalWritableInstance = undefined; + } + if (globalReadonlyInstance) { + try { + globalReadonlyInstance.close(); + } catch (error) { + logger.error('removeDB: Failed to close readonly database:', error.stack); + } + globalReadonlyInstance = undefined; } if (!databaseFilePath) { throw new Error( @@ -656,65 +682,77 @@ async function removeIndexedDBFiles(): Promise { indexedDBPath = undefined; } -function getInstance(): Database { - if (!globalInstance) { - throw new Error('getInstance: globalInstance not set!'); +function getReadonlyInstance(): Database { + if (!globalReadonlyInstance) { + throw new Error('getReadonlyInstance: globalReadonlyInstance not set!'); } - return globalInstance; + return globalReadonlyInstance; +} + +async function getWritableInstance(): Promise { + if (!globalWritableInstance) { + throw new Error('getWritableInstance: globalWritableInstance not set!'); + } + + return globalWritableInstance; } const IDENTITY_KEYS_TABLE = 'identityKeys'; async function createOrUpdateIdentityKey( data: StoredIdentityKeyType ): Promise { - return createOrUpdate(getInstance(), IDENTITY_KEYS_TABLE, data); + return createOrUpdate(await getWritableInstance(), IDENTITY_KEYS_TABLE, data); } async function getIdentityKeyById( id: IdentityKeyIdType ): Promise { - return getById(getInstance(), IDENTITY_KEYS_TABLE, id); + return getById(getReadonlyInstance(), IDENTITY_KEYS_TABLE, id); } async function bulkAddIdentityKeys( array: Array ): Promise { - return bulkAdd(getInstance(), IDENTITY_KEYS_TABLE, array); + return bulkAdd(await getWritableInstance(), IDENTITY_KEYS_TABLE, array); } async function removeIdentityKeyById(id: IdentityKeyIdType): Promise { - return removeById(getInstance(), IDENTITY_KEYS_TABLE, id); + return removeById(await getWritableInstance(), IDENTITY_KEYS_TABLE, id); } async function removeAllIdentityKeys(): Promise { - return removeAllFromTable(getInstance(), IDENTITY_KEYS_TABLE); + return removeAllFromTable(await getWritableInstance(), IDENTITY_KEYS_TABLE); } async function getAllIdentityKeys(): Promise> { - return getAllFromTable(getInstance(), IDENTITY_KEYS_TABLE); + return getAllFromTable(getReadonlyInstance(), IDENTITY_KEYS_TABLE); } const KYBER_PRE_KEYS_TABLE = 'kyberPreKeys'; async function createOrUpdateKyberPreKey( data: StoredKyberPreKeyType ): Promise { - return createOrUpdate(getInstance(), KYBER_PRE_KEYS_TABLE, data); + return createOrUpdate( + await getWritableInstance(), + KYBER_PRE_KEYS_TABLE, + data + ); } async function getKyberPreKeyById( id: PreKeyIdType ): Promise { - return getById(getInstance(), KYBER_PRE_KEYS_TABLE, id); + return getById(getReadonlyInstance(), KYBER_PRE_KEYS_TABLE, id); } async function bulkAddKyberPreKeys( array: Array ): Promise { - return bulkAdd(getInstance(), KYBER_PRE_KEYS_TABLE, array); + return bulkAdd(await getWritableInstance(), KYBER_PRE_KEYS_TABLE, array); } async function removeKyberPreKeyById( id: PreKeyIdType | Array ): Promise { - return removeById(getInstance(), KYBER_PRE_KEYS_TABLE, id); + return removeById(await getWritableInstance(), KYBER_PRE_KEYS_TABLE, id); } async function removeKyberPreKeysByServiceId( serviceId: ServiceIdString ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( 'DELETE FROM kyberPreKeys WHERE ourServiceId IS $serviceId;' ).run({ @@ -722,33 +760,33 @@ async function removeKyberPreKeysByServiceId( }); } async function removeAllKyberPreKeys(): Promise { - return removeAllFromTable(getInstance(), KYBER_PRE_KEYS_TABLE); + return removeAllFromTable(await getWritableInstance(), KYBER_PRE_KEYS_TABLE); } async function getAllKyberPreKeys(): Promise> { - return getAllFromTable(getInstance(), KYBER_PRE_KEYS_TABLE); + return getAllFromTable(getReadonlyInstance(), KYBER_PRE_KEYS_TABLE); } const PRE_KEYS_TABLE = 'preKeys'; async function createOrUpdatePreKey(data: StoredPreKeyType): Promise { - return createOrUpdate(getInstance(), PRE_KEYS_TABLE, data); + return createOrUpdate(await getWritableInstance(), PRE_KEYS_TABLE, data); } async function getPreKeyById( id: PreKeyIdType ): Promise { - return getById(getInstance(), PRE_KEYS_TABLE, id); + return getById(getReadonlyInstance(), PRE_KEYS_TABLE, id); } async function bulkAddPreKeys(array: Array): Promise { - return bulkAdd(getInstance(), PRE_KEYS_TABLE, array); + return bulkAdd(await getWritableInstance(), PRE_KEYS_TABLE, array); } async function removePreKeyById( id: PreKeyIdType | Array ): Promise { - return removeById(getInstance(), PRE_KEYS_TABLE, id); + return removeById(await getWritableInstance(), PRE_KEYS_TABLE, id); } async function removePreKeysByServiceId( serviceId: ServiceIdString ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( 'DELETE FROM preKeys WHERE ourServiceId IS $serviceId;' ).run({ @@ -756,37 +794,41 @@ async function removePreKeysByServiceId( }); } async function removeAllPreKeys(): Promise { - return removeAllFromTable(getInstance(), PRE_KEYS_TABLE); + return removeAllFromTable(await getWritableInstance(), PRE_KEYS_TABLE); } async function getAllPreKeys(): Promise> { - return getAllFromTable(getInstance(), PRE_KEYS_TABLE); + return getAllFromTable(getReadonlyInstance(), PRE_KEYS_TABLE); } const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys'; async function createOrUpdateSignedPreKey( data: StoredSignedPreKeyType ): Promise { - return createOrUpdate(getInstance(), SIGNED_PRE_KEYS_TABLE, data); + return createOrUpdate( + await getWritableInstance(), + SIGNED_PRE_KEYS_TABLE, + data + ); } async function getSignedPreKeyById( id: SignedPreKeyIdType ): Promise { - return getById(getInstance(), SIGNED_PRE_KEYS_TABLE, id); + return getById(getReadonlyInstance(), SIGNED_PRE_KEYS_TABLE, id); } async function bulkAddSignedPreKeys( array: Array ): Promise { - return bulkAdd(getInstance(), SIGNED_PRE_KEYS_TABLE, array); + return bulkAdd(await getWritableInstance(), SIGNED_PRE_KEYS_TABLE, array); } async function removeSignedPreKeyById( id: SignedPreKeyIdType | Array ): Promise { - return removeById(getInstance(), SIGNED_PRE_KEYS_TABLE, id); + return removeById(await getWritableInstance(), SIGNED_PRE_KEYS_TABLE, id); } async function removeSignedPreKeysByServiceId( serviceId: ServiceIdString ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( 'DELETE FROM signedPreKeys WHERE ourServiceId IS $serviceId;' ).run({ @@ -794,10 +836,10 @@ async function removeSignedPreKeysByServiceId( }); } async function removeAllSignedPreKeys(): Promise { - return removeAllFromTable(getInstance(), SIGNED_PRE_KEYS_TABLE); + return removeAllFromTable(await getWritableInstance(), SIGNED_PRE_KEYS_TABLE); } async function getAllSignedPreKeys(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: JSONRows = db .prepare( ` @@ -815,15 +857,15 @@ const ITEMS_TABLE = 'items'; async function createOrUpdateItem( data: StoredItemType ): Promise { - return createOrUpdate(getInstance(), ITEMS_TABLE, data); + return createOrUpdate(await getWritableInstance(), ITEMS_TABLE, data); } async function getItemById( id: K ): Promise | undefined> { - return getById(getInstance(), ITEMS_TABLE, id); + return getById(getReadonlyInstance(), ITEMS_TABLE, id); } async function getAllItems(): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: JSONRows = db .prepare('SELECT json FROM items ORDER BY id ASC;') .all(); @@ -843,19 +885,18 @@ async function getAllItems(): Promise { async function removeItemById( id: ItemKeyType | Array ): Promise { - return removeById(getInstance(), ITEMS_TABLE, id); + return removeById(await getWritableInstance(), ITEMS_TABLE, id); } async function removeAllItems(): Promise { - return removeAllFromTable(getInstance(), ITEMS_TABLE); + return removeAllFromTable(await getWritableInstance(), ITEMS_TABLE); } async function createOrUpdateSenderKey(key: SenderKeyType): Promise { - createOrUpdateSenderKeySync(key); + const db = await getWritableInstance(); + createOrUpdateSenderKeySync(db, key); } -function createOrUpdateSenderKeySync(key: SenderKeyType): void { - const db = getInstance(); - +function createOrUpdateSenderKeySync(db: Database, key: SenderKeyType): void { prepare( db, ` @@ -878,7 +919,7 @@ function createOrUpdateSenderKeySync(key: SenderKeyType): void { async function getSenderKeyById( id: SenderKeyIdType ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const row = prepare(db, 'SELECT * FROM senderKeys WHERE id = $id').get({ id, }); @@ -886,17 +927,17 @@ async function getSenderKeyById( return row; } async function removeAllSenderKeys(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); prepare(db, 'DELETE FROM senderKeys').run(); } async function getAllSenderKeys(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = prepare(db, 'SELECT * FROM senderKeys').all(); return rows; } async function removeSenderKeyById(id: SenderKeyIdType): Promise { - const db = getInstance(); + const db = await getWritableInstance(); prepare(db, 'DELETE FROM senderKeys WHERE id = $id').run({ id }); } @@ -907,7 +948,7 @@ async function insertSentProto( messageIds: SentMessagesType; } ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const { recipients, messageIds } = options; // Note: we use `pluck` in this function to fetch only the first column of returned row. @@ -1000,7 +1041,7 @@ async function insertSentProto( } async function deleteSentProtosOlderThan(timestamp: number): Promise { - const db = getInstance(); + const db = await getWritableInstance(); prepare( db, @@ -1016,7 +1057,7 @@ async function deleteSentProtosOlderThan(timestamp: number): Promise { } async function deleteSentProtoByMessageId(messageId: string): Promise { - const db = getInstance(); + const db = await getWritableInstance(); prepare( db, @@ -1040,7 +1081,7 @@ async function insertProtoRecipients({ recipientServiceId: ServiceIdString; deviceIds: Array; }): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { const statement = prepare( @@ -1073,7 +1114,7 @@ async function deleteSentProtoRecipient( | DeleteSentProtoRecipientOptionsType | ReadonlyArray ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const items = Array.isArray(options) ? options : [options]; @@ -1190,13 +1231,13 @@ async function getSentProtoByRecipient({ recipientServiceId: ServiceIdString; timestamp: number; }): Promise { - const db = getInstance(); - const HOUR = 1000 * 60 * 60; const oneDayAgo = now - HOUR * 24; await deleteSentProtosOlderThan(oneDayAgo); + const db = getReadonlyInstance(); + const row = prepare( db, ` @@ -1231,11 +1272,11 @@ async function getSentProtoByRecipient({ }; } async function removeAllSentProtos(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); prepare(db, 'DELETE FROM sendLogPayloads;').run(); } async function getAllSentProtos(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = prepare(db, 'SELECT * FROM sendLogPayloads;').all(); return rows.map(row => ({ @@ -1249,7 +1290,7 @@ async function getAllSentProtos(): Promise> { async function _getAllSentProtoRecipients(): Promise< Array > { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = prepare( db, 'SELECT * FROM sendLogRecipients;' @@ -1258,7 +1299,7 @@ async function _getAllSentProtoRecipients(): Promise< return rows; } async function _getAllSentProtoMessageIds(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = prepare( db, 'SELECT * FROM sendLogMessageIds;' @@ -1268,8 +1309,7 @@ async function _getAllSentProtoMessageIds(): Promise> { } const SESSIONS_TABLE = 'sessions'; -function createOrUpdateSessionSync(data: SessionType): void { - const db = getInstance(); +function createOrUpdateSessionSync(db: Database, data: SessionType): void { const { id, conversationId, ourServiceId, serviceId } = data; if (!id) { throw new Error( @@ -1308,17 +1348,18 @@ function createOrUpdateSessionSync(data: SessionType): void { }); } async function createOrUpdateSession(data: SessionType): Promise { - return createOrUpdateSessionSync(data); + const db = await getWritableInstance(); + return createOrUpdateSessionSync(db, data); } async function createOrUpdateSessions( array: Array ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { for (const item of array) { - assertSync(createOrUpdateSessionSync(item)); + assertSync(createOrUpdateSessionSync(db, item)); } })(); } @@ -1332,33 +1373,33 @@ async function commitDecryptResult({ sessions: Array; unprocessed: Array; }): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { for (const item of senderKeys) { - assertSync(createOrUpdateSenderKeySync(item)); + assertSync(createOrUpdateSenderKeySync(db, item)); } for (const item of sessions) { - assertSync(createOrUpdateSessionSync(item)); + assertSync(createOrUpdateSessionSync(db, item)); } for (const item of unprocessed) { - assertSync(saveUnprocessedSync(item)); + assertSync(saveUnprocessedSync(db, item)); } })(); } async function bulkAddSessions(array: Array): Promise { - return bulkAdd(getInstance(), SESSIONS_TABLE, array); + return bulkAdd(await getWritableInstance(), SESSIONS_TABLE, array); } async function removeSessionById(id: SessionIdType): Promise { - return removeById(getInstance(), SESSIONS_TABLE, id); + return removeById(await getWritableInstance(), SESSIONS_TABLE, id); } async function removeSessionsByConversation( conversationId: string ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` DELETE FROM sessions @@ -1371,7 +1412,7 @@ async function removeSessionsByConversation( async function removeSessionsByServiceId( serviceId: ServiceIdString ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` DELETE FROM sessions @@ -1382,15 +1423,15 @@ async function removeSessionsByServiceId( }); } async function removeAllSessions(): Promise { - return removeAllFromTable(getInstance(), SESSIONS_TABLE); + return removeAllFromTable(await getWritableInstance(), SESSIONS_TABLE); } async function getAllSessions(): Promise> { - return getAllFromTable(getInstance(), SESSIONS_TABLE); + return getAllFromTable(getReadonlyInstance(), SESSIONS_TABLE); } // Conversations async function getConversationCount(): Promise { - return getCountFromTable(getInstance(), 'conversations'); + return getCountFromTable(getReadonlyInstance(), 'conversations'); } function getConversationMembersList({ members, membersV2 }: ConversationType) { @@ -1403,10 +1444,7 @@ function getConversationMembersList({ members, membersV2 }: ConversationType) { return null; } -function saveConversationSync( - data: ConversationType, - db = getInstance() -): void { +function saveConversationSync(db: Database, data: ConversationType): void { const { active_at, e164, @@ -1479,29 +1517,24 @@ function saveConversationSync( }); } -async function saveConversation( - data: ConversationType, - db = getInstance() -): Promise { - return saveConversationSync(data, db); +async function saveConversation(data: ConversationType): Promise { + const db = await getWritableInstance(); + return saveConversationSync(db, data); } async function saveConversations( arrayOfConversations: Array ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { for (const conversation of arrayOfConversations) { - assertSync(saveConversationSync(conversation)); + assertSync(saveConversationSync(db, conversation)); } })(); } -function updateConversationSync( - data: ConversationType, - db = getInstance() -): void { +function updateConversationSync(db: Database, data: ConversationType): void { const { id, active_at, @@ -1555,24 +1588,26 @@ function updateConversationSync( } async function updateConversation(data: ConversationType): Promise { - return updateConversationSync(data); + const db = await getWritableInstance(); + return updateConversationSync(db, data); } async function updateConversations( array: Array ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { for (const item of array) { - assertSync(updateConversationSync(item)); + assertSync(updateConversationSync(db, item)); } })(); } -function removeConversationsSync(ids: ReadonlyArray): void { - const db = getInstance(); - +function removeConversationsSync( + db: Database, + ids: ReadonlyArray +): void { // Our node interface doesn't seem to allow you to replace one single ? with an array db.prepare( ` @@ -1583,7 +1618,7 @@ function removeConversationsSync(ids: ReadonlyArray): void { } async function removeConversation(id: Array | string): Promise { - const db = getInstance(); + const db = await getWritableInstance(); if (!Array.isArray(id)) { db.prepare('DELETE FROM conversations WHERE id = $id;').run({ @@ -1597,18 +1632,18 @@ async function removeConversation(id: Array | string): Promise { throw new Error('removeConversation: No ids to delete!'); } - batchMultiVarQuery(db, id, removeConversationsSync); + batchMultiVarQuery(db, id, ids => removeConversationsSync(db, ids)); } async function _removeAllConversations(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare('DELETE from conversations;').run(); } async function getConversationById( id: string ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const row: { json: string } = db .prepare('SELECT json FROM conversations WHERE id = $id;') .get({ id }); @@ -1620,7 +1655,9 @@ async function getConversationById( return jsonToObject(row.json); } -function getAllConversationsSync(db = getInstance()): Array { +function getAllConversationsSync( + db = getReadonlyInstance() +): Array { const rows: ConversationRows = db .prepare( ` @@ -1639,7 +1676,7 @@ async function getAllConversations(): Promise> { } async function getAllConversationIds(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: Array<{ id: string }> = db .prepare( ` @@ -1654,7 +1691,7 @@ async function getAllConversationIds(): Promise> { async function getAllGroupsInvolvingServiceId( serviceId: ServiceIdString ): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: ConversationRows = db .prepare( ` @@ -1685,7 +1722,9 @@ async function searchMessages({ }): Promise> { const { limit = conversationId ? 100 : 500 } = options ?? {}; - const db = getInstance(); + // We don't actually write to the database, but temporary tables below + // require write access. + const db = await getWritableInstance(); // sqlite queries with a join on a virtual table (like FTS5) are de-optimized // and can't use indices for ordering results. Instead an in-memory index of @@ -1831,7 +1870,7 @@ async function searchMessages({ function getMessageCountSync( conversationId?: string, - db = getInstance() + db = getReadonlyInstance() ): number { if (conversationId === undefined) { return getCountFromTable(db, 'messages'); @@ -1852,7 +1891,7 @@ function getMessageCountSync( } async function getStoryCount(conversationId: string): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); return db .prepare( ` @@ -1872,7 +1911,7 @@ async function getMessageCount(conversationId?: string): Promise { // Note: we really only use this in 1:1 conversations, where story replies are always // shown, so this has no need to be story-aware. function hasUserInitiatedMessages(conversationId: string): boolean { - const db = getInstance(); + const db = getReadonlyInstance(); const exists: number = db .prepare( @@ -1893,27 +1932,21 @@ function hasUserInitiatedMessages(conversationId: string): boolean { } function saveMessageSync( + db: Database, data: MessageType, options: { alreadyInTransaction?: boolean; - db?: Database; forceSave?: boolean; jobToInsert?: StoredJob; ourAci: AciString; } ): string { - const { - alreadyInTransaction, - db = getInstance(), - forceSave, - jobToInsert, - ourAci, - } = options; + const { alreadyInTransaction, forceSave, jobToInsert, ourAci } = options; if (!alreadyInTransaction) { return db.transaction(() => { return assertSync( - saveMessageSync(data, { + saveMessageSync(db, data, { ...options, alreadyInTransaction: true, }) @@ -2127,33 +2160,32 @@ async function saveMessage( ourAci: AciString; } ): Promise { - return saveMessageSync(data, options); + const db = await getWritableInstance(); + return saveMessageSync(db, data, options); } async function saveMessages( arrayOfMessages: ReadonlyArray, options: { forceSave?: boolean; ourAci: AciString } ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { for (const message of arrayOfMessages) { assertSync( - saveMessageSync(message, { ...options, alreadyInTransaction: true }) + saveMessageSync(db, message, { ...options, alreadyInTransaction: true }) ); } })(); } async function removeMessage(id: string): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare('DELETE FROM messages WHERE id = $id;').run({ id }); } -function removeMessagesSync(ids: ReadonlyArray): void { - const db = getInstance(); - +function removeMessagesSync(db: Database, ids: ReadonlyArray): void { db.prepare( ` DELETE FROM messages @@ -2163,11 +2195,12 @@ function removeMessagesSync(ids: ReadonlyArray): void { } async function removeMessages(ids: ReadonlyArray): Promise { - batchMultiVarQuery(getInstance(), ids, removeMessagesSync); + const db = await getWritableInstance(); + batchMultiVarQuery(db, ids, batch => removeMessagesSync(db, batch)); } async function getMessageById(id: string): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); return getMessageByIdSync(db, id); } @@ -2191,7 +2224,7 @@ export function getMessageByIdSync( async function getMessagesById( messageIds: ReadonlyArray ): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); return batchMultiVarQuery( db, @@ -2209,7 +2242,7 @@ async function getMessagesById( } async function _getAllMessages(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: JSONRows = db .prepare('SELECT json FROM messages ORDER BY id ASC;') .all(); @@ -2217,14 +2250,14 @@ async function _getAllMessages(): Promise> { return rows.map(row => jsonToObject(row.json)); } async function _removeAllMessages(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.exec(` DELETE FROM messages; `); } async function getAllMessageIds(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: Array<{ id: string }> = db .prepare('SELECT id FROM messages ORDER BY id ASC;') .all(); @@ -2243,7 +2276,7 @@ async function getMessageBySender({ sourceDevice?: number; sent_at: number; }): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: JSONRows = prepare( db, ` @@ -2307,7 +2340,7 @@ async function getUnreadByConversationAndMarkRead({ readAt?: number; now?: number; }): Promise { - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { const expirationStartTimestamp = Math.min(now, readAt ?? Infinity); @@ -2396,7 +2429,7 @@ async function getUnreadReactionsAndMarkRead({ newestUnreadAt: number; storyId?: string; }): Promise> { - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { const unreadMessages: Array = db @@ -2439,7 +2472,7 @@ async function markReactionAsRead( targetAuthorServiceId: ServiceIdString, targetTimestamp: number ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { const readReaction = db .prepare( @@ -2484,7 +2517,7 @@ async function addReaction({ targetAuthorAci, targetTimestamp, }: ReactionType): Promise { - const db = getInstance(); + const db = await getWritableInstance(); await db .prepare( `INSERT INTO reactions ( @@ -2530,7 +2563,7 @@ async function removeReactionFromConversation({ targetAuthorServiceId: ServiceIdString; targetTimestamp: number; }): Promise { - const db = getInstance(); + const db = await getWritableInstance(); await db .prepare( `DELETE FROM reactions WHERE @@ -2548,11 +2581,11 @@ async function removeReactionFromConversation({ } async function _getAllReactions(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); return db.prepare('SELECT * from reactions;').all(); } async function _removeAllReactions(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare('DELETE from reactions;').run(); } @@ -2580,7 +2613,7 @@ function getRecentStoryRepliesSync( sentAt = Number.MAX_VALUE, }: GetRecentStoryRepliesOptionsType = {} ): Array { - const db = getInstance(); + const db = getReadonlyInstance(); const timeFilters = { first: sqlFragment`received_at = ${receivedAt} AND sent_at < ${sentAt}`, second: sqlFragment`received_at < ${receivedAt}`, @@ -2621,7 +2654,7 @@ function getAdjacentMessagesByConversationSync( storyId, }: AdjacentMessagesByConversationOptionsType ): Array { - const db = getInstance(); + const db = getReadonlyInstance(); let timeFilters: { first: QueryFragment; second: QueryFragment }; let timeOrder: QueryFragment; @@ -2717,7 +2750,7 @@ async function getAllStories({ conversationId?: string; sourceServiceId?: ServiceIdString; }): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: ReadonlyArray<{ json: string; hasReplies: number; @@ -2777,7 +2810,7 @@ function getOldestMessageForConversation( includeStoryReplies: boolean; } ): MessageMetricsType | undefined { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT received_at, sent_at, id FROM messages WHERE conversationId = ${conversationId} AND @@ -2805,7 +2838,7 @@ function getNewestMessageForConversation( includeStoryReplies: boolean; } ): MessageMetricsType | undefined { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT received_at, sent_at, id FROM messages WHERE conversationId = ${conversationId} AND @@ -2833,7 +2866,7 @@ async function getMessagesBetween( conversationId: string, options: GetMessagesBetweenOptions ): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); // In the future we could accept this as an option, but for now we just // use it for the story predicate. @@ -2875,7 +2908,7 @@ async function getNearbyMessageFromDeletedSet({ storyId, includeStoryReplies, }: GetNearbyMessageFromDeletedSetOptionsType): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); function runQuery(after: boolean) { const dir = after ? sqlFragment`ASC` : sqlFragment`DESC`; @@ -2920,7 +2953,7 @@ function getLastConversationActivity({ conversationId: string; includeStoryReplies: boolean; }): MessageType | undefined { - const db = getInstance(); + const db = getReadonlyInstance(); const row = prepare( db, ` @@ -2956,7 +2989,7 @@ function getLastConversationPreview({ json: string; }>; - const db = getInstance(); + const db = getReadonlyInstance(); const index = includeStoryReplies ? 'messages_preview' @@ -2993,7 +3026,7 @@ async function getConversationMessageStats({ conversationId: string; includeStoryReplies: boolean; }): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); return db.transaction(() => { return { @@ -3015,7 +3048,7 @@ async function getLastConversationMessage({ }: { conversationId: string; }): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const row = db .prepare( ` @@ -3046,7 +3079,7 @@ function getOldestUnseenMessageForConversation( includeStoryReplies: boolean; } ): MessageMetricsType | undefined { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT received_at, sent_at, id FROM messages WHERE @@ -3084,7 +3117,7 @@ export function getOldestUnreadMentionOfMeForConversationSync( includeStoryReplies: boolean; } ): MessageMetricsType | undefined { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT received_at, sent_at, id FROM messages WHERE conversationId = ${conversationId} AND @@ -3118,7 +3151,7 @@ function getTotalUnreadForConversationSync( includeStoryReplies: boolean; } ): number { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT count(1) FROM messages @@ -3151,7 +3184,7 @@ function getTotalUnreadMentionsOfMeForConversationSync( includeStoryReplies: boolean; } ): number { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT count(1) FROM messages @@ -3176,7 +3209,7 @@ function getTotalUnseenForConversationSync( includeStoryReplies: boolean; } ): number { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT count(1) FROM messages @@ -3230,7 +3263,7 @@ async function getConversationRangeCenteredOnMessage( ): Promise< GetConversationRangeCenteredOnMessageResultType > { - const db = getInstance(); + const db = getReadonlyInstance(); return db.transaction(() => { return { @@ -3248,7 +3281,7 @@ async function getConversationRangeCenteredOnMessage( } async function getAllCallHistory(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const [query] = sql` SELECT * FROM callsHistory; `; @@ -3258,7 +3291,7 @@ async function getAllCallHistory(): Promise> { async function clearCallHistory( beforeTimestamp: number ): Promise> { - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { const whereMessages = sqlFragment` WHERE messages.type IS 'call-history' @@ -3296,7 +3329,7 @@ async function clearCallHistory( } async function cleanupCallHistoryMessages(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); return db .transaction(() => { const [query, params] = sql` @@ -3317,7 +3350,7 @@ async function getCallHistoryMessageByCallId(options: { conversationId: string; callId: string; }): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT json FROM messages @@ -3336,7 +3369,7 @@ async function getCallHistory( callId: string, peerId: ServiceIdString | string ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT * FROM callsHistory @@ -3361,7 +3394,7 @@ const CALL_STATUS_INCOMING = sqlConstant(CallDirection.Incoming); const FOUR_HOURS_IN_MS = sqlConstant(4 * 60 * 60 * 1000); async function getCallHistoryUnreadCount(): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT count(*) FROM messages LEFT JOIN callsHistory ON callsHistory.callId = messages.callId @@ -3375,7 +3408,7 @@ async function getCallHistoryUnreadCount(): Promise { } async function markCallHistoryRead(callId: string): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const [query, params] = sql` UPDATE messages SET readStatus = ${READ_STATUS_READ} @@ -3386,7 +3419,7 @@ async function markCallHistoryRead(callId: string): Promise { } async function markAllCallHistoryRead(): Promise> { - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { const where = sqlFragment` @@ -3602,7 +3635,9 @@ const countSchema = z.number().int().nonnegative(); async function getCallHistoryGroupsCount( filter: CallHistoryFilter ): Promise { - const db = getInstance(); + // getCallHistoryGroupDataSync creates a temporary table and thus requires + // write access. + const db = await getWritableInstance(); const result = getCallHistoryGroupDataSync(db, true, filter, { limit: 0, offset: 0, @@ -3628,7 +3663,9 @@ async function getCallHistoryGroups( filter: CallHistoryFilter, pagination: CallHistoryPagination ): Promise> { - const db = getInstance(); + // getCallHistoryGroupDataSync creates a temporary table and thus requires + // write access. + const db = await getWritableInstance(); const groupsData = groupsDataSchema.parse( getCallHistoryGroupDataSync(db, false, filter, pagination) ); @@ -3663,7 +3700,7 @@ async function getCallHistoryGroups( } async function saveCallHistory(callHistory: CallHistoryDetails): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const [insertQuery, insertParams] = sql` INSERT OR REPLACE INTO callsHistory ( @@ -3694,7 +3731,7 @@ async function hasGroupCallHistoryMessage( conversationId: string, eraId: string ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const exists: number = db .prepare( @@ -3721,7 +3758,7 @@ async function migrateConversationMessages( obsoleteId: string, currentId: string ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` @@ -3739,7 +3776,7 @@ async function migrateConversationMessages( async function getMessagesBySentAt( sentAt: number ): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const [query, params] = sql` SELECT messages.json, received_at, sent_at FROM edited_messages @@ -3758,7 +3795,7 @@ async function getMessagesBySentAt( } async function getExpiredMessages(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const now = Date.now(); const rows: JSONRows = db @@ -3777,7 +3814,7 @@ async function getExpiredMessages(): Promise> { async function getMessagesUnexpectedlyMissingExpirationStartTimestamp(): Promise< Array > { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: JSONRows = db .prepare( ` @@ -3802,7 +3839,7 @@ async function getMessagesUnexpectedlyMissingExpirationStartTimestamp(): Promise } async function getSoonestMessageExpiry(): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); // Note: we use `pluck` to only get the first column. const result: null | number = db @@ -3825,7 +3862,7 @@ async function getSoonestMessageExpiry(): Promise { async function getNextTapToViewMessageTimestampToAgeOut(): Promise< undefined | number > { - const db = getInstance(); + const db = getReadonlyInstance(); const row = db .prepare( ` @@ -3849,7 +3886,7 @@ async function getNextTapToViewMessageTimestampToAgeOut(): Promise< } async function getTapToViewMessagesNeedingErase(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const THIRTY_DAYS_AGO = Date.now() - 30 * 24 * 60 * 60 * 1000; const rows: JSONRows = db @@ -3873,8 +3910,7 @@ async function getTapToViewMessagesNeedingErase(): Promise> { const MAX_UNPROCESSED_ATTEMPTS = 10; -function saveUnprocessedSync(data: UnprocessedType): string { - const db = getInstance(); +function saveUnprocessedSync(db: Database, data: UnprocessedType): string { const { id, timestamp, @@ -3951,10 +3987,10 @@ function saveUnprocessedSync(data: UnprocessedType): string { } function updateUnprocessedWithDataSync( + db: Database, id: string, data: UnprocessedUpdateType ): void { - const db = getInstance(); const { source, sourceServiceId, @@ -3991,17 +4027,18 @@ async function updateUnprocessedWithData( id: string, data: UnprocessedUpdateType ): Promise { - return updateUnprocessedWithDataSync(id, data); + const db = await getWritableInstance(); + return updateUnprocessedWithDataSync(db, id, data); } async function updateUnprocessedsWithData( arrayOfUnprocessed: Array<{ id: string; data: UnprocessedUpdateType }> ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { for (const { id, data } of arrayOfUnprocessed) { - assertSync(updateUnprocessedWithDataSync(id, data)); + assertSync(updateUnprocessedWithDataSync(db, id, data)); } })(); } @@ -4009,7 +4046,7 @@ async function updateUnprocessedsWithData( async function getUnprocessedById( id: string ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const row = db .prepare('SELECT * FROM unprocessed WHERE id = $id;') .get({ @@ -4024,12 +4061,12 @@ async function getUnprocessedById( } async function getUnprocessedCount(): Promise { - return getCountFromTable(getInstance(), 'unprocessed'); + return getCountFromTable(getReadonlyInstance(), 'unprocessed'); } async function getAllUnprocessedIds(): Promise> { log.info('getAllUnprocessedIds'); - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { // cleanup first @@ -4080,7 +4117,7 @@ async function getUnprocessedByIdsAndIncrementAttempts( ): Promise> { log.info('getUnprocessedByIdsAndIncrementAttempts', { totalIds: ids.length }); - const db = getInstance(); + const db = await getWritableInstance(); batchMultiVarQuery(db, ids, batch => { return db @@ -4113,10 +4150,11 @@ async function getUnprocessedByIdsAndIncrementAttempts( }); } -function removeUnprocessedsSync(ids: ReadonlyArray): void { +function removeUnprocessedsSync( + db: Database, + ids: ReadonlyArray +): void { log.info('removeUnprocessedsSync', { totalIds: ids.length }); - const db = getInstance(); - db.prepare( ` DELETE FROM unprocessed @@ -4125,10 +4163,8 @@ function removeUnprocessedsSync(ids: ReadonlyArray): void { ).run(ids); } -function removeUnprocessedSync(id: string | Array): void { +function removeUnprocessedSync(db: Database, id: string | Array): void { log.info('removeUnprocessedSync', { id }); - const db = getInstance(); - if (!Array.isArray(id)) { prepare(db, 'DELETE FROM unprocessed WHERE id = $id;').run({ id }); @@ -4141,15 +4177,19 @@ function removeUnprocessedSync(id: string | Array): void { return; } - assertSync(batchMultiVarQuery(db, id, removeUnprocessedsSync)); + assertSync( + batchMultiVarQuery(db, id, batch => removeUnprocessedsSync(db, batch)) + ); } async function removeUnprocessed(id: string | Array): Promise { - removeUnprocessedSync(id); + const db = await getWritableInstance(); + + removeUnprocessedSync(db, id); } async function removeAllUnprocessed(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare('DELETE FROM unprocessed;').run(); } @@ -4159,13 +4199,13 @@ const ATTACHMENT_DOWNLOADS_TABLE = 'attachment_downloads'; async function getAttachmentDownloadJobById( id: string ): Promise { - return getById(getInstance(), ATTACHMENT_DOWNLOADS_TABLE, id); + return getById(getReadonlyInstance(), ATTACHMENT_DOWNLOADS_TABLE, id); } async function getNextAttachmentDownloadJobs( limit?: number, options: { timestamp?: number } = {} ): Promise> { - const db = getInstance(); + const db = await getWritableInstance(); const timestamp = options && options.timestamp ? options.timestamp : Date.now(); @@ -4195,7 +4235,7 @@ async function getNextAttachmentDownloadJobs( `JSON: '${row.json}' ` + `Error: ${Errors.toLogFormat(error)}` ); - removeAttachmentDownloadJobSync(row.id); + removeAttachmentDownloadJobSync(db, row.id); throw new Error(INNER_ERROR); } }); @@ -4209,7 +4249,7 @@ async function getNextAttachmentDownloadJobs( async function saveAttachmentDownloadJob( job: AttachmentDownloadJobType ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const { id, pending, timestamp } = job; if (!id) { throw new Error( @@ -4242,7 +4282,7 @@ async function setAttachmentDownloadJobPending( id: string, pending: boolean ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` UPDATE attachment_downloads @@ -4255,7 +4295,7 @@ async function setAttachmentDownloadJobPending( }); } async function resetAttachmentDownloadPending(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` UPDATE attachment_downloads @@ -4264,20 +4304,24 @@ async function resetAttachmentDownloadPending(): Promise { ` ).run(); } -function removeAttachmentDownloadJobSync(id: string): void { - return removeById(getInstance(), ATTACHMENT_DOWNLOADS_TABLE, id); +function removeAttachmentDownloadJobSync(db: Database, id: string): void { + return removeById(db, ATTACHMENT_DOWNLOADS_TABLE, id); } async function removeAttachmentDownloadJob(id: string): Promise { - return removeAttachmentDownloadJobSync(id); + const db = await getWritableInstance(); + return removeAttachmentDownloadJobSync(db, id); } async function removeAllAttachmentDownloadJobs(): Promise { - return removeAllFromTable(getInstance(), ATTACHMENT_DOWNLOADS_TABLE); + return removeAllFromTable( + await getWritableInstance(), + ATTACHMENT_DOWNLOADS_TABLE + ); } // Stickers async function createOrUpdateStickerPack(pack: StickerPackType): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const { attemptedStatus, author, @@ -4416,11 +4460,11 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise { ).run(payload); } function updateStickerPackStatusSync( + db: Database, id: string, status: StickerPackStatusType, options?: { timestamp: number } ): void { - const db = getInstance(); const timestamp = options ? options.timestamp || Date.now() : Date.now(); const installedAt = status === 'installed' ? timestamp : null; @@ -4441,7 +4485,8 @@ async function updateStickerPackStatus( status: StickerPackStatusType, options?: { timestamp: number } ): Promise { - return updateStickerPackStatusSync(id, status, options); + const db = await getWritableInstance(); + return updateStickerPackStatusSync(db, id, status, options); } async function updateStickerPackInfo({ id, @@ -4451,7 +4496,7 @@ async function updateStickerPackInfo({ storageNeedsSync, uninstalledAt, }: StickerPackInfoType): Promise { - const db = getInstance(); + const db = await getWritableInstance(); if (uninstalledAt) { db.prepare( @@ -4492,7 +4537,7 @@ async function updateStickerPackInfo({ } } async function clearAllErrorStickerPackAttempts(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` @@ -4503,7 +4548,7 @@ async function clearAllErrorStickerPackAttempts(): Promise { ).run(); } async function createOrUpdateSticker(sticker: StickerType): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const { emoji, height, id, isCoverOnly, lastUsed, packId, path, width } = sticker; @@ -4556,7 +4601,7 @@ async function updateStickerLastUsed( stickerId: number, lastUsed: number ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` UPDATE stickers @@ -4583,7 +4628,7 @@ async function addStickerPackReference( messageId: string, packId: string ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); if (!messageId) { throw new Error( @@ -4615,7 +4660,7 @@ async function deleteStickerPackReference( messageId: string, packId: string ): Promise | undefined> { - const db = getInstance(); + const db = await getWritableInstance(); if (!messageId) { throw new Error( @@ -4707,7 +4752,7 @@ async function deleteStickerPackReference( } async function deleteStickerPack(packId: string): Promise> { - const db = getInstance(); + const db = await getWritableInstance(); if (!packId) { throw new Error( @@ -4748,10 +4793,10 @@ async function deleteStickerPack(packId: string): Promise> { } async function getStickerCount(): Promise { - return getCountFromTable(getInstance(), 'stickers'); + return getCountFromTable(getReadonlyInstance(), 'stickers'); } async function getAllStickerPacks(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = db .prepare( @@ -4772,9 +4817,10 @@ async function getAllStickerPacks(): Promise> { }; }); } -function addUninstalledStickerPackSync(pack: UninstalledStickerPackType): void { - const db = getInstance(); - +function addUninstalledStickerPackSync( + db: Database, + pack: UninstalledStickerPackType +): void { db.prepare( ` INSERT OR REPLACE INTO uninstalled_sticker_packs @@ -4800,22 +4846,22 @@ function addUninstalledStickerPackSync(pack: UninstalledStickerPackType): void { async function addUninstalledStickerPack( pack: UninstalledStickerPackType ): Promise { - return addUninstalledStickerPackSync(pack); + const db = await getWritableInstance(); + return addUninstalledStickerPackSync(db, pack); } -function removeUninstalledStickerPackSync(packId: string): void { - const db = getInstance(); - +function removeUninstalledStickerPackSync(db: Database, packId: string): void { db.prepare( 'DELETE FROM uninstalled_sticker_packs WHERE id IS $id' ).run({ id: packId }); } async function removeUninstalledStickerPack(packId: string): Promise { - return removeUninstalledStickerPackSync(packId); + const db = await getWritableInstance(); + return removeUninstalledStickerPackSync(db, packId); } async function getUninstalledStickerPacks(): Promise< Array > { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = db .prepare( @@ -4826,7 +4872,7 @@ async function getUninstalledStickerPacks(): Promise< return rows || []; } async function getInstalledStickerPacks(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); // If sticker pack has a storageID - it is being downloaded and about to be // installed so we better sync it back to storage service if asked. @@ -4848,7 +4894,7 @@ async function getInstalledStickerPacks(): Promise> { async function getStickerPackInfo( packId: string ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); return db.transaction(() => { const uninstalled = db @@ -4884,22 +4930,22 @@ async function installStickerPack( packId: string, timestamp: number ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { const status = 'installed'; - updateStickerPackStatusSync(packId, status, { timestamp }); + updateStickerPackStatusSync(db, packId, status, { timestamp }); - removeUninstalledStickerPackSync(packId); + removeUninstalledStickerPackSync(db, packId); })(); } async function uninstallStickerPack( packId: string, timestamp: number ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { const status = 'downloaded'; - updateStickerPackStatusSync(packId, status); + updateStickerPackStatusSync(db, packId, status); db.prepare( ` @@ -4912,7 +4958,7 @@ async function uninstallStickerPack( ` ).run({ packId }); - addUninstalledStickerPackSync({ + addUninstalledStickerPackSync(db, { id: packId, uninstalledAt: timestamp, storageNeedsSync: true, @@ -4920,7 +4966,7 @@ async function uninstallStickerPack( })(); } async function getAllStickers(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = db .prepare( @@ -4936,7 +4982,7 @@ async function getAllStickers(): Promise> { async function getRecentStickers({ limit }: { limit?: number } = {}): Promise< Array > { - const db = getInstance(); + const db = getReadonlyInstance(); // Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index const rows = db @@ -4961,7 +5007,7 @@ async function updateEmojiUsage( shortName: string, timeUsed: number = Date.now() ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { const rows = db @@ -4995,7 +5041,7 @@ async function updateEmojiUsage( } async function getRecentEmojis(limit = 32): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = db .prepare( ` @@ -5011,7 +5057,7 @@ async function getRecentEmojis(limit = 32): Promise> { } async function getAllBadges(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const [badgeRows, badgeImageFileRows] = db.transaction(() => [ db.prepare('SELECT * FROM badges').all(), @@ -5048,7 +5094,7 @@ async function getAllBadges(): Promise> { async function updateOrCreateBadges( badges: ReadonlyArray ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const insertBadge = prepare( db, @@ -5127,7 +5173,7 @@ async function badgeImageFileDownloaded( url: string, localPath: string ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); prepare( db, 'UPDATE badgeImageFiles SET localPath = $localPath WHERE url = $url' @@ -5135,7 +5181,7 @@ async function badgeImageFileDownloaded( } async function getAllBadgeImageFileLocalPaths(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const localPaths = db .prepare( 'SELECT localPath FROM badgeImageFiles WHERE localPath IS NOT NULL' @@ -5204,7 +5250,7 @@ function freezeStoryDistribution( async function _getAllStoryDistributions(): Promise< Array > { - const db = getInstance(); + const db = getReadonlyInstance(); const storyDistributions = db .prepare('SELECT * FROM storyDistributions;') .all(); @@ -5214,13 +5260,13 @@ async function _getAllStoryDistributions(): Promise< async function _getAllStoryDistributionMembers(): Promise< Array > { - const db = getInstance(); + const db = getReadonlyInstance(); return db .prepare('SELECT * FROM storyDistributionMembers;') .all(); } async function _deleteAllStoryDistributions(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare('DELETE FROM storyDistributions;').run(); } async function createNewStoryDistribution( @@ -5231,7 +5277,7 @@ async function createNewStoryDistribution( 'Distribution list does not have a valid name' ); - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { const payload = freezeStoryDistribution(distribution); @@ -5304,7 +5350,7 @@ async function getAllStoryDistributionsWithMembers(): Promise< async function getStoryDistributionWithMembers( id: string ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); const storyDistribution: StoryDistributionForDatabase | undefined = prepare( db, 'SELECT * FROM storyDistributions WHERE id = $id;' @@ -5411,7 +5457,7 @@ async function modifyStoryDistributionWithMembers( }: { toAdd: Array; toRemove: Array } ): Promise { const payload = freezeStoryDistribution(distribution); - const db = getInstance(); + const db = await getWritableInstance(); if (toAdd.length || toRemove.length) { db.transaction(() => { @@ -5426,7 +5472,7 @@ async function modifyStoryDistribution( distribution: StoryDistributionType ): Promise { const payload = freezeStoryDistribution(distribution); - const db = getInstance(); + const db = await getWritableInstance(); modifyStoryDistributionSync(db, payload); } async function modifyStoryDistributionMembers( @@ -5436,7 +5482,7 @@ async function modifyStoryDistributionMembers( toRemove, }: { toAdd: Array; toRemove: Array } ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { modifyStoryDistributionMembersSync(db, listId, { toAdd, toRemove }); @@ -5445,22 +5491,22 @@ async function modifyStoryDistributionMembers( async function deleteStoryDistribution( id: StoryDistributionIdString ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare('DELETE FROM storyDistributions WHERE id = $id;').run({ id, }); } async function _getAllStoryReads(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); return db.prepare('SELECT * FROM storyReads;').all(); } async function _deleteAllStoryReads(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare('DELETE FROM storyReads;').run(); } async function addNewStoryRead(read: StoryReadType): Promise { - const db = getInstance(); + const db = await getWritableInstance(); prepare( db, @@ -5490,7 +5536,7 @@ async function getLastStoryReadsForAuthor({ }): Promise> { const limit = initialLimit || 5; - const db = getInstance(); + const db = getReadonlyInstance(); return db .prepare( ` @@ -5512,7 +5558,7 @@ async function getLastStoryReadsForAuthor({ async function countStoryReadsByConversation( conversationId: string ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); return db .prepare( ` @@ -5526,7 +5572,7 @@ async function countStoryReadsByConversation( // All data in database async function removeAll(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { db.exec(` @@ -5585,7 +5631,7 @@ async function removeAll(): Promise { async function removeAllConfiguration( mode = RemoveAllConfiguration.Full ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { db.exec( @@ -5633,7 +5679,7 @@ async function removeAllConfiguration( } async function eraseStorageServiceState(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.exec(` -- Conversations @@ -5672,7 +5718,7 @@ async function getMessagesNeedingUpgrade( limit: number, { maxVersion }: { maxVersion: number } ): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: JSONRows = db .prepare( @@ -5701,7 +5747,7 @@ async function getMessagesWithVisualMediaAttachments( conversationId: string, { limit }: { limit: number } ): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows: JSONRows = db .prepare( ` @@ -5730,7 +5776,7 @@ async function getMessagesWithFileAttachments( conversationId: string, { limit }: { limit: number } ): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const rows = db .prepare( ` @@ -5754,7 +5800,7 @@ async function getMessagesWithFileAttachments( async function getMessageServerGuidsForSpam( conversationId: string ): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); // The server's maximum is 3, which is why you see `LIMIT 3` in this query. Note that we // use `pluck` here to only get the first column! @@ -5878,7 +5924,7 @@ function getExternalDraftFilesForConversation( async function getKnownMessageAttachments( cursor?: MessageAttachmentsCursorType ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const result = new Set(); const chunkSize = 1000; @@ -5971,7 +6017,7 @@ async function finishGetKnownMessageAttachments({ count, done, }: MessageAttachmentsCursorType): Promise { - const db = getInstance(); + const db = await getWritableInstance(); const logId = `finishGetKnownMessageAttachments(${runId})`; if (!done) { @@ -5987,7 +6033,7 @@ async function finishGetKnownMessageAttachments({ } async function getKnownConversationAttachments(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const result = new Set(); const chunkSize = 500; @@ -6038,7 +6084,7 @@ async function getKnownConversationAttachments(): Promise> { async function removeKnownStickers( allStickers: ReadonlyArray ): Promise> { - const db = getInstance(); + const db = await getWritableInstance(); const lookup: Dictionary = fromPairs( map(allStickers, file => [file, true]) ); @@ -6089,7 +6135,7 @@ async function removeKnownStickers( async function removeKnownDraftAttachments( allStickers: ReadonlyArray ): Promise> { - const db = getInstance(); + const db = await getWritableInstance(); const lookup: Dictionary = fromPairs( map(allStickers, file => [file, true]) ); @@ -6147,7 +6193,7 @@ async function removeKnownDraftAttachments( } async function getJobsInQueue(queueType: string): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); return getJobsInQueueSync(db, queueType); } @@ -6190,12 +6236,12 @@ export function insertJobSync(db: Database, job: Readonly): void { } async function insertJob(job: Readonly): Promise { - const db = getInstance(); + const db = await getWritableInstance(); return insertJobSync(db, job); } async function deleteJob(id: string): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare('DELETE FROM jobs WHERE id = $id').run({ id }); } @@ -6203,7 +6249,7 @@ async function deleteJob(id: string): Promise { async function wasGroupCallRingPreviouslyCanceled( ringId: bigint ): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); return db .prepare( @@ -6223,7 +6269,7 @@ async function wasGroupCallRingPreviouslyCanceled( } async function processGroupCallRingCancellation(ringId: bigint): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` @@ -6239,7 +6285,7 @@ async function processGroupCallRingCancellation(ringId: bigint): Promise { const MAX_GROUP_CALL_RING_AGE = 30 * durations.MINUTE; async function cleanExpiredGroupCallRingCancellations(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` @@ -6252,7 +6298,7 @@ async function cleanExpiredGroupCallRingCancellations(): Promise { } async function getMaxMessageCounter(): Promise { - const db = getInstance(); + const db = getReadonlyInstance(); return db .prepare( @@ -6271,7 +6317,7 @@ async function getMaxMessageCounter(): Promise { } async function getStatisticsForLogging(): Promise> { - const db = getInstance(); + const db = getReadonlyInstance(); const counts = await pProps({ messageCount: getMessageCount(), conversationCount: getConversationCount(), @@ -6288,7 +6334,7 @@ async function updateAllConversationColors( value: CustomColorType; } ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.prepare( ` @@ -6305,7 +6351,7 @@ async function updateAllConversationColors( } async function removeAllProfileKeyCredentials(): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.exec( ` @@ -6321,11 +6367,11 @@ async function saveEditedMessage( ourAci: AciString, { conversationId, messageId, readStatus, sentAt }: EditedMessageType ): Promise { - const db = getInstance(); + const db = await getWritableInstance(); db.transaction(() => { assertSync( - saveMessageSync(mainMessage, { + saveMessageSync(db, mainMessage, { ourAci, alreadyInTransaction: true, }) @@ -6352,7 +6398,7 @@ async function saveEditedMessage( async function _getAllEditedMessages(): Promise< Array<{ messageId: string; sentAt: number }> > { - const db = getInstance(); + const db = getReadonlyInstance(); return db .prepare( @@ -6370,7 +6416,7 @@ async function getUnreadEditedMessagesAndMarkRead({ conversationId: string; newestUnreadAt: number; }): Promise { - const db = getInstance(); + const db = await getWritableInstance(); return db.transaction(() => { const [selectQuery, selectParams] = sql`