diff --git a/prototypes/idb-continue-on-constrainterror.html b/prototypes/idb-continue-on-constrainterror.html new file mode 100644 index 00000000..71e56c27 --- /dev/null +++ b/prototypes/idb-continue-on-constrainterror.html @@ -0,0 +1,100 @@ + + +
+ + + + + + + + diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index 3fd9f15f..f0dcf79f 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -247,8 +247,8 @@ export function tests() { storage.storeNames.timelineFragments ]); txn.timelineFragments.add({id: 1, roomId}); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 2, event: messageEvent, roomId}); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 3, event: myReactionEvent, roomId}); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 2, event: messageEvent, roomId}, new NullLogItem()); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 3, event: myReactionEvent, roomId}, new NullLogItem()); await relationWriter.writeRelation(myReactionEntry, txn, new NullLogItem()); await txn.complete(); // 2. setup queue & timeline @@ -309,7 +309,7 @@ export function tests() { storage.storeNames.timelineFragments ]); txn.timelineFragments.add({id: 1, roomId}); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 2, event: messageEvent, roomId}); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 2, event: messageEvent, roomId}, new NullLogItem()); await txn.complete(); // 2. setup queue & timeline const queue = new SendQueue({roomId, storage, hsApi: new MockHomeServer().api}); diff --git a/src/logging/NullLogger.js b/src/logging/NullLogger.js index 202d01f3..da04f16b 100644 --- a/src/logging/NullLogger.js +++ b/src/logging/NullLogger.js @@ -20,7 +20,7 @@ function noop () {} export class NullLogger { constructor() { - this.item = new NullLogItem(); + this.item = new NullLogItem(this); } log() {} @@ -51,6 +51,10 @@ export class NullLogger { } export class NullLogItem { + constructor(logger) { + this.logger = logger; + } + wrap(_, callback) { return callback(this); } diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 54eabf96..9169b029 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -447,10 +447,10 @@ export function tests() { // 1. put event and reaction into storage const storage = await createMockStorage(); const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]); - txn.timelineEvents.insert({ + txn.timelineEvents.tryInsert({ event: withContent(createAnnotation(messageId, "👋"), createEvent("m.reaction", reactionId, bob)), fragmentId: 1, eventIndex: 1, roomId - }); + }, new NullLogItem()); txn.timelineRelations.add(roomId, messageId, ANNOTATION_RELATION_TYPE, reactionId); await txn.complete(); // 2. setup the timeline @@ -543,10 +543,10 @@ export function tests() { // 1. put reaction in storage const storage = await createMockStorage(); const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]); - txn.timelineEvents.insert({ + txn.timelineEvents.tryInsert({ event: withContent(createAnnotation(messageId, "👋"), createEvent("m.reaction", reactionId, bob)), fragmentId: 1, eventIndex: 3, roomId - }); + }, new NullLogItem()); await txn.complete(); // 2. setup timeline const pendingEvents = new ObservableArray(); diff --git a/src/matrix/room/timeline/persistence/GapWriter.js b/src/matrix/room/timeline/persistence/GapWriter.js index e133d713..ec23db5a 100644 --- a/src/matrix/room/timeline/persistence/GapWriter.js +++ b/src/matrix/room/timeline/persistence/GapWriter.js @@ -124,9 +124,10 @@ export class GapWriter { if (updatedRelationTargetEntries) { updatedEntries.push(...updatedRelationTargetEntries); } - txn.timelineEvents.insert(eventStorageEntry); - const eventEntry = new EventEntry(eventStorageEntry, this._fragmentIdComparer); - directionalAppend(entries, eventEntry, direction); + if (await txn.timelineEvents.tryInsert(eventStorageEntry, log)) { + const eventEntry = new EventEntry(eventStorageEntry, this._fragmentIdComparer); + directionalAppend(entries, eventEntry, direction); + } } return {entries, updatedEntries}; } diff --git a/src/matrix/room/timeline/persistence/RelationWriter.js b/src/matrix/room/timeline/persistence/RelationWriter.js index 4116b775..0466b3da 100644 --- a/src/matrix/room/timeline/persistence/RelationWriter.js +++ b/src/matrix/room/timeline/persistence/RelationWriter.js @@ -275,7 +275,7 @@ export function tests() { const storage = await createMockStorage(); const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 2, event, roomId}); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 2, event, roomId}, new NullLogItem()); const updatedEntries = await relationWriter.writeRelation(redactionEntry, txn, new NullLogItem()); await txn.complete(); @@ -300,7 +300,7 @@ export function tests() { const storage = await createMockStorage(); const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 2, event, roomId}); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 2, event, roomId}, new NullLogItem()); const updatedEntries = await relationWriter.writeRelation(reactionEntry, txn, new NullLogItem()); await txn.complete(); @@ -329,7 +329,7 @@ export function tests() { const storage = await createMockStorage(); const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 2, event, roomId}); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 2, event, roomId}, new NullLogItem()); await relationWriter.writeRelation(reaction1Entry, txn, new NullLogItem()); const updatedEntries = await relationWriter.writeRelation(reaction2Entry, txn, new NullLogItem()); await txn.complete(); @@ -358,10 +358,10 @@ export function tests() { const storage = await createMockStorage(); const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 2, event, roomId}); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 3, event: myReaction, roomId}); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 2, event, roomId}, new NullLogItem()); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 3, event: myReaction, roomId}, new NullLogItem()); await relationWriter.writeRelation(myReactionEntry, txn, new NullLogItem()); - txn.timelineEvents.insert({fragmentId: 1, eventIndex: 4, event: bobReaction, roomId}); + txn.timelineEvents.tryInsert({fragmentId: 1, eventIndex: 4, event: bobReaction, roomId}, new NullLogItem()); await relationWriter.writeRelation(bobReactionEntry, txn, new NullLogItem()); const updatedEntries = await relationWriter.writeRelation(myReactionRedactionEntry, txn, new NullLogItem()); await txn.complete(); diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js index 07326225..af6f55bc 100644 --- a/src/matrix/room/timeline/persistence/SyncWriter.js +++ b/src/matrix/room/timeline/persistence/SyncWriter.js @@ -162,7 +162,10 @@ export class SyncWriter { storageEntry.displayName = member.displayName; storageEntry.avatarUrl = member.avatarUrl; } - txn.timelineEvents.insert(storageEntry, log); + const couldInsert = await txn.timelineEvents.tryInsert(storageEntry, log); + if (!couldInsert) { + continue; + } const entry = new EventEntry(storageEntry, this._fragmentIdComparer); entries.push(entry); const updatedRelationTargetEntries = await this._relationWriter.writeRelation(entry, txn, log); @@ -252,3 +255,35 @@ export class SyncWriter { return this._lastLiveKey; } } + +import {createMockStorage} from "../../../../mocks/Storage.js"; +import {createEvent, withTextBody} from "../../../../mocks/event.js"; +import {Instance as nullLogger} from "../../../../logging/NullLogger.js"; +export function tests() { + const roomId = "!abc:hs.tld"; + return { + "calling timelineEvents.tryInsert with the same event id a second time fails": async assert => { + const storage = await createMockStorage(); + const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents]); + const event = withTextBody("hello!", createEvent("m.room.message", "$abc", "@alice:hs.tld")); + const entry1 = createEventEntry(EventKey.defaultLiveKey, roomId, event); + assert.equal(await txn.timelineEvents.tryInsert(entry1, nullLogger.item), true); + const entry2 = createEventEntry(EventKey.defaultLiveKey.nextKey(), roomId, event); + assert.equal(await txn.timelineEvents.tryInsert(entry2, nullLogger.item), false); + // fake-indexeddb still aborts the transaction when preventDefault is called by tryInsert, so don't await as it will abort + // await txn.complete(); + }, + "calling timelineEvents.tryInsert with the same event key a second time fails": async assert => { + const storage = await createMockStorage(); + const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents]); + const event1 = withTextBody("hello!", createEvent("m.room.message", "$abc", "@alice:hs.tld")); + const entry1 = createEventEntry(EventKey.defaultLiveKey, roomId, event1); + assert.equal(await txn.timelineEvents.tryInsert(entry1, nullLogger.item), true); + const event2 = withTextBody("hello!", createEvent("m.room.message", "$def", "@alice:hs.tld")); + const entry2 = createEventEntry(EventKey.defaultLiveKey, roomId, event2); + assert.equal(await txn.timelineEvents.tryInsert(entry2, nullLogger.item), false); + // fake-indexeddb still aborts the transaction when preventDefault is called by tryInsert, so don't await as it will abort + // await txn.complete(); + }, + } +} diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 42a41ae6..84e02e60 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -15,9 +15,9 @@ limitations under the License. */ import {QueryTarget, IDBQuery} from "./QueryTarget"; -import {IDBRequestAttemptError} from "./error"; +import {IDBRequestError, IDBRequestAttemptError} from "./error"; import {reqAsPromise} from "./utils"; -import {Transaction} from "./Transaction"; +import {Transaction, IDBKey} from "./Transaction"; import {LogItem} from "../../../logging/LogItem.js"; const LOG_REQUESTS = false; @@ -126,6 +126,10 @@ class QueryTargetWrapper