diff --git a/Postbox.xcodeproj/project.pbxproj b/Postbox.xcodeproj/project.pbxproj index 751e452158..f21bbcfcce 100644 --- a/Postbox.xcodeproj/project.pbxproj +++ b/Postbox.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ D00E0FBE1B85D1B5002E4EB5 /* LmdbValueBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00E0FBD1B85D1B5002E4EB5 /* LmdbValueBox.swift */; }; D00EED1E1C81F28D00341DFF /* MessageHistoryTagsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00EED1D1C81F28D00341DFF /* MessageHistoryTagsTable.swift */; }; D01F7D9B1CBEC390008765C9 /* MessageHistoryInvalidatedReadStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F7D9A1CBEC390008765C9 /* MessageHistoryInvalidatedReadStateTable.swift */; }; - D01F7D9D1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F7D9C1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift */; }; + D01F7D9D1CBF8586008765C9 /* SynchronizePeerReadStatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F7D9C1CBF8586008765C9 /* SynchronizePeerReadStatesView.swift */; }; D033A6F71C73D512006A2EAB /* MessageHistoryUnsentTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033A6F61C73D512006A2EAB /* MessageHistoryUnsentTable.swift */; }; D033A6F91C73E440006A2EAB /* UnsentMessageHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033A6F81C73E440006A2EAB /* UnsentMessageHistoryView.swift */; }; D03BCCF81C73561C0097A291 /* Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03BCCF71C73561C0097A291 /* Table.swift */; }; @@ -27,6 +27,7 @@ D044E1641B2AD718001EE087 /* MurMurHash32.h in Headers */ = {isa = PBXBuildFile; fileRef = D044E1611B2AD667001EE087 /* MurMurHash32.h */; settings = {ATTRIBUTES = (Public, ); }; }; D055BD331B7D3D2D00F06C0A /* MediaBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D055BD321B7D3D2D00F06C0A /* MediaBox.swift */; }; D05F09A61C9E9F9300BB6F96 /* MediaResourceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05F09A51C9E9F9300BB6F96 /* MediaResourceStatus.swift */; }; + D060B77B1CF4845A0050BE9B /* ReadStateTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D060B77A1CF4845A0050BE9B /* ReadStateTableTests.swift */; }; D07516441B2D9CEF00AE42E0 /* sqlite3.c in Sources */ = {isa = PBXBuildFile; fileRef = D07516401B2D9CEF00AE42E0 /* sqlite3.c */; settings = {COMPILER_FLAGS = "-Wno-conversion -Wno-ambiguous-macro -Wno-conditional-uninitialized -Wno-unused-const-variable -Wno-unused-function -Wno-unreachable-code"; }; }; D07516451B2D9CEF00AE42E0 /* sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = D07516411B2D9CEF00AE42E0 /* sqlite3.h */; }; D07516461B2D9CEF00AE42E0 /* sqlite3ext.h in Headers */ = {isa = PBXBuildFile; fileRef = D07516421B2D9CEF00AE42E0 /* sqlite3ext.h */; }; @@ -91,7 +92,7 @@ D00E0FBD1B85D1B5002E4EB5 /* LmdbValueBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LmdbValueBox.swift; sourceTree = ""; }; D00EED1D1C81F28D00341DFF /* MessageHistoryTagsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTagsTable.swift; sourceTree = ""; }; D01F7D9A1CBEC390008765C9 /* MessageHistoryInvalidatedReadStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryInvalidatedReadStateTable.swift; sourceTree = ""; }; - D01F7D9C1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvalidatedPeerReadStatesView.swift; sourceTree = ""; }; + D01F7D9C1CBF8586008765C9 /* SynchronizePeerReadStatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizePeerReadStatesView.swift; sourceTree = ""; }; D033A6F61C73D512006A2EAB /* MessageHistoryUnsentTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryUnsentTable.swift; sourceTree = ""; }; D033A6F81C73E440006A2EAB /* UnsentMessageHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsentMessageHistoryView.swift; sourceTree = ""; }; D03BCCF71C73561C0097A291 /* Table.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Table.swift; sourceTree = ""; }; @@ -103,6 +104,7 @@ D044E1621B2AD677001EE087 /* MurMurHash32.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MurMurHash32.m; sourceTree = ""; }; D055BD321B7D3D2D00F06C0A /* MediaBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaBox.swift; sourceTree = ""; }; D05F09A51C9E9F9300BB6F96 /* MediaResourceStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaResourceStatus.swift; sourceTree = ""; }; + D060B77A1CF4845A0050BE9B /* ReadStateTableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadStateTableTests.swift; sourceTree = ""; }; D07516401B2D9CEF00AE42E0 /* sqlite3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sqlite3.c; sourceTree = ""; }; D07516411B2D9CEF00AE42E0 /* sqlite3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlite3.h; sourceTree = ""; }; D07516421B2D9CEF00AE42E0 /* sqlite3ext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlite3ext.h; sourceTree = ""; }; @@ -119,7 +121,7 @@ D0977F9F1B8244D7009994B2 /* SqliteValueBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SqliteValueBox.swift; sourceTree = ""; }; D0A7D9441C556CFE0016A115 /* MessageHistoryIndexTableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryIndexTableTests.swift; sourceTree = ""; }; D0B76BE61B66639F0095CF45 /* DeferredString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeferredString.swift; sourceTree = ""; }; - D0C07F691B67DB4800966E43 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-gbpsmqzuwcmmxadrqcwyrluaftwp/Build/Products/Debug-iphoneos/SwiftSignalKit.framework"; sourceTree = ""; }; + D0C07F691B67DB4800966E43 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D0C674C71CBB11C600183765 /* MessageHistoryReadStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryReadStateTable.swift; sourceTree = ""; }; D0C674CB1CBB14A700183765 /* PeerReadState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerReadState.swift; sourceTree = ""; }; D0C735271C864DF300BB3149 /* PeerChatStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatStateTable.swift; sourceTree = ""; }; @@ -297,7 +299,7 @@ D003E4E51B38DBDB00C22CBC /* MessageHistoryView.swift */, D0F9E86A1C59719800037222 /* ChatListView.swift */, D033A6F81C73E440006A2EAB /* UnsentMessageHistoryView.swift */, - D01F7D9C1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift */, + D01F7D9C1CBF8586008765C9 /* SynchronizePeerReadStatesView.swift */, ); name = Views; sourceTree = ""; @@ -361,6 +363,7 @@ D0F9E8601C57766A00037222 /* MessageHistoryTableTests.swift */, D0F9E8681C58FA9300037222 /* ChatListTableTests.swift */, D0C8FCB61C5C2D200028C27F /* MessageHistoryViewTests.swift */, + D060B77A1CF4845A0050BE9B /* ReadStateTableTests.swift */, ); path = PostboxTests; sourceTree = ""; @@ -499,7 +502,7 @@ D00E0FBE1B85D1B5002E4EB5 /* LmdbValueBox.swift in Sources */, D08C713A1C501F0700779C0F /* MessageHistoryHole.swift in Sources */, D044CA2A1C617D39002160FF /* SeedConfiguration.swift in Sources */, - D01F7D9D1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift in Sources */, + D01F7D9D1CBF8586008765C9 /* SynchronizePeerReadStatesView.swift in Sources */, D0F9E85B1C565EBB00037222 /* MessageMediaTable.swift in Sources */, D0E1DE151C5E1C6900C7826E /* ViewTracker.swift in Sources */, D0F9E8751C5A334100037222 /* SimpleDictionary.swift in Sources */, @@ -548,6 +551,7 @@ D0F9E8611C57766A00037222 /* MessageHistoryTableTests.swift in Sources */, D0C8FCB71C5C2D200028C27F /* MessageHistoryViewTests.swift in Sources */, D0F9E8691C58FA9300037222 /* ChatListTableTests.swift in Sources */, + D060B77B1CF4845A0050BE9B /* ReadStateTableTests.swift in Sources */, D0A7D9451C556CFE0016A115 /* MessageHistoryIndexTableTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Postbox/InvalidatedPeerReadStatesView.swift b/Postbox/InvalidatedPeerReadStatesView.swift deleted file mode 100644 index ee44dea37f..0000000000 --- a/Postbox/InvalidatedPeerReadStatesView.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -final class InvalidatedPeerReadStatesView { - var peerIds = Set() - - init(peerIds: [PeerId]) { - for peerId in peerIds { - self.peerIds.insert(peerId) - } - } - - func replay(operations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) -> Bool { - var updated = false - for operation in operations { - switch operation { - case let .Insert(peerId): - if !self.peerIds.contains(peerId) { - self.peerIds.insert(peerId) - updated = true - } - case let .Remove(peerId): - if let _ = self.peerIds.remove(peerId) { - updated = true - } - } - } - - return updated - } -} diff --git a/Postbox/Message.swift b/Postbox/Message.swift index c3745d7b00..92c9d2a72e 100644 --- a/Postbox/Message.swift +++ b/Postbox/Message.swift @@ -124,15 +124,19 @@ public struct MessageIndex: Equatable, Comparable, Hashable { return self.id.hashValue } - static func absoluteUpperBound() -> MessageIndex { + public static func absoluteUpperBound() -> MessageIndex { return MessageIndex(id: MessageId(peerId: PeerId(namespace: Int32.max, id: Int32.max), namespace: Int32.max, id: Int32.max), timestamp: Int32.max) } - static func absoluteLowerBound() -> MessageIndex { + public static func absoluteLowerBound() -> MessageIndex { return MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: 0), timestamp: 0) } - static func upperBound(peerId: PeerId) -> MessageIndex { + public static func lowerBound(peerId: PeerId) -> MessageIndex { + return MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: 0) + } + + public static func upperBound(peerId: PeerId) -> MessageIndex { return MessageIndex(id: MessageId(peerId: peerId, namespace: Int32.max, id: Int32.max), timestamp: Int32.max) } } @@ -215,13 +219,34 @@ public struct StoreMessageForwardInfo { } } -public struct MessageForwardInfo { +public struct MessageForwardInfo: Equatable { public let author: Peer public let source: Peer? public let sourceMessageId: MessageId? public let date: Int32 } +public func ==(lhs: MessageForwardInfo, rhs: MessageForwardInfo) -> Bool { + if !lhs.author.isEqual(rhs.author) { + return false + } + if let lhsSource = lhs.source, rhsSource = rhs.source { + if !lhsSource.isEqual(rhsSource) { + return false + } + } else if (lhs.source == nil) != (rhs.source == nil) { + return false + } + if lhs.sourceMessageId != rhs.sourceMessageId { + return false + } + if lhs.date != rhs.date { + return false + } + + return true +} + public protocol MessageAttribute: Coding { var associatedPeerIds: [PeerId] { get } var associatedMessageIds: [MessageId] { get } @@ -237,7 +262,7 @@ public extension MessageAttribute { } } -public class Message { +public final class Message { public let stableId: UInt32 public let id: MessageId public let timestamp: Int32 diff --git a/Postbox/MessageHistoryIndexTable.swift b/Postbox/MessageHistoryIndexTable.swift index c7e4b030a8..9b4f704072 100644 --- a/Postbox/MessageHistoryIndexTable.swift +++ b/Postbox/MessageHistoryIndexTable.swift @@ -14,10 +14,46 @@ enum HistoryIndexEntry { } } -public enum HoleFillType { +public enum HoleFillDirection: Equatable { case UpperToLower case LowerToUpper - case Complete + case AroundIndex(MessageIndex) +} + +public func ==(lhs: HoleFillDirection, rhs: HoleFillDirection) -> Bool { + switch lhs { + case .UpperToLower: + switch rhs { + case .UpperToLower: + return true + default: + return false + } + case .LowerToUpper: + switch rhs { + case .LowerToUpper: + return true + default: + return false + } + case let .AroundIndex(lhsIndex): + switch rhs { + case let .AroundIndex(rhsIndex) where lhsIndex == rhsIndex: + return true + default: + return false + } + } +} + +public struct HoleFill { + public let complete: Bool + public let direction: HoleFillDirection + + public init(complete: Bool, direction: HoleFillDirection) { + self.complete = complete + self.direction = direction + } } public enum AddMessagesLocation { @@ -319,7 +355,7 @@ final class MessageHistoryIndexTable: Table { } } - func fillHole(id: MessageId, fillType: HoleFillType, tagMask: MessageTags?, messages: [InternalStoreMessage], inout operations: [MessageHistoryIndexOperation]) { + func fillHole(id: MessageId, fillType: HoleFill, tagMask: MessageTags?, messages: [InternalStoreMessage], inout operations: [MessageHistoryIndexOperation]) { self.ensureInitialized(id.peerId, operations: &operations) var upperItem: HistoryIndexEntry? @@ -336,6 +372,10 @@ final class MessageHistoryIndexTable: Table { switch upperItem { case let .Hole(upperHole): if let tagMask = tagMask { + if case .AroundIndex = fillType.direction { + assertionFailure(".AroundIndex not supported") + } + var messagesInRange: [InternalStoreMessage] = [] var i = 0 while i < remainingMessages.count { @@ -362,11 +402,10 @@ final class MessageHistoryIndexTable: Table { if i == 0 { if upperHole.min < message.id.id { let holeTags: UInt32 - switch fillType { - case .LowerToUpper, .Complete: - holeTags = clearedTags - case .UpperToLower: - holeTags = upperHole.tags + if fillType.complete || fillType.direction == .LowerToUpper { + holeTags = clearedTags + } else { + holeTags = upperHole.tags } self.justInsertHole(MessageHistoryHole(stableId: self.metadataTable.getNextStableMessageIndexId(), maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: message.id.id - 1), timestamp: message.timestamp), min: upperHole.min, tags: holeTags), operations: &operations) } @@ -380,11 +419,10 @@ final class MessageHistoryIndexTable: Table { if i == messagesInRange.count - 1 { if upperHole.maxIndex.id.id > message.id.id { let holeTags: UInt32 - switch fillType { - case .LowerToUpper: - holeTags = upperHole.tags - case .UpperToLower, .Complete: - holeTags = clearedTags + if fillType.complete || fillType.direction == .UpperToLower { + holeTags = clearedTags + } else { + holeTags = upperHole.tags } self.justInsertHole(MessageHistoryHole(stableId: self.metadataTable.getNextStableMessageIndexId(), maxIndex: upperHole.maxIndex, min: message.id.id + 1, tags: holeTags), operations: &operations) } @@ -401,58 +439,77 @@ final class MessageHistoryIndexTable: Table { while i < remainingMessages.count { let message = remainingMessages[i] if message.id.id >= upperHole.min && message.id.id <= upperHole.maxIndex.id.id { - if (fillType == .UpperToLower || fillType == .Complete) && (minMessageInRange == nil || minMessageInRange!.id > message.id) { + if (minMessageInRange == nil || minMessageInRange!.id > message.id) { minMessageInRange = message - if !removedHole { - removedHole = true - self.justRemove(upperHole.maxIndex, operations: &operations) + if (fillType.complete || fillType.direction == .UpperToLower) { + if !removedHole { + removedHole = true + self.justRemove(upperHole.maxIndex, operations: &operations) + } } } - if (fillType == .LowerToUpper || fillType == .Complete) && (maxMessageInRange == nil || maxMessageInRange!.id < message.id) { + + if (maxMessageInRange == nil || maxMessageInRange!.id < message.id) { maxMessageInRange = message - if !removedHole { - removedHole = true - self.justRemove(upperHole.maxIndex, operations: &operations) + if (fillType.complete || fillType.direction == .LowerToUpper) { + if !removedHole { + removedHole = true + self.justRemove(upperHole.maxIndex, operations: &operations) + } } } + self.justInsertMessage(message, operations: &operations) remainingMessages.removeAtIndex(i) } else { i += 1 } } - switch fillType { - case .Complete: - if !removedHole { - removedHole = true - self.justRemove(upperHole.maxIndex, operations: &operations) + if fillType.complete { + if !removedHole { + removedHole = true + self.justRemove(upperHole.maxIndex, operations: &operations) + } + } else if fillType.direction == .LowerToUpper { + if let maxMessageInRange = maxMessageInRange where maxMessageInRange.id.id != Int32.max && maxMessageInRange.id.id + 1 <= upperHole.maxIndex.id.id { + let stableId: UInt32 + let tags: UInt32 = upperHole.tags + if removedHole { + stableId = upperHole.stableId + } else { + stableId = self.metadataTable.getNextStableMessageIndexId() } - case .LowerToUpper: - if let maxMessageInRange = maxMessageInRange where maxMessageInRange.id.id != Int32.max && maxMessageInRange.id.id + 1 <= upperHole.maxIndex.id.id { - let stableId: UInt32 - let tags: UInt32 - if removedHole { - stableId = upperHole.stableId - tags = upperHole.tags - } else { - stableId = self.metadataTable.getNextStableMessageIndexId() - tags = MessageTags.All.rawValue - } - self.justInsertHole(MessageHistoryHole(stableId: stableId, maxIndex: upperHole.maxIndex, min: maxMessageInRange.id.id + 1, tags: tags), operations: &operations) - } - case .UpperToLower: - if let minMessageInRange = minMessageInRange where minMessageInRange.id.id - 1 >= upperHole.min { - let stableId: UInt32 - let tags: UInt32 - if removedHole { - stableId = upperHole.stableId - tags = upperHole.tags - } else { - stableId = self.metadataTable.getNextStableMessageIndexId() - tags = MessageTags.All.rawValue - } - self.justInsertHole(MessageHistoryHole(stableId: stableId, maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: minMessageInRange.id.id - 1), timestamp: minMessageInRange.timestamp), min: upperHole.min, tags: tags), operations: &operations) + self.justInsertHole(MessageHistoryHole(stableId: stableId, maxIndex: upperHole.maxIndex, min: maxMessageInRange.id.id + 1, tags: tags), operations: &operations) + } + } else if fillType.direction == .UpperToLower { + if let minMessageInRange = minMessageInRange where minMessageInRange.id.id - 1 >= upperHole.min { + let stableId: UInt32 + let tags: UInt32 = upperHole.tags + if removedHole { + stableId = upperHole.stableId + } else { + stableId = self.metadataTable.getNextStableMessageIndexId() } + self.justInsertHole(MessageHistoryHole(stableId: stableId, maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: minMessageInRange.id.id - 1), timestamp: minMessageInRange.timestamp), min: upperHole.min, tags: tags), operations: &operations) + } + } else if case .AroundIndex = fillType.direction { + if !removedHole { + self.justRemove(upperHole.maxIndex, operations: &operations) + removedHole = true + } + + if let minMessageInRange = minMessageInRange where minMessageInRange.id.id - 1 >= upperHole.min { + let stableId: UInt32 = upperHole.stableId + let tags: UInt32 = upperHole.tags + + self.justInsertHole(MessageHistoryHole(stableId: stableId, maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: minMessageInRange.id.id - 1), timestamp: minMessageInRange.timestamp), min: upperHole.min, tags: tags), operations: &operations) + } + + if let maxMessageInRange = maxMessageInRange where maxMessageInRange.id.id != Int32.max && maxMessageInRange.id.id + 1 <= upperHole.maxIndex.id.id { + let stableId: UInt32 = self.metadataTable.getNextStableMessageIndexId() + let tags: UInt32 = upperHole.tags + self.justInsertHole(MessageHistoryHole(stableId: stableId, maxIndex: upperHole.maxIndex, min: maxMessageInRange.id.id + 1, tags: tags), operations: &operations) + } } } case .Message: @@ -510,17 +567,17 @@ final class MessageHistoryIndexTable: Table { } } - private func adjacentItems(id: MessageId) -> (lower: HistoryIndexEntry?, upper: HistoryIndexEntry?) { + func adjacentItems(id: MessageId, bindUpper: Bool = true) -> (lower: HistoryIndexEntry?, upper: HistoryIndexEntry?) { let key = self.key(id) var lowerItem: HistoryIndexEntry? - self.valueBox.range(self.tableId, start: key, end: self.lowerBound(id.peerId, namespace: id.namespace), values: { key, value in + self.valueBox.range(self.tableId, start: bindUpper ? key : key.successor, end: self.lowerBound(id.peerId, namespace: id.namespace), values: { key, value in lowerItem = readHistoryIndexEntry(id.peerId, namespace: id.namespace, key: key, value: value) return true }, limit: 1) var upperItem: HistoryIndexEntry? - self.valueBox.range(self.tableId, start: key.predecessor, end: self.upperBound(id.peerId, namespace: id.namespace), values: { key, value in + self.valueBox.range(self.tableId, start: bindUpper ? key.predecessor : key, end: self.upperBound(id.peerId, namespace: id.namespace), values: { key, value in upperItem = readHistoryIndexEntry(id.peerId, namespace: id.namespace, key: key, value: value) return true }, limit: 1) @@ -539,10 +596,35 @@ final class MessageHistoryIndexTable: Table { return nil } + func top(peerId: PeerId, namespace: MessageId.Namespace) -> HistoryIndexEntry? { + var operations: [MessageHistoryIndexOperation] = [] + self.ensureInitialized(peerId, operations: &operations) + + var entry: HistoryIndexEntry? + self.valueBox.range(self.tableId, start: self.upperBound(peerId, namespace: namespace), end: self.lowerBound(peerId, namespace: namespace), values: { key, value in + entry = readHistoryIndexEntry(peerId, namespace: namespace, key: key, value: value) + return false + }, limit: 1) + + return entry + } + func exists(id: MessageId) -> Bool { return self.valueBox.exists(self.tableId, key: self.key(id)) } + func holeContainingId(id: MessageId) -> MessageHistoryHole? { + var result: MessageHistoryHole? + self.valueBox.range(self.tableId, start: self.key(MessageId(peerId: id.peerId, namespace: id.namespace, id: id.id)).predecessor, end: self.upperBound(id.peerId, namespace: id.namespace), values: { key, value in + if case let .Hole(hole) = readHistoryIndexEntry(id.peerId, namespace: id.namespace, key: key, value: value) { + result = hole + } + return true + }, limit: 1) + + return result + } + func incomingMessageCountInRange(peerId: PeerId, namespace: MessageId.Namespace, minId: MessageId.Id, maxId: MessageId.Id) -> (Int, Bool) { var count = 0 var holes = false diff --git a/Postbox/MessageHistoryInvalidatedReadStateTable.swift b/Postbox/MessageHistoryInvalidatedReadStateTable.swift index cd5b1be7a1..57f2628803 100644 --- a/Postbox/MessageHistoryInvalidatedReadStateTable.swift +++ b/Postbox/MessageHistoryInvalidatedReadStateTable.swift @@ -1,11 +1,30 @@ import Foundation -enum IntermediateMessageHistoryInvalidatedReadStateOperation { - case Insert(PeerId) - case Remove(PeerId) +public enum PeerReadStateSynchronizationOperation: Equatable { + case Push(thenSync: Bool) + case Validate } -final class MessageHistoryInvalidatedReadStateTable: Table { +public func ==(lhs: PeerReadStateSynchronizationOperation, rhs: PeerReadStateSynchronizationOperation) -> Bool { + switch lhs { + case let .Push(lhsThenSync): + switch rhs { + case let .Push(rhsThenSync) where lhsThenSync == rhsThenSync: + return true + default: + return false + } + case .Validate: + switch rhs { + case .Validate: + return true + default: + return false + } + } +} + +final class MessageHistorySynchronizeReadStateTable: Table { private let sharedKey = ValueBoxKey(length: 8) private func key(peerId: PeerId) -> ValueBoxKey { @@ -13,7 +32,7 @@ final class MessageHistoryInvalidatedReadStateTable: Table { return self.sharedKey } - private var updatedPeerIds: [PeerId: Bool] = [:] + private var updatedPeerIds: [PeerId: PeerReadStateSynchronizationOperation?] = [:] private func lowerBound() -> ValueBoxKey { let key = ValueBoxKey(length: 1) @@ -27,35 +46,52 @@ final class MessageHistoryInvalidatedReadStateTable: Table { return key } - func add(peerId: PeerId, inout operations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { - if !self.valueBox.exists(self.tableId, key: self.key(peerId)) { - self.valueBox.set(self.tableId, key: self.key(peerId), value: MemoryBuffer()) - operations.append(.Insert(peerId)) - } + func set(peerId: PeerId, operation: PeerReadStateSynchronizationOperation?, inout operations: [PeerId: PeerReadStateSynchronizationOperation?]) { + self.updatedPeerIds[peerId] = operation + operations[peerId] = operation } - func remove(peerId: PeerId, inout operations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { - if self.valueBox.exists(self.tableId, key: self.key(peerId)) { - self.valueBox.remove(self.tableId, key: self.key(peerId)) - operations.append(.Remove(peerId)) - } - } - - func get() -> [PeerId] { - var peerIds: [PeerId] = [] - self.valueBox.range(self.tableId, start: self.lowerBound(), end: self.upperBound(), keys: { key in - peerIds.append(PeerId(key.getInt64(0))) + func get() -> [PeerId: PeerReadStateSynchronizationOperation] { + self.beforeCommit() + + var operations: [PeerId: PeerReadStateSynchronizationOperation] = [:] + self.valueBox.range(self.tableId, start: self.lowerBound(), end: self.upperBound(), values: { key, value in + var operationValue: Int8 = 0 + value.read(&operationValue, offset: 0, length: 1) + + let operation: PeerReadStateSynchronizationOperation + if operationValue == 0 { + var syncValue: Int8 = 0 + value.read(&syncValue, offset: 0, length: 1) + operation = .Push(thenSync: syncValue != 0) + } else { + operation = .Validate + } + + operations[PeerId(key.getInt64(0))] = operation return true }, limit: 0) - return peerIds + return operations } override func beforeCommit() { let key = ValueBoxKey(length: 8) - let buffer = MemoryBuffer() - for (peerId, invalidated) in self.updatedPeerIds { + let buffer = WriteBuffer() + for (peerId, operation) in self.updatedPeerIds { key.setInt64(0, value: peerId.toInt64()) - if invalidated { + if let operation = operation { + buffer.reset() + switch operation { + case let .Push(thenSync): + var operationValue: Int8 = 0 + buffer.write(&operationValue, offset: 0, length: 1) + var syncValue: Int8 = thenSync ? 1 : 0 + buffer.write(&syncValue, offset: 0, length: 1) + case .Validate: + var operationValue: Int8 = 1 + buffer.write(&operationValue, offset: 0, length: 1) + } + self.valueBox.set(self.tableId, key: key, value: buffer) } else { self.valueBox.remove(self.tableId, key: key) diff --git a/Postbox/MessageHistoryReadStateTable.swift b/Postbox/MessageHistoryReadStateTable.swift index 3793491890..f0fb8772fd 100644 --- a/Postbox/MessageHistoryReadStateTable.swift +++ b/Postbox/MessageHistoryReadStateTable.swift @@ -1,5 +1,12 @@ import Foundation +private let traceReadStates = false + +enum ApplyInteractiveMaxReadIdResult { + case None + case Push(thenSync: Bool) +} + private final class InternalPeerReadStates { var namespaces: [MessageId.Namespace: PeerReadState] @@ -61,6 +68,10 @@ final class MessageHistoryReadStateTable: Table { } func resetStates(peerId: PeerId, namespaces: [MessageId.Namespace: PeerReadState]) -> CombinedPeerReadState? { + if traceReadStates { + print("[ReadStateTable] resetStates peerId: \(peerId), namespaces: \(namespaces)") + } + self.updatedPeerIds.insert(peerId) if let states = self.get(peerId) { @@ -96,6 +107,10 @@ final class MessageHistoryReadStateTable: Table { } if let states = self.get(peerId) { + if traceReadStates { + print("[ReadStateTable] addIncomingMessages peerId: \(peerId), ids: \(ids) (before: \(states.namespaces))") + } + var updated = false var invalidated = false for (namespace, ids) in idsByNamespace { @@ -112,6 +127,10 @@ final class MessageHistoryReadStateTable: Table { if addedUnreadCount != 0 { states.namespaces[namespace] = PeerReadState(maxReadId: currentState.maxReadId, maxKnownId: currentState.maxKnownId, count: currentState.count + addedUnreadCount) updated = true + + if traceReadStates { + print("[ReadStateTable] added \(addedUnreadCount)") + } } } } @@ -122,6 +141,9 @@ final class MessageHistoryReadStateTable: Table { return (updated ? CombinedPeerReadState(states: states.namespaces.map({$0})) : nil, invalidated) } else { + if traceReadStates { + print("[ReadStateTable] addIncomingMessages peerId: \(peerId), just invalidated)") + } return (nil, true) } @@ -139,6 +161,10 @@ final class MessageHistoryReadStateTable: Table { } if let states = self.get(peerId) { + if traceReadStates { + print("[ReadStateTable] deleteMessages peerId: \(peerId), ids: \(ids) (before: \(states.namespaces))") + } + var updated = false var invalidate = false for (namespace, ids) in idsByNamespace { @@ -174,13 +200,28 @@ final class MessageHistoryReadStateTable: Table { return (nil, false) } - func applyMaxReadId(peerId: PeerId, namespace: MessageId.Namespace, maxReadId: MessageId.Id, maxKnownId: MessageId.Id, incomingStatsInRange: (MessageId.Id, MessageId.Id) -> (count: Int, holes: Bool)) -> (CombinedPeerReadState?, Bool) { - if let states = self.get(peerId), state = states.namespaces[namespace] { - if state.maxReadId < maxReadId { - let (deltaCount, holes) = incomingStatsInRange(state.maxReadId + 1, maxReadId) + func applyMaxReadId(messageId: MessageId, incomingStatsInRange: (MessageId.Id, MessageId.Id) -> (count: Int, holes: Bool), topMessageId: MessageId.Id?) -> (CombinedPeerReadState?, Bool) { + if let states = self.get(messageId.peerId), state = states.namespaces[messageId.namespace] { + if traceReadStates { + print("[ReadStateTable] applyMaxReadId peerId: \(messageId.peerId), maxReadId: \(messageId.id) (before: \(states.namespaces))") + } + + if state.maxReadId < messageId.id || messageId.id == topMessageId { + var (deltaCount, holes) = incomingStatsInRange(state.maxReadId + 1, messageId.id) - states.namespaces[namespace] = PeerReadState(maxReadId: maxReadId, maxKnownId: max(state.maxKnownId, maxReadId), count: state.count - Int32(deltaCount)) - self.updatedPeerIds.insert(peerId) + if traceReadStates { + print("[ReadStateTable] applyMaxReadId after deltaCount: \(deltaCount), holes: \(holes)") + } + + if messageId.id == topMessageId { + if deltaCount != Int(state.count) { + deltaCount = Int(state.count) + holes = true + } + } + + states.namespaces[messageId.namespace] = PeerReadState(maxReadId: messageId.id, maxKnownId: state.maxKnownId, count: state.count - Int32(deltaCount)) + self.updatedPeerIds.insert(messageId.peerId) return (CombinedPeerReadState(states: states.namespaces.map({$0})), holes) } } else { @@ -190,29 +231,14 @@ final class MessageHistoryReadStateTable: Table { return (nil, false) } - func clearUnreadLocally(peerId: PeerId, topId: (PeerId, MessageId.Namespace) -> MessageId.Id?) -> CombinedPeerReadState? { - if let states = self.get(peerId) { - var updatedNamespaces: [MessageId.Namespace: PeerReadState] = [:] - var updated = false - for (namespace, state) in states.namespaces { - if let topMessageId = topId(peerId, namespace) { - let updatedState = PeerReadState(maxReadId: topMessageId, maxKnownId: topMessageId, count: 0) - if updatedState != state { - updated = true - } - updatedNamespaces[namespace] = updatedState - } else { - let updatedState = PeerReadState(maxReadId: state.maxReadId, maxKnownId: state.maxKnownId, count: 0) - updated = true - } - } - if updated { - self.updatedPeerIds.insert(peerId) - return CombinedPeerReadState(states: states.namespaces.map({$0})) - } + func applyInteractiveMaxReadId(messageId: MessageId, incomingStatsInRange: (MessageId.Id, MessageId.Id) -> (count: Int, holes: Bool), topMessageId: MessageId.Id?) -> (combinedState: CombinedPeerReadState?, ApplyInteractiveMaxReadIdResult) { + let (combinedState, holes) = self.applyMaxReadId(messageId, incomingStatsInRange: incomingStatsInRange, topMessageId: topMessageId) + + if let combinedState = combinedState { + return (combinedState, .Push(thenSync: holes)) } - return nil + return (combinedState, holes ? .Push(thenSync: true) : .None) } override func beforeCommit() { diff --git a/Postbox/MessageHistoryTable.swift b/Postbox/MessageHistoryTable.swift index 4cc285612c..d83f01ef6c 100644 --- a/Postbox/MessageHistoryTable.swift +++ b/Postbox/MessageHistoryTable.swift @@ -7,6 +7,11 @@ enum MessageHistoryOperation { case UpdateReadState(CombinedPeerReadState) } +struct MessageHistoryAnchorIndex { + let index: MessageIndex + let exact: Bool +} + enum IntermediateMessageHistoryEntry { case Message(IntermediateMessage) case Hole(MessageHistoryHole) @@ -42,16 +47,16 @@ final class MessageHistoryTable: Table { let unsentTable: MessageHistoryUnsentTable let tagsTable: MessageHistoryTagsTable let readStateTable: MessageHistoryReadStateTable - let invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable + let synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable - init(valueBox: ValueBox, tableId: Int32, messageHistoryIndexTable: MessageHistoryIndexTable, messageMediaTable: MessageMediaTable, historyMetadataTable: MessageHistoryMetadataTable, unsentTable: MessageHistoryUnsentTable, tagsTable: MessageHistoryTagsTable, readStateTable: MessageHistoryReadStateTable, invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable) { + init(valueBox: ValueBox, tableId: Int32, messageHistoryIndexTable: MessageHistoryIndexTable, messageMediaTable: MessageMediaTable, historyMetadataTable: MessageHistoryMetadataTable, unsentTable: MessageHistoryUnsentTable, tagsTable: MessageHistoryTagsTable, readStateTable: MessageHistoryReadStateTable, synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable) { self.messageHistoryIndexTable = messageHistoryIndexTable self.messageMediaTable = messageMediaTable self.historyMetadataTable = historyMetadataTable self.unsentTable = unsentTable self.tagsTable = tagsTable self.readStateTable = readStateTable - self.invalidatedReadStateTable = invalidatedReadStateTable + self.synchronizeReadStateTable = synchronizeReadStateTable super.init(valueBox: valueBox, tableId: tableId) } @@ -106,7 +111,7 @@ final class MessageHistoryTable: Table { return dict } - private func processIndexOperations(peerId: PeerId, operations: [MessageHistoryIndexOperation], inout processedOperationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + private func processIndexOperations(peerId: PeerId, operations: [MessageHistoryIndexOperation], inout processedOperationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { let sharedKey = self.key(MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: 0), timestamp: 0)) let sharedBuffer = WriteBuffer() let sharedEncoder = Encoder() @@ -188,7 +193,7 @@ final class MessageHistoryTable: Table { outputOperations.append(.UpdateReadState(combinedState)) } if invalidate { - self.invalidatedReadStateTable.add(peerId, operations: &invalidatedReadStateOperations) + self.synchronizeReadStateTable.set(peerId, operation: .Validate, operations: &updatedPeerReadStateOperations) } } @@ -213,26 +218,26 @@ final class MessageHistoryTable: Table { return internalStoreMessages } - func addMessages(messages: [StoreMessage], location: AddMessagesLocation, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + func addMessages(messages: [StoreMessage], location: AddMessagesLocation, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { let messagesByPeerId = self.messagesGroupedByPeerId(messages) for (peerId, peerMessages) in messagesByPeerId { var operations: [MessageHistoryIndexOperation] = [] self.messageHistoryIndexTable.addMessages(self.internalStoreMessages(peerMessages), location: location, operations: &operations) - self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) + self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } } - func addHoles(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + func addHoles(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { for (peerId, messageIds) in self.messageIdsByPeerId(messageIds) { var operations: [MessageHistoryIndexOperation] = [] for id in messageIds { self.messageHistoryIndexTable.addHole(id, operations: &operations) } - self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) + self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } } - func removeMessages(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + func removeMessages(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { for (peerId, messageIds) in self.messageIdsByPeerId(messageIds) { var operations: [MessageHistoryIndexOperation] = [] @@ -243,7 +248,7 @@ final class MessageHistoryTable: Table { for id in messageIds { self.messageHistoryIndexTable.removeMessage(id, operations: &operations) } - self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) + self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) if let combinedState = combinedState { var outputOperations: [MessageHistoryOperation] = [] @@ -257,24 +262,24 @@ final class MessageHistoryTable: Table { } if invalidate { - self.invalidatedReadStateTable.add(peerId, operations: &invalidatedReadStateOperations) + self.synchronizeReadStateTable.set(peerId, operation: .Validate, operations: &updatedPeerReadStateOperations) } } } - func fillHole(id: MessageId, fillType: HoleFillType, tagMask: MessageTags?, messages: [StoreMessage], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + func fillHole(id: MessageId, fillType: HoleFill, tagMask: MessageTags?, messages: [StoreMessage], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { var operations: [MessageHistoryIndexOperation] = [] self.messageHistoryIndexTable.fillHole(id, fillType: fillType, tagMask: tagMask, messages: self.internalStoreMessages(messages), operations: &operations) - self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) + self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } - func updateMessage(id: MessageId, message: StoreMessage, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + func updateMessage(id: MessageId, message: StoreMessage, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { var operations: [MessageHistoryIndexOperation] = [] self.messageHistoryIndexTable.updateMessage(id, message: self.internalStoreMessages([message]).first!, operations: &operations) - self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) + self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } - func resetIncomingReadStates(states: [PeerId: [MessageId.Namespace: PeerReadState]], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + func resetIncomingReadStates(states: [PeerId: [MessageId.Namespace: PeerReadState]], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { for (peerId, namespaces) in states { if let combinedState = self.readStateTable.resetStates(peerId, namespaces: namespaces) { if operationsByPeerId[peerId] == nil { @@ -283,14 +288,19 @@ final class MessageHistoryTable: Table { operationsByPeerId[peerId]!.append(.UpdateReadState(combinedState)) } } - self.invalidatedReadStateTable.remove(peerId, operations: &invalidatedReadStateOperations) + self.synchronizeReadStateTable.set(peerId, operation: nil, operations: &updatedPeerReadStateOperations) } } - func applyIncomingReadMaxId(messageId: MessageId, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { - let (combinedState, invalidated) = self.readStateTable.applyMaxReadId(messageId.peerId, namespace: messageId.namespace, maxReadId: messageId.id, maxKnownId: messageId.id, incomingStatsInRange: { fromId, toId in + func applyIncomingReadMaxId(messageId: MessageId, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { + var topMessageId: MessageId.Id? + if let topEntry = self.messageHistoryIndexTable.top(messageId.peerId, namespace: messageId.namespace), case let .Message(index) = topEntry { + topMessageId = index.id.id + } + + let (combinedState, invalidated) = self.readStateTable.applyMaxReadId(messageId, incomingStatsInRange: { fromId, toId in return self.messageHistoryIndexTable.incomingMessageCountInRange(messageId.peerId, namespace: messageId.namespace, minId: fromId, maxId: toId) - }) + }, topMessageId: topMessageId) if let combinedState = combinedState { if operationsByPeerId[messageId.peerId] == nil { @@ -301,7 +311,33 @@ final class MessageHistoryTable: Table { } if invalidated { - self.invalidatedReadStateTable.add(messageId.peerId, operations: &invalidatedReadStateOperations) + self.synchronizeReadStateTable.set(messageId.peerId, operation: .Validate, operations: &updatedPeerReadStateOperations) + } + } + + func applyInteractiveMaxReadId(messageId: MessageId, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { + var topMessageId: MessageId.Id? + if let topEntry = self.messageHistoryIndexTable.top(messageId.peerId, namespace: messageId.namespace), case let .Message(index) = topEntry { + topMessageId = index.id.id + } + + let (combinedState, result) = self.readStateTable.applyInteractiveMaxReadId(messageId, incomingStatsInRange: { fromId, toId in + return self.messageHistoryIndexTable.incomingMessageCountInRange(messageId.peerId, namespace: messageId.namespace, minId: fromId, maxId: toId) + }, topMessageId: topMessageId) + + if let combinedState = combinedState { + if operationsByPeerId[messageId.peerId] == nil { + operationsByPeerId[messageId.peerId] = [.UpdateReadState(combinedState)] + } else { + operationsByPeerId[messageId.peerId]!.append(.UpdateReadState(combinedState)) + } + } + + switch result { + case let .Push(thenSync): + self.synchronizeReadStateTable.set(messageId.peerId, operation: .Push(thenSync: thenSync), operations: &updatedPeerReadStateOperations) + case .None: + break } } @@ -1222,6 +1258,30 @@ final class MessageHistoryTable: Table { return entries } + func maxReadIndex(peerId: PeerId) -> MessageHistoryAnchorIndex? { + if let combinedState = self.readStateTable.getCombinedState(peerId), state = combinedState.states.first where state.1.count != 0 { + return self.anchorIndex(MessageId(peerId: peerId, namespace: state.0, id: state.1.maxReadId)) + } + return nil + } + + func anchorIndex(messageId: MessageId) -> MessageHistoryAnchorIndex? { + let (lower, upper) = self.messageHistoryIndexTable.adjacentItems(messageId, bindUpper: false) + if let lower = lower, case let .Hole(hole) = lower where messageId.id >= hole.min && messageId.id <= hole.maxIndex.id.id { + return MessageHistoryAnchorIndex(index: MessageIndex(id: messageId, timestamp: lower.index.timestamp), exact: false) + } + if let upper = upper, case let .Hole(hole) = upper where messageId.id >= hole.min && messageId.id <= hole.maxIndex.id.id { + return MessageHistoryAnchorIndex(index: MessageIndex(id: messageId, timestamp: upper.index.timestamp), exact: false) + } + + if let lower = lower { + return MessageHistoryAnchorIndex(index: MessageIndex(id: messageId, timestamp: lower.index.timestamp), exact: true) + } else if let upper = upper { + return MessageHistoryAnchorIndex(index: MessageIndex(id: messageId, timestamp: upper.index.timestamp), exact: true) + } + return nil + } + func debugList(peerId: PeerId, peerTable: PeerTable) -> [RenderedMessageHistoryEntry] { return self.laterEntries(peerId, index: nil, count: 1000).map({ entry -> RenderedMessageHistoryEntry in switch entry { diff --git a/Postbox/MessageHistoryView.swift b/Postbox/MessageHistoryView.swift index 12e2b49ff9..bf7b27b039 100644 --- a/Postbox/MessageHistoryView.swift +++ b/Postbox/MessageHistoryView.swift @@ -1,5 +1,25 @@ import Foundation +public struct MessageHistoryViewId: Equatable { + let peerId: PeerId + let id: Int + let version: Int + + init(peerId: PeerId, id: Int, version: Int = 0) { + self.peerId = peerId + self.id = id + self.version = version + } + + var nextVersion: MessageHistoryViewId { + return MessageHistoryViewId(peerId: self.peerId, id: self.id, version: self.version + 1) + } +} + +public func ==(lhs: MessageHistoryViewId, rhs: MessageHistoryViewId) -> Bool { + return lhs.peerId == rhs.peerId && lhs.id == rhs.id && lhs.version == rhs.version +} + enum MutableMessageHistoryEntry { case IntermediateMessageEntry(IntermediateMessage) case MessageEntry(Message) @@ -68,23 +88,109 @@ final class MutableMessageHistoryViewReplayContext { } final class MutableMessageHistoryView { + private(set) var id: MessageHistoryViewId let tagMask: MessageTags? - private var combinedReadState: CombinedPeerReadState? - private let count: Int + private var anchorIndex: MessageHistoryAnchorIndex + private let combinedReadState: CombinedPeerReadState? private var earlier: MutableMessageHistoryEntry? private var later: MutableMessageHistoryEntry? private var entries: [MutableMessageHistoryEntry] + private let fillCount: Int - init(combinedReadState: CombinedPeerReadState?, earlier: MutableMessageHistoryEntry?, entries: [MutableMessageHistoryEntry], later: MutableMessageHistoryEntry?, tagMask: MessageTags?, count: Int) { + init(id: MessageHistoryViewId, anchorIndex: MessageHistoryAnchorIndex, combinedReadState: CombinedPeerReadState?, earlier: MutableMessageHistoryEntry?, entries: [MutableMessageHistoryEntry], later: MutableMessageHistoryEntry?, tagMask: MessageTags?, count: Int) { + self.id = id + self.anchorIndex = anchorIndex self.combinedReadState = combinedReadState self.earlier = earlier self.entries = entries self.later = later self.tagMask = tagMask - self.count = count + self.fillCount = count } - func replay(operations: [MessageHistoryOperation], context: MutableMessageHistoryViewReplayContext) -> Bool { + func incrementVersion() { + self.id = self.id.nextVersion + } + + func updateVisibleRange(earliestVisibleIndex earliestVisibleIndex: MessageIndex, latestVisibleIndex: MessageIndex, context: MutableMessageHistoryViewReplayContext) -> Bool { + if (true) { + //return false + } + + var minIndex: Int? + var maxIndex: Int? + + for i in 0 ..< self.entries.count { + if self.entries[i].index >= earliestVisibleIndex { + minIndex = i + break + } + } + + for i in (0 ..< self.entries.count).reverse() { + if self.entries[i].index <= latestVisibleIndex { + maxIndex = i + break + } + } + + if let minIndex = minIndex, maxIndex = maxIndex { + var minClipIndex = minIndex + var maxClipIndex = maxIndex + + while maxClipIndex - minClipIndex <= self.fillCount { + if maxClipIndex != self.entries.count - 1 { + maxClipIndex += 1 + } + + if minClipIndex != 0 { + minClipIndex -= 1 + } else if maxClipIndex == self.entries.count - 1 { + break + } + } + + if minClipIndex != 0 || maxClipIndex != self.entries.count - 1 { + if minClipIndex != 0 { + self.earlier = self.entries[minClipIndex - 1] + } + + if maxClipIndex != self.entries.count - 1 { + self.later = self.entries[maxClipIndex + 1] + } + + for _ in 0 ..< self.entries.count - 1 - maxClipIndex { + /*if case let .MessageEntry(message) = self.entries.last! { + print("remove last \(message.text)") + }*/ + self.entries.removeLast() + } + + for _ in 0 ..< minClipIndex { + /*if case let .MessageEntry(message) = self.entries.first! { + print("remove first \(message.text)") + }*/ + self.entries.removeFirst() + } + + return true + } + } + + return false + } + + func updateAnchorIndex(getIndex: (MessageId) -> MessageHistoryAnchorIndex?) -> Bool { + if !self.anchorIndex.exact { + if let index = getIndex(self.anchorIndex.index.id) { + self.anchorIndex = index + return true + } + } + return false + } + + func replay(operations: [MessageHistoryOperation], holeFillDirections: [MessageIndex: HoleFillDirection], context: MutableMessageHistoryViewReplayContext) -> Bool { let tagMask = self.tagMask let unwrappedTagMask: UInt32 = tagMask?.rawValue ?? 0 @@ -93,13 +199,13 @@ final class MutableMessageHistoryView { switch operation { case let .InsertHole(hole): if tagMask == nil || (hole.tags & unwrappedTagMask) != 0 { - if self.add(.HoleEntry(hole)) { + if self.add(.HoleEntry(hole), holeFillDirections: holeFillDirections) { hasChanges = true } } case let .InsertMessage(intermediateMessage): if tagMask == nil || (intermediateMessage.tags.rawValue & unwrappedTagMask) != 0 { - if self.add(.IntermediateMessageEntry(intermediateMessage)) { + if self.add(.IntermediateMessageEntry(intermediateMessage), holeFillDirections: holeFillDirections) { hasChanges = true } } @@ -109,20 +215,20 @@ final class MutableMessageHistoryView { } case let .UpdateReadState(combinedReadState): hasChanges = true - self.combinedReadState = combinedReadState + //self.combinedReadState = combinedReadState } } return hasChanges } - private func add(entry: MutableMessageHistoryEntry) -> Bool { + private func add(entry: MutableMessageHistoryEntry, holeFillDirections: [MessageIndex: HoleFillDirection]) -> Bool { if self.entries.count == 0 { self.entries.append(entry) return true } else { - let first = self.entries[self.entries.count - 1].index - let last = self.entries[0].index + let latestIndex = self.entries[self.entries.count - 1].index + let earliestIndex = self.entries[0].index var next: MessageIndex? if let later = self.later { @@ -131,38 +237,26 @@ final class MutableMessageHistoryView { let index = entry.index - if index < last { + if index < earliestIndex { if self.earlier == nil || self.earlier!.index < index { - if self.entries.count < self.count { - self.entries.insert(entry, atIndex: 0) - } else { - self.earlier = entry - } + self.entries.insert(entry, atIndex: 0) return true } else { return false } - } else if index > first { - if next != nil && index > next! { - if self.later == nil || self.later!.index > index { - if self.entries.count < self.count { - self.entries.append(entry) - } else { - self.later = entry - } + } else if index > latestIndex { + if let later = self.later { + if index < later.index { + self.entries.append(entry) return true } else { return false } } else { self.entries.append(entry) - if self.entries.count > self.count { - self.earlier = self.entries[0] - self.entries.removeAtIndex(0) - } return true } - } else if index != last && index != first { + } else if index != earliestIndex && index != latestIndex { var i = self.entries.count while i >= 1 { if self.entries[i - 1].index < index { @@ -171,10 +265,6 @@ final class MutableMessageHistoryView { i -= 1 } self.entries.insert(entry, atIndex: i) - if self.entries.count > self.count { - self.earlier = self.entries[0] - self.entries.removeAtIndex(0) - } return true } else { return false @@ -214,64 +304,45 @@ final class MutableMessageHistoryView { } func complete(context: MutableMessageHistoryViewReplayContext, fetchEarlier: (MessageIndex?, Int) -> [MutableMessageHistoryEntry], fetchLater: (MessageIndex?, Int) -> [MutableMessageHistoryEntry]) { - if context.removedEntries { - var addedEntries: [MutableMessageHistoryEntry] = [] - - var latestAnchor: MessageIndex? - if let last = self.entries.last { - latestAnchor = last.index - } - - if latestAnchor == nil { - if let later = self.later { - latestAnchor = later.index - } - } - - if let later = self.later { - addedEntries += fetchLater(later.index.predecessor(), self.count) - } - if let earlier = self.earlier { - addedEntries += fetchEarlier(earlier.index.successor(), self.count) - } - - addedEntries += self.entries - addedEntries.sortInPlace({ $0.index < $1.index }) - var i = addedEntries.count - 1 - while i >= 1 { - if addedEntries[i].index.id == addedEntries[i - 1].index.id { - addedEntries.removeAtIndex(i) - } - i -= 1 - } - self.entries = [] - - var anchorIndex = addedEntries.count - 1 - if let latestAnchor = latestAnchor { - var i = addedEntries.count - 1 - while i >= 0 { - if addedEntries[i].index <= latestAnchor { - anchorIndex = i - break + if context.removedEntries && self.entries.count < self.fillCount { + if self.entries.count == 0 { + let anchorIndex = (self.later ?? self.earlier)?.index + + let fetchedEntries = fetchEarlier(anchorIndex, self.fillCount + 2) + if fetchedEntries.count >= self.fillCount + 2 { + self.earlier = fetchedEntries.last + for i in (1 ..< fetchedEntries.count - 1).reverse() { + self.entries.append(fetchedEntries[i]) } - i -= 1 + self.later = fetchedEntries.first + } + } else { + let fetchedEntries = fetchEarlier(self.entries[0].index, self.fillCount - self.entries.count) + for entry in fetchedEntries { + self.entries.insert(entry, atIndex: 0) + } + + if context.invalidEarlier { + var earlyId: MessageIndex? + let i = 0 + if i < self.entries.count { + earlyId = self.entries[i].index + } + + let earlierEntries = fetchEarlier(earlyId, 1) + self.earlier = earlierEntries.first + } + + if context.invalidLater { + var laterId: MessageIndex? + let i = self.entries.count - 1 + if i >= 0 { + laterId = self.entries[i].index + } + + let laterEntries = fetchLater(laterId, 1) + self.later = laterEntries.first } - } - - self.later = nil - if anchorIndex + 1 < addedEntries.count { - self.later = addedEntries[anchorIndex + 1] - } - - i = anchorIndex - while i >= 0 && i > anchorIndex - self.count { - self.entries.insert(addedEntries[i], atIndex: 0) - i -= 1 - } - - self.earlier = nil - if anchorIndex - self.count >= 0 { - self.earlier = addedEntries[anchorIndex - self.count] } } else { if context.invalidEarlier { @@ -313,24 +384,68 @@ final class MutableMessageHistoryView { } } - func firstHole() -> MessageHistoryHole? { - for entry in self.entries.reverse() as ReverseCollection { - if case let .HoleEntry(hole) = entry { - return hole + func firstHole() -> (MessageHistoryHole, HoleFillDirection)? { + if self.entries.isEmpty { + return nil + } + + var referenceIndex = self.entries.count - 1 + for i in 0 ..< self.entries.count { + if self.entries[i].index >= self.anchorIndex.index { + referenceIndex = i + break } } + var i = referenceIndex + var j = referenceIndex + 1 + + while i >= 0 || j < self.entries.count { + if j < self.entries.count { + if case let .HoleEntry(hole) = self.entries[j] { + if self.anchorIndex.index.id.namespace == hole.id.namespace { + if self.anchorIndex.index.id.id >= hole.min && self.anchorIndex.index.id.id <= hole.maxIndex.id.id { + return (hole, .AroundIndex(self.anchorIndex.index)) + } + } + + return (hole, hole.maxIndex <= self.anchorIndex.index ? .UpperToLower : .LowerToUpper) + } + } + + if i >= 0 { + if case let .HoleEntry(hole) = self.entries[i] { + if self.anchorIndex.index.id.namespace == hole.id.namespace { + if self.anchorIndex.index.id.id >= hole.min && self.anchorIndex.index.id.id <= hole.maxIndex.id.id { + return (hole, .AroundIndex(self.anchorIndex.index)) + } + } + + return (hole, hole.maxIndex <= self.anchorIndex.index ? .UpperToLower : .LowerToUpper) + } + } + + i -= 1 + j += 1 + } + return nil } } public final class MessageHistoryView { + public let id: MessageHistoryViewId + public let anchorIndex: MessageIndex public let earlierId: MessageIndex? public let laterId: MessageIndex? public let entries: [MessageHistoryEntry] public let maxReadIndex: MessageIndex? + public let combinedReadState: CombinedPeerReadState? init(_ mutableView: MutableMessageHistoryView) { + self.id = mutableView.id + self.anchorIndex = mutableView.anchorIndex.index + var entries: [MessageHistoryEntry] = [] for entry in mutableView.entries { switch entry { @@ -347,16 +462,42 @@ public final class MessageHistoryView { self.earlierId = mutableView.earlier?.index self.laterId = mutableView.later?.index - if let combinedReadState = mutableView.combinedReadState { + self.combinedReadState = mutableView.combinedReadState + + if let combinedReadState = mutableView.combinedReadState where combinedReadState.count != 0 { var maxIndex: MessageIndex? for (namespace, state) in combinedReadState.states { - for entry in entries { - if entry.index.id.namespace == namespace { - if maxIndex == nil || maxIndex! < entry.index { - maxIndex = entry.index + var maxNamespaceIndex: MessageIndex? + var index = entries.count - 1 + for entry in entries.reverse() { + if entry.index.id.namespace == namespace && entry.index.id.id <= state.maxReadId { + maxNamespaceIndex = entry.index + break + } + index -= 1 + } + if maxNamespaceIndex == nil && index == -1 && entries.count != 0 { + index = 0 + for entry in entries { + if entry.index.id.namespace == namespace { + maxNamespaceIndex = entry.index + break + } + index += 1 + } + } + if let _ = maxNamespaceIndex where index + 1 < entries.count { + for i in index + 1 ..< entries.count { + if case let .MessageEntry(message) = entries[i] where !message.flags.contains(.Incoming) { + maxNamespaceIndex = MessageIndex(message) + } else { + break } } } + if let maxNamespaceIndex = maxNamespaceIndex where maxIndex == nil || maxIndex! < maxNamespaceIndex { + maxIndex = maxNamespaceIndex + } } self.maxReadIndex = maxIndex } else { diff --git a/Postbox/PeerReadState.swift b/Postbox/PeerReadState.swift index a8a66588c4..ab58e09fef 100644 --- a/Postbox/PeerReadState.swift +++ b/Postbox/PeerReadState.swift @@ -1,5 +1,5 @@ -public struct PeerReadState: Equatable { +public struct PeerReadState: Equatable, CustomStringConvertible { public let maxReadId: MessageId.Id public let maxKnownId: MessageId.Id public let count: Int32 @@ -9,12 +9,23 @@ public struct PeerReadState: Equatable { self.maxKnownId = maxKnownId self.count = count } + + public var description: String { + return "(PeerReadState maxReadId: \(maxReadId), maxKnownId: \(maxKnownId), count: \(count))" + } } public func ==(lhs: PeerReadState, rhs: PeerReadState) -> Bool { return lhs.maxReadId == rhs.maxReadId && lhs.maxKnownId == rhs.maxKnownId && lhs.count == rhs.count } -struct CombinedPeerReadState { +public struct CombinedPeerReadState { let states: [(MessageId.Namespace, PeerReadState)] + var count: Int32 { + var result: Int32 = 0 + for (_, state) in self.states { + result += state.count + } + return result + } } diff --git a/Postbox/Postbox.swift b/Postbox/Postbox.swift index 190a3a50db..aa7d968adc 100644 --- a/Postbox/Postbox.swift +++ b/Postbox/Postbox.swift @@ -2,6 +2,11 @@ import Foundation import SwiftSignalKit import sqlcipher +public enum PreloadedMessageHistoryView { + case Loading + case Preloaded(MessageHistoryView) +} + public protocol PeerChatState: Coding { func equals(other: PeerChatState) -> Bool } @@ -21,7 +26,7 @@ public final class Modifier { self.postbox?.addHole(messageId) } - public func fillHole(hole: MessageHistoryHole, fillType: HoleFillType, tagMask: MessageTags?, messages: [StoreMessage]) { + public func fillHole(hole: MessageHistoryHole, fillType: HoleFill, tagMask: MessageTags?, messages: [StoreMessage]) { self.postbox?.fillHole(hole, fillType: fillType, tagMask: tagMask, messages: messages) } @@ -44,12 +49,16 @@ public final class Modifier { self.postbox?.resetIncomingReadStates(states) } + public func confirmSynchronizedIncomingReadState(peerId: PeerId) { + self.postbox?.confirmSynchronizedIncomingReadState(peerId) + } + public func applyIncomingReadMaxId(messageId: MessageId) { self.postbox?.applyIncomingReadMaxId(messageId) } - public func validateIncomingReadState(peerId: PeerId) { - self.postbox?.validateIncomingReadState(peerId) + public func applyInteractiveReadMaxId(messageId: MessageId) { + self.postbox?.applyInteractiveReadMaxId(messageId) } public func getState() -> Coding? { @@ -72,6 +81,10 @@ public final class Modifier { return self.postbox?.peerTable.get(id) } + public func getPeerReadStates(id: PeerId) -> [(MessageId.Namespace, PeerReadState)]? { + return self.postbox?.readStateTable.getCombinedState(id)?.states + } + public func updatePeers(peers: [Peer], update: (Peer, Peer) -> Peer?) { self.postbox?.updatePeers(peers, update: update) } @@ -97,14 +110,16 @@ public final class Postbox { private var valueBox: ValueBox! private var viewTracker: ViewTracker! + private var nextViewId = 0 private var peerPipes: [PeerId: Pipe] = [:] private var currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] private var currentUnsentOperations: [IntermediateMessageHistoryUnsentOperation] = [] - private var currentInvalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation] = [] + private var currentUpdatedSynchronizeReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] - private var currentFilledHolesByPeerId = Set() + private var currentRemovedHolesByPeerId: [PeerId: [MessageIndex: HoleFillDirection]] = [:] + private var currentFilledHolesByPeerId: [PeerId: [MessageIndex: HoleFillDirection]] = [:] private var currentUpdatedPeers: [PeerId: Peer] = [:] private var currentReplaceChatListHoles: [(MessageIndex, ChatListHole?)] = [] @@ -115,8 +130,8 @@ public final class Postbox { self.fetchChatListHoleImpl.set(single(fetch, NoError.self)) } - private let fetchMessageHistoryHoleImpl = Promise<(MessageHistoryHole, MessageTags?) -> Signal>() - public func setFetchMessageHistoryHole(fetch: (MessageHistoryHole, MessageTags?) -> Signal) { + private let fetchMessageHistoryHoleImpl = Promise<(MessageHistoryHole, HoleFillDirection, MessageTags?) -> Signal>() + public func setFetchMessageHistoryHole(fetch: (MessageHistoryHole, HoleFillDirection, MessageTags?) -> Signal) { self.fetchMessageHistoryHoleImpl.set(single(fetch, NoError.self)) } @@ -125,9 +140,9 @@ public final class Postbox { self.sendUnsentMessageImpl.set(single(sendUnsentMessage, NoError.self)) } - private let validatePeerReadStateImpl = Promise Signal>() - public func setValidatePeerReadState(validatePeerReadState: PeerId -> Signal) { - self.validatePeerReadStateImpl.set(.single(validatePeerReadState)) + private let synchronizePeerReadStateImpl = Promise<(PeerId, PeerReadStateSynchronizationOperation) -> Signal>() + public func setSynchronizePeerReadState(synchronizePeerReadState: (PeerId, PeerReadStateSynchronizationOperation) -> Signal) { + self.synchronizePeerReadStateImpl.set(.single(synchronizePeerReadState)) } public let mediaBox: MediaBox @@ -149,7 +164,7 @@ public final class Postbox { var messageHistoryTagsTable: MessageHistoryTagsTable! var peerChatStateTable: PeerChatStateTable! var readStateTable: MessageHistoryReadStateTable! - var invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable! + var synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable! public init(basePath: String, globalMessageIdsNamespace: MessageId.Namespace, seedConfiguration: SeedConfiguration) { self.basePath = basePath @@ -163,7 +178,7 @@ public final class Postbox { } private func debugSaveState(name: String) { - self.queue.dispatch { + self.queue.justDispatch({ let path = self.basePath + name let _ = try? NSFileManager.defaultManager().removeItemAtPath(path) do { @@ -171,11 +186,11 @@ public final class Postbox { } catch (let e) { print("(Postbox debugSaveState: error \(e))") } - } + }) } private func debugRestoreState(name: String) { - self.queue.dispatch { + self.queue.justDispatch({ let path = self.basePath + name let _ = try? NSFileManager.defaultManager().removeItemAtPath(self.basePath) do { @@ -183,11 +198,11 @@ public final class Postbox { } catch (let e) { print("(Postbox debugRestoreState: error \(e))") } - } + }) } private func openDatabase() { - self.queue.dispatch { + self.queue.justDispatch({ let startTime = CFAbsoluteTimeGetCurrent() do { @@ -196,15 +211,26 @@ public final class Postbox { } //let _ = try? NSFileManager.defaultManager().removeItemAtPath(self.basePath + "/media") - //self.debugSaveState("beforeGetDiff") - //self.debugRestoreState("beforeGetDiff") - //self.debugRestoreState("clean") + + //#if TARGET_IPHONE_SIMULATOR + + //self.debugRestoreState("_empty") + + // debugging large amount of updates + //self.debugSaveState("beforeHoles") + //self.debugRestoreState("beforeHoles") + + // debugging unread counters + //self.debugSaveState("afterLogin") + //self.debugRestoreState("afterLogin") + + //#endif self.valueBox = SqliteValueBox(basePath: self.basePath + "/db") self.metadataTable = MetadataTable(valueBox: self.valueBox, tableId: 0) let userVersion: Int32? = self.metadataTable.userVersion() - let currentUserVersion: Int32 = 1 + let currentUserVersion: Int32 = 3 if userVersion != currentUserVersion { self.valueBox.drop() @@ -221,8 +247,8 @@ public final class Postbox { self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox, tableId: 5) self.mediaTable = MessageMediaTable(valueBox: self.valueBox, tableId: 6, mediaCleanupTable: self.mediaCleanupTable) self.readStateTable = MessageHistoryReadStateTable(valueBox: self.valueBox, tableId: 14) - self.invalidatedReadStateTable = MessageHistoryInvalidatedReadStateTable(valueBox: self.valueBox, tableId: 15) - self.messageHistoryTable = MessageHistoryTable(valueBox: self.valueBox, tableId: 7, messageHistoryIndexTable: self.messageHistoryIndexTable, messageMediaTable: self.mediaTable, historyMetadataTable: self.messageHistoryMetadataTable, unsentTable: self.messageHistoryUnsentTable!, tagsTable: self.messageHistoryTagsTable, readStateTable: self.readStateTable, invalidatedReadStateTable: self.invalidatedReadStateTable!) + self.synchronizeReadStateTable = MessageHistorySynchronizeReadStateTable(valueBox: self.valueBox, tableId: 15) + self.messageHistoryTable = MessageHistoryTable(valueBox: self.valueBox, tableId: 7, messageHistoryIndexTable: self.messageHistoryIndexTable, messageMediaTable: self.mediaTable, historyMetadataTable: self.messageHistoryMetadataTable, unsentTable: self.messageHistoryUnsentTable!, tagsTable: self.messageHistoryTagsTable, readStateTable: self.readStateTable, synchronizeReadStateTable: self.synchronizeReadStateTable!) self.chatListIndexTable = ChatListIndexTable(valueBox: self.valueBox, tableId: 8) self.chatListTable = ChatListTable(valueBox: self.valueBox, tableId: 9, indexTable: self.chatListIndexTable, metadataTable: self.messageHistoryMetadataTable, seedConfiguration: self.seedConfiguration) self.peerChatStateTable = PeerChatStateTable(valueBox: self.valueBox, tableId: 13) @@ -239,24 +265,30 @@ public final class Postbox { self.tables.append(self.chatListTable) self.tables.append(self.peerChatStateTable) self.tables.append(self.readStateTable) - self.tables.append(self.invalidatedReadStateTable) + self.tables.append(self.synchronizeReadStateTable) - self.viewTracker = ViewTracker(queue: self.queue, fetchEarlierHistoryEntries: self.fetchEarlierHistoryEntries, fetchLaterHistoryEntries: self.fetchLaterHistoryEntries, fetchEarlierChatEntries: self.fetchEarlierChatEntries, fetchLaterChatEntries: self.fetchLaterChatEntries, renderMessage: self.renderIntermediateMessage, fetchChatListHole: self.fetchChatListHoleWrapper, fetchMessageHistoryHole: self.fetchMessageHistoryHoleWrapper, sendUnsentMessage: self.sendUnsentMessageWrapper, unsentMessageIndices: self.messageHistoryUnsentTable!.get(), validateReadState: self.validatePeerReadStateWrapper, invalidatedReadStatePeerIds: self.invalidatedReadStateTable!.get()) + self.viewTracker = ViewTracker(queue: self.queue, fetchEarlierHistoryEntries: self.fetchEarlierHistoryEntries, fetchLaterHistoryEntries: self.fetchLaterHistoryEntries, fetchEarlierChatEntries: self.fetchEarlierChatEntries, fetchLaterChatEntries: self.fetchLaterChatEntries, fetchAnchorIndex: self.fetchAnchorIndex, renderMessage: self.renderIntermediateMessage, fetchChatListHole: self.fetchChatListHoleWrapper, fetchMessageHistoryHole: self.fetchMessageHistoryHoleWrapper, sendUnsentMessage: self.sendUnsentMessageWrapper, unsentMessageIndices: self.messageHistoryUnsentTable!.get(), synchronizeReadState: self.synchronizePeerReadStateWrapper, synchronizePeerReadStateOperations: self.synchronizeReadStateTable!.get()) print("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") - } + }) + } + + private func takeNextViewId() -> Int { + let nextId = self.nextViewId + self.nextViewId += 1 + return nextId } private var cachedState: Coding? private func setState(state: Coding) { - self.queue.dispatch { + self.queue.justDispatch({ self.cachedState = state self.metadataTable.setState(state) self.statePipe.putNext(state) - } + }) } private func getState() -> Coding? { @@ -275,12 +307,12 @@ public final class Postbox { public func state() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() - self.queue.dispatch { + self.queue.justDispatch({ subscriber.putNext(self.getState()) disposable.set(self.statePipe.signal().start(next: { next in subscriber.putNext(next) })) - } + }) return disposable } @@ -299,16 +331,51 @@ public final class Postbox { } private func addMessages(messages: [StoreMessage], location: AddMessagesLocation) { - self.messageHistoryTable.addMessages(messages, location: location, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + self.messageHistoryTable.addMessages(messages, location: location, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) } private func addHole(id: MessageId) { - self.messageHistoryTable.addHoles([id], operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + self.messageHistoryTable.addHoles([id], operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) } - private func fillHole(hole: MessageHistoryHole, fillType: HoleFillType, tagMask: MessageTags?, messages: [StoreMessage]) { - self.messageHistoryTable.fillHole(hole.id, fillType: fillType, tagMask: tagMask, messages: messages, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) - self.currentFilledHolesByPeerId.insert(hole.id.peerId) + private func fillHole(hole: MessageHistoryHole, fillType: HoleFill, tagMask: MessageTags?, messages: [StoreMessage]) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.messageHistoryTable.fillHole(hole.id, fillType: fillType, tagMask: tagMask, messages: messages, operationsByPeerId: &operationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) + for (peerId, operations) in operationsByPeerId { + if self.currentOperationsByPeerId[peerId] == nil { + self.currentOperationsByPeerId[peerId] = operations + } else { + self.currentOperationsByPeerId[peerId]!.appendContentsOf(operations) + } + + var filledMessageIndices: [MessageIndex: HoleFillDirection] = [:] + for operation in operations { + switch operation { + case let .InsertHole(hole): + filledMessageIndices[hole.maxIndex] = fillType.direction + case let .InsertMessage(message): + filledMessageIndices[MessageIndex(message)] = fillType.direction + default: + break + } + } + + if !filledMessageIndices.isEmpty { + if self.currentFilledHolesByPeerId[peerId] == nil { + self.currentFilledHolesByPeerId[peerId] = filledMessageIndices + } else { + for (messageIndex, direction) in filledMessageIndices { + self.currentFilledHolesByPeerId[peerId]![messageIndex] = direction + } + } + } + + if self.currentRemovedHolesByPeerId[peerId] == nil { + self.currentRemovedHolesByPeerId[peerId] = [hole.maxIndex: fillType.direction] + } else { + self.currentRemovedHolesByPeerId[peerId]![hole.maxIndex] = fillType.direction + } + } } private func replaceChatListHole(index: MessageIndex, hole: ChatListHole?) { @@ -316,19 +383,23 @@ public final class Postbox { } private func deleteMessages(messageIds: [MessageId]) { - self.messageHistoryTable.removeMessages(messageIds, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + self.messageHistoryTable.removeMessages(messageIds, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) } private func resetIncomingReadStates(states: [PeerId: [MessageId.Namespace: PeerReadState]]) { - self.messageHistoryTable.resetIncomingReadStates(states, operationsByPeerId: &self.currentOperationsByPeerId, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + self.messageHistoryTable.resetIncomingReadStates(states, operationsByPeerId: &self.currentOperationsByPeerId, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) + } + + private func confirmSynchronizedIncomingReadState(peerId: PeerId) { + self.synchronizeReadStateTable.set(peerId, operation: nil, operations: &self.currentUpdatedSynchronizeReadStateOperations) } private func applyIncomingReadMaxId(messageId: MessageId) { - self.messageHistoryTable.applyIncomingReadMaxId(messageId, operationsByPeerId: &self.currentOperationsByPeerId, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + self.messageHistoryTable.applyIncomingReadMaxId(messageId, operationsByPeerId: &self.currentOperationsByPeerId, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) } - private func validateIncomingReadState(peerId: PeerId) { - self.invalidatedReadStateTable.remove(peerId, operations: &self.currentInvalidatedReadStateOperations) + private func applyInteractiveReadMaxId(messageId: MessageId) { + self.messageHistoryTable.applyInteractiveMaxReadId(messageId, operationsByPeerId: &self.currentOperationsByPeerId, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) } private func fetchEarlierHistoryEntries(peerId: PeerId, index: MessageIndex?, count: Int, tagMask: MessageTags? = nil) -> [MutableMessageHistoryEntry] { @@ -488,6 +559,10 @@ public final class Postbox { return entries } + private func fetchAnchorIndex(id: MessageId) -> MessageHistoryAnchorIndex? { + return self.messageHistoryTable.anchorIndex(id) + } + private func renderIntermediateMessage(message: IntermediateMessage) -> Message { return self.messageHistoryTable.renderMessage(message, peerTable: self.peerTable) } @@ -498,9 +573,9 @@ public final class Postbox { }).start() } - private func fetchMessageHistoryHoleWrapper(hole: MessageHistoryHole, tagMask: MessageTags?) -> Disposable { + private func fetchMessageHistoryHoleWrapper(hole: MessageHistoryHole, direction: HoleFillDirection, tagMask: MessageTags?) -> Disposable { return (self.fetchMessageHistoryHoleImpl.get() |> mapToSignal { fetch in - return fetch(hole, tagMask) + return fetch(hole, direction, tagMask) }).start() } @@ -515,9 +590,9 @@ public final class Postbox { }).start() } - private func validatePeerReadStateWrapper(peerId: PeerId) -> Disposable { - return (self.validatePeerReadStateImpl.get() |> mapToSignal { validate -> Signal in - return validate(peerId) + private func synchronizePeerReadStateWrapper(peerId: PeerId, operation: PeerReadStateSynchronizationOperation) -> Disposable { + return (self.synchronizePeerReadStateImpl.get() |> mapToSignal { validate -> Signal in + return validate(peerId, operation) }).start() } @@ -528,14 +603,15 @@ public final class Postbox { self.chatListTable.replaceHole(index, hole: hole, operations: &chatListOperations) } - self.viewTracker.updateViews(currentOperationsByPeerId: self.currentOperationsByPeerId, peerIdsWithFilledHoles: self.currentFilledHolesByPeerId, chatListOperations: chatListOperations, currentUpdatedPeers: self.currentUpdatedPeers, unsentMessageOperations: self.currentUnsentOperations, invalidatedReadStateOperations: self.currentInvalidatedReadStateOperations) + self.viewTracker.updateViews(currentOperationsByPeerId: self.currentOperationsByPeerId, peerIdsWithFilledHoles: self.currentFilledHolesByPeerId, removedHolesByPeerId: self.currentRemovedHolesByPeerId, chatListOperations: chatListOperations, currentUpdatedPeers: self.currentUpdatedPeers, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations) self.currentOperationsByPeerId.removeAll() self.currentFilledHolesByPeerId.removeAll() + self.currentRemovedHolesByPeerId.removeAll() self.currentUpdatedPeers.removeAll() self.currentReplaceChatListHoles.removeAll() self.currentUnsentOperations.removeAll() - self.currentInvalidatedReadStateOperations.removeAll() + self.currentUpdatedSynchronizeReadStateOperations.removeAll() for table in self.tables { table.beforeCommit() @@ -568,7 +644,7 @@ public final class Postbox { private func updateMessage(index: MessageIndex, update: Message -> StoreMessage) { if let intermediateMessage = self.messageHistoryTable.getMessage(index) { let message = self.renderIntermediateMessage(intermediateMessage) - self.messageHistoryTable.updateMessage(index.id, message: update(message), operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: &self.currentUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + self.messageHistoryTable.updateMessage(index.id, message: update(message), operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: &self.currentUnsentOperations, updatedPeerReadStateOperations: &self.currentUpdatedSynchronizeReadStateOperations) } } @@ -586,7 +662,8 @@ public final class Postbox { public func modify(f: Modifier -> T) -> Signal { return Signal { subscriber in - self.queue.dispatch { + self.queue.justDispatch({ + //let startTime = CFAbsoluteTimeGetCurrent() //self.valueBox.beginStats() self.valueBox.begin() let result = f(Modifier(postbox: self)) @@ -594,26 +671,104 @@ public final class Postbox { self.valueBox.commit() subscriber.putNext(result) + //print("modify \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") subscriber.putCompletion() - } + }) return EmptyDisposable } } - - public func tailMessageHistoryViewForPeerId(peerId: PeerId, count: Int, tagMask: MessageTags? = nil) -> Signal<(MessageHistoryView, ViewUpdateType), NoError> { - return self.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId), count: count, tagMask: tagMask) + + public func preloadedAroundUnreadMessageHistoryViewForPeerId(peerId: PeerId, count: Int, tagMask: MessageTags? = nil) -> Signal<(MessageHistoryView, ViewUpdateType), NoError> { + return self.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask) |> filter { view, _ in + if let maxReadIndex = view.maxReadIndex { + var targetIndex = 0 + for i in 0 ..< view.entries.count { + if view.entries[i].index >= maxReadIndex { + targetIndex = i + break + } + } + + /*print("entries:") + for i in 0 ..< view.entries.count { + switch view.entries[i] { + case let .MessageEntry(message): + print(" \(i == targetIndex ? "*" : " ")\(message.id.id): \(message.text)") + case let .HoleEntry(hole): + print(" \(i == targetIndex ? "*" : " ")\(hole.min) — \(hole.maxIndex.id.id): hole") + } + }*/ + + let maxIndex = min(view.entries.count, targetIndex + count / 2) + if maxIndex >= targetIndex { + for i in targetIndex ..< maxIndex { + if case .HoleEntry = view.entries[i] { + return false + } + } + } + + return true + } else { + return true + } + } |> take(1) |> mapToSignal { _ in + return self.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask) + } } - public func aroundMessageHistoryViewForPeerId(peerId: PeerId, index: MessageIndex, count: Int, tagMask: MessageTags? = nil) -> Signal<(MessageHistoryView, ViewUpdateType), NoError> { + public func aroundUnreadMessageHistoryViewForPeerId(peerId: PeerId, count: Int, tagMask: MessageTags? = nil) -> Signal<(MessageHistoryView, ViewUpdateType), NoError> { return Signal { subscriber in let disposable = MetaDisposable() - self.queue.dispatch { + var index = MessageHistoryAnchorIndex(index: MessageIndex.upperBound(peerId), exact: true) + if let maxReadIndex = self.messageHistoryTable.maxReadIndex(peerId) { + index = maxReadIndex + } + disposable.set(self.aroundMessageHistoryViewForPeerId(peerId, index: index.index, count: count, anchorIndex: index, unreadIndex: index.index, fixedCombinedReadState: nil, tagMask: tagMask).start(next: { next in + subscriber.putNext(next) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable + } + } + + public func aroundMessageHistoryViewForPeerId(peerId: PeerId, index: MessageIndex, count: Int, anchorIndex: MessageIndex, fixedCombinedReadState: CombinedPeerReadState?, tagMask: MessageTags? = nil) -> Signal<(MessageHistoryView, ViewUpdateType), NoError> { + return self.aroundMessageHistoryViewForPeerId(peerId, index: index, count: count, anchorIndex: MessageHistoryAnchorIndex(index: anchorIndex, exact: true), unreadIndex: nil, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) + } + + private func aroundMessageHistoryViewForPeerId(peerId: PeerId, index: MessageIndex, count: Int, anchorIndex: MessageHistoryAnchorIndex, unreadIndex: MessageIndex?, fixedCombinedReadState: CombinedPeerReadState?, tagMask: MessageTags? = nil) -> Signal<(MessageHistoryView, ViewUpdateType), NoError> { + return Signal { subscriber in + let disposable = MetaDisposable() + let startTime = CFAbsoluteTimeGetCurrent() + self.queue.justDispatch({ + //print("+ queue \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") let (entries, earlier, later) = self.fetchAroundHistoryEntries(index, count: count, tagMask: tagMask) + print("aroundMessageHistoryViewForPeerId fetchAroundHistoryEntries \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") - let mutableView = MutableMessageHistoryView(combinedReadState: self.readStateTable.getCombinedState(peerId), earlier: earlier, entries: entries, later: later, tagMask: tagMask, count: count) + var adjustedAnchorIndex = anchorIndex + /*if entries.count != 0 { + let minIndex = entries[0].index + if anchorIndex < minIndex { + //adjustedAnchorIndex = minIndex + } + }*/ + + let mutableView = MutableMessageHistoryView(id: MessageHistoryViewId(peerId: peerId, id: self.takeNextViewId()), anchorIndex: adjustedAnchorIndex, combinedReadState: fixedCombinedReadState ?? self.readStateTable.getCombinedState(peerId), earlier: earlier, entries: entries, later: later, tagMask: tagMask, count: count) mutableView.render(self.renderIntermediateMessage) - subscriber.putNext((MessageHistoryView(mutableView), .Generic)) + //print("+ render \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + + let initialUpdateType: ViewUpdateType + if let unreadIndex = unreadIndex { + initialUpdateType = .InitialUnread(unreadIndex) + } else { + initialUpdateType = .Generic + } + subscriber.putNext((MessageHistoryView(mutableView), initialUpdateType)) let (index, signal) = self.viewTracker.addMessageHistoryView(peerId, view: mutableView) @@ -631,6 +786,27 @@ public final class Postbox { } return }) + }) + + return disposable + } + } + + public func messageIndexAtId(id: MessageId) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.queue.justDispatch { + if let entry = self.messageHistoryIndexTable.get(id), case let .Message(index) = entry { + subscriber.putNext(index) + subscriber.putCompletion() + } else if let hole = self.messageHistoryIndexTable.holeContainingId(id) { + subscriber.putNext(nil) + subscriber.putCompletion() + } else { + subscriber.putNext(nil) + subscriber.putCompletion() + } } return disposable @@ -645,7 +821,7 @@ public final class Postbox { return Signal { subscriber in let disposable = MetaDisposable() - self.queue.dispatch { + self.queue.justDispatch({ let (entries, earlier, later) = self.fetchAroundChatEntries(index, count: count) let mutableView = MutableChatListView(earlier: earlier, entries: entries, later: later, count: count) @@ -668,7 +844,7 @@ public final class Postbox { } return }) - } + }) return disposable } @@ -677,12 +853,18 @@ public final class Postbox { public func peerWithId(id: PeerId) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() - self.queue.dispatch { + self.queue.justDispatch({ if let peer: Peer = self.peerTable.get(id) { subscriber.putNext(peer) } - } + }) return disposable } } + + public func updateMessageHistoryViewVisibleRange(id: MessageHistoryViewId, earliestVisibleIndex: MessageIndex, latestVisibleIndex: MessageIndex) { + self.queue.justDispatch({ + self.viewTracker.updateMessageHistoryViewVisibleRange(id, earliestVisibleIndex: earliestVisibleIndex, latestVisibleIndex: latestVisibleIndex) + }) + } } diff --git a/Postbox/SimpleDictionary.swift b/Postbox/SimpleDictionary.swift index 48d8af5a1f..84d03084c6 100644 --- a/Postbox/SimpleDictionary.swift +++ b/Postbox/SimpleDictionary.swift @@ -41,4 +41,5 @@ public struct SimpleDictionary: SequenceType { return nil } } -} \ No newline at end of file +} + diff --git a/Postbox/SqliteValueBox.swift b/Postbox/SqliteValueBox.swift index 8185d75c6f..4ad5dbf0db 100644 --- a/Postbox/SqliteValueBox.swift +++ b/Postbox/SqliteValueBox.swift @@ -380,7 +380,7 @@ public final class SqliteValueBox: ValueBox { resultStatement = statement } else { var statement: COpaquePointer = nil - sqlite3_prepare_v2(self.database.handle, "SELECT key FROM t\(table) WHERE key=?", -1, &statement, nil) + sqlite3_prepare_v2(self.database.handle, "SELECT rowid FROM t\(table) WHERE key=?", -1, &statement, nil) let preparedStatement = SqlitePreparedStatement(statement: statement) self.existsStatements[table] = preparedStatement resultStatement = preparedStatement diff --git a/Postbox/SynchronizePeerReadStatesView.swift b/Postbox/SynchronizePeerReadStatesView.swift new file mode 100644 index 0000000000..b9a0427a8d --- /dev/null +++ b/Postbox/SynchronizePeerReadStatesView.swift @@ -0,0 +1,29 @@ +import Foundation + +final class SynchronizePeerReadStatesView { + var operations: [PeerId: PeerReadStateSynchronizationOperation] + + init(operations: [PeerId: PeerReadStateSynchronizationOperation]) { + self.operations = operations + } + + func replay(updatedOperations: [PeerId: PeerReadStateSynchronizationOperation?]) -> [PeerId: PeerReadStateSynchronizationOperation?] { + var updates: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + + for (peerId, operation) in updatedOperations { + if let operation = operation { + if self.operations[peerId] == nil || self.operations[peerId]! != operation { + self.operations[peerId] = operation + updates[peerId] = operation + } + } else { + if let _ = self.operations[peerId] { + self.operations.removeValueForKey(peerId) + updates[peerId] = nil + } + } + } + + return updates + } +} diff --git a/Postbox/ViewTracker.swift b/Postbox/ViewTracker.swift index 772c2efdee..4733db9008 100644 --- a/Postbox/ViewTracker.swift +++ b/Postbox/ViewTracker.swift @@ -2,8 +2,10 @@ import Foundation import SwiftSignalKit public enum ViewUpdateType { + case InitialUnread(MessageIndex) case Generic - case FillHole + case FillHole(insertions: [MessageIndex: HoleFillDirection], deletions: [MessageIndex: HoleFillDirection]) + case UpdateVisible } final class ViewTracker { @@ -12,38 +14,45 @@ final class ViewTracker { private let fetchLaterHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry] private let fetchEarlierChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry] private let fetchLaterChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry] + private let fetchAnchorIndex: (MessageId) -> MessageHistoryAnchorIndex? private let renderMessage: IntermediateMessage -> Message private let fetchChatListHole: ChatListHole -> Disposable - private let fetchMessageHistoryHole: (MessageHistoryHole, MessageTags?) -> Disposable + private let fetchMessageHistoryHole: (MessageHistoryHole, HoleFillDirection, MessageTags?) -> Disposable private let sendUnsentMessage: MessageIndex -> Disposable - private let validateReadState: PeerId -> Disposable + private let synchronizeReadState: (PeerId, PeerReadStateSynchronizationOperation) -> Disposable private var chatListViews = Bag<(MutableChatListView, Pipe<(ChatListView, ViewUpdateType)>)>() private var messageHistoryViews: [PeerId: Bag<(MutableMessageHistoryView, Pipe<(MessageHistoryView, ViewUpdateType)>)>] = [:] private var unsentMessageView: UnsentMessageHistoryView - private var invalidatedReadStatesView: InvalidatedPeerReadStatesView + private var synchronizeReadStatesView: SynchronizePeerReadStatesView private var chatListHoleDisposables: [(ChatListHole, Disposable)] = [] private var holeDisposablesByPeerId: [PeerId: [(MessageHistoryHole, Disposable)]] = [:] private var unsentMessageDisposables: [MessageIndex: Disposable] = [:] - private var validateReadStatesDisposables: [PeerId: Disposable] = [:] + private var synchronizeReadStatesDisposables: [PeerId: (PeerReadStateSynchronizationOperation, Disposable)] = [:] - init(queue: Queue, fetchEarlierHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry], fetchLaterHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry], fetchEarlierChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], fetchLaterChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], renderMessage: IntermediateMessage -> Message, fetchChatListHole: ChatListHole -> Disposable, fetchMessageHistoryHole: (MessageHistoryHole, MessageTags?) -> Disposable, sendUnsentMessage: MessageIndex -> Disposable, unsentMessageIndices: [MessageIndex], validateReadState: PeerId -> Disposable, invalidatedReadStatePeerIds: [PeerId]) { + init(queue: Queue, fetchEarlierHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry], fetchLaterHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry], fetchEarlierChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], fetchLaterChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], fetchAnchorIndex: (MessageId) -> MessageHistoryAnchorIndex?, renderMessage: IntermediateMessage -> Message, fetchChatListHole: ChatListHole -> Disposable, fetchMessageHistoryHole: (MessageHistoryHole, HoleFillDirection, MessageTags?) -> Disposable, sendUnsentMessage: MessageIndex -> Disposable, unsentMessageIndices: [MessageIndex], synchronizeReadState: (PeerId, PeerReadStateSynchronizationOperation) -> Disposable, synchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation]) { self.queue = queue self.fetchEarlierHistoryEntries = fetchEarlierHistoryEntries self.fetchLaterHistoryEntries = fetchLaterHistoryEntries self.fetchEarlierChatEntries = fetchEarlierChatEntries self.fetchLaterChatEntries = fetchLaterChatEntries + self.fetchAnchorIndex = fetchAnchorIndex self.renderMessage = renderMessage self.fetchChatListHole = fetchChatListHole self.fetchMessageHistoryHole = fetchMessageHistoryHole self.sendUnsentMessage = sendUnsentMessage - self.validateReadState = validateReadState + self.synchronizeReadState = synchronizeReadState self.unsentMessageView = UnsentMessageHistoryView(indices: unsentMessageIndices) - self.invalidatedReadStatesView = InvalidatedPeerReadStatesView(peerIds: invalidatedReadStatePeerIds) + self.synchronizeReadStatesView = SynchronizePeerReadStatesView(operations: synchronizePeerReadStateOperations) self.unsentViewUpdated() - self.invalidatedReadStateViewUpdated() + + var initialPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + for (peerId, operation) in synchronizePeerReadStateOperations { + initialPeerReadStateOperations[peerId] = operation + } + self.synchronizeReadStateViewUpdated(initialPeerReadStateOperations) } deinit { @@ -97,7 +106,39 @@ final class ViewTracker { self.updateTrackedChatListHoles() } - func updateViews(currentOperationsByPeerId currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], peerIdsWithFilledHoles: Set, chatListOperations: [ChatListOperation], currentUpdatedPeers: [PeerId: Peer], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + func updateMessageHistoryViewVisibleRange(id: MessageHistoryViewId, earliestVisibleIndex: MessageIndex, latestVisibleIndex: MessageIndex) { + if let bag = self.messageHistoryViews[id.peerId] { + for (mutableView, pipe) in bag.copyItems() { + if mutableView.id == id { + let context = MutableMessageHistoryViewReplayContext() + var updated = false + + let updateType: ViewUpdateType = .UpdateVisible + + if mutableView.updateVisibleRange(earliestVisibleIndex: earliestVisibleIndex, latestVisibleIndex: latestVisibleIndex, context: context) { + mutableView.complete(context, fetchEarlier: { index, count in + return self.fetchEarlierHistoryEntries(id.peerId, index, count, mutableView.tagMask) + }, fetchLater: { index, count in + return self.fetchLaterHistoryEntries(id.peerId, index, count, mutableView.tagMask) + }) + mutableView.incrementVersion() + updated = true + } + + if updated { + mutableView.render(self.renderMessage) + pipe.putNext((MessageHistoryView(mutableView), updateType)) + + self.updateTrackedHoles(id.peerId) + } + + break + } + } + } + } + + func updateViews(currentOperationsByPeerId currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], peerIdsWithFilledHoles: [PeerId: [MessageIndex: HoleFillDirection]], removedHolesByPeerId: [PeerId: [MessageIndex: HoleFillDirection]], chatListOperations: [ChatListOperation], currentUpdatedPeers: [PeerId: Peer], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?]) { var updateTrackedHolesPeerIds: [PeerId] = [] for (peerId, bag) in self.messageHistoryViews { @@ -108,12 +149,24 @@ final class ViewTracker { let context = MutableMessageHistoryViewReplayContext() var updated = false - if mutableView.replay(operations, context: context) { + let updateType: ViewUpdateType + if let filledIndices = peerIdsWithFilledHoles[peerId] { + updateType = .FillHole(insertions: filledIndices, deletions: removedHolesByPeerId[peerId] ?? [:]) + } else { + updateType = .Generic + } + + if mutableView.replay(operations, holeFillDirections: peerIdsWithFilledHoles[peerId] ?? [:], context: context) { mutableView.complete(context, fetchEarlier: { index, count in return self.fetchEarlierHistoryEntries(peerId, index, count, mutableView.tagMask) }, fetchLater: { index, count in return self.fetchLaterHistoryEntries(peerId, index, count, mutableView.tagMask) }) + mutableView.incrementVersion() + updated = true + } + + if mutableView.updateAnchorIndex(self.fetchAnchorIndex) { updated = true } @@ -123,7 +176,9 @@ final class ViewTracker { if updated { mutableView.render(self.renderMessage) - pipe.putNext((MessageHistoryView(mutableView), peerIdsWithFilledHoles.contains(peerId) ? .FillHole : .Generic)) + + + pipe.putNext((MessageHistoryView(mutableView), updateType)) } } } @@ -154,8 +209,9 @@ final class ViewTracker { self.unsentViewUpdated() } - if self.invalidatedReadStatesView.replay(invalidatedReadStateOperations) { - self.invalidatedReadStateViewUpdated() + let synchronizeReadStateUpdates = self.synchronizeReadStatesView.replay(updatedSynchronizePeerReadStateOperations) + if !synchronizeReadStateUpdates.isEmpty { + self.synchronizeReadStateViewUpdated(synchronizeReadStateUpdates) } } @@ -217,11 +273,11 @@ final class ViewTracker { private func updateTrackedHoles(peerId: PeerId) { if let bag = self.messageHistoryViews[peerId] { var disposeHoles: [Disposable] = [] - var firstHolesAndTags: [(MessageHistoryHole, MessageTags?)] = [] + var firstHolesAndTags: [(MessageHistoryHole, HoleFillDirection, MessageTags?)] = [] for (view, _) in bag.copyItems() { - if let hole = view.firstHole() { - firstHolesAndTags.append((hole, view.tagMask)) + if let (hole, direction) = view.firstHole() { + firstHolesAndTags.append((hole, direction, view.tagMask)) } } @@ -229,7 +285,7 @@ final class ViewTracker { var i = 0 for (hole, disposable) in holes { var exists = false - for (firstHole, _) in firstHolesAndTags { + for (firstHole, _, _) in firstHolesAndTags { if hole == firstHole { exists = true break @@ -265,7 +321,7 @@ final class ViewTracker { self.holeDisposablesByPeerId[peerId] = [] } - self.holeDisposablesByPeerId[peerId]!.append((anyHoleAndTag.0, self.fetchMessageHistoryHole(anyHoleAndTag.0, anyHoleAndTag.1))) + self.holeDisposablesByPeerId[peerId]!.append((anyHoleAndTag.0, self.fetchMessageHistoryHole(anyHoleAndTag.0, anyHoleAndTag.1, anyHoleAndTag.2))) } } } @@ -313,30 +369,14 @@ final class ViewTracker { } } - private func invalidatedReadStateViewUpdated() { - var removePeerIds: [PeerId] = [] - let currentPeerIds = self.invalidatedReadStatesView.peerIds - for (peerId, _) in self.validateReadStatesDisposables { - if !currentPeerIds.contains(peerId) { - removePeerIds.append(peerId) - } - } - - for peerId in removePeerIds { - self.validateReadStatesDisposables.removeValueForKey(peerId)?.dispose() - } - - for peerId in currentPeerIds { - var found = false - for (currentPeerId, _) in validateReadStatesDisposables { - if peerId == currentPeerId { - found = true - break - } + private func synchronizeReadStateViewUpdated(updates: [PeerId: PeerReadStateSynchronizationOperation?]) { + for (peerId, operation) in updates { + if let (_, disposable) = self.synchronizeReadStatesDisposables.removeValueForKey(peerId) { + disposable.dispose() } - if !found { - self.validateReadStatesDisposables[peerId] = self.validateReadState(peerId) + if let operation = operation { + self.synchronizeReadStatesDisposables[peerId] = (operation, self.synchronizeReadState(peerId, operation)) } } } diff --git a/PostboxTests/ChatListTableTests.swift b/PostboxTests/ChatListTableTests.swift index 7f569f75a4..96aa20e6da 100644 --- a/PostboxTests/ChatListTableTests.swift +++ b/PostboxTests/ChatListTableTests.swift @@ -74,7 +74,7 @@ class ChatListTableTests: XCTestCase { var unsentTable: MessageHistoryUnsentTable? var tagsTable: MessageHistoryTagsTable? var readStateTable: MessageHistoryReadStateTable? - var invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable? + var synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable? override class func setUp() { super.setUp() @@ -98,8 +98,8 @@ class ChatListTableTests: XCTestCase { self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox!, tableId: 3) self.mediaTable = MessageMediaTable(valueBox: self.valueBox!, tableId: 2, mediaCleanupTable: self.mediaCleanupTable!) self.readStateTable = MessageHistoryReadStateTable(valueBox: self.valueBox!, tableId: 11) - self.invalidatedReadStateTable = MessageHistoryInvalidatedReadStateTable(valueBox: self.valueBox!, tableId: 12) - self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!, readStateTable: self.readStateTable!, invalidatedReadStateTable: invalidatedReadStateTable) + self.synchronizeReadStateTable = MessageHistorySynchronizeReadStateTable(valueBox: self.valueBox!, tableId: 12) + self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!, readStateTable: self.readStateTable!, synchronizeReadStateTable: self.synchronizeReadStateTable!) self.chatListIndexTable = ChatListIndexTable(valueBox: self.valueBox!, tableId: 5) self.chatListTable = ChatListTable(valueBox: self.valueBox!, tableId: 6, indexTable: self.chatListIndexTable!, metadataTable: self.historyMetadataTable!, seedConfiguration: seedConfiguration) } @@ -122,7 +122,8 @@ class ChatListTableTests: XCTestCase { private func addMessage(peerId: Int32, _ id: Int32, _ timestamp: Int32, _ text: String = "", _ media: [Media] = []) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id), timestamp: timestamp, flags: [], tags: [], forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media)], location: .Random, operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id), timestamp: timestamp, flags: [], tags: [], forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media)], location: .Random, operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) var operations: [ChatListOperation] = [] self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) } @@ -130,7 +131,8 @@ class ChatListTableTests: XCTestCase { private func addHole(peerId: Int32, _ id: Int32) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.addHoles([MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id)], operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.addHoles([MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id)], operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) var operations: [ChatListOperation] = [] self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) } @@ -153,15 +155,17 @@ class ChatListTableTests: XCTestCase { private func removeMessages(peerId: Int32, _ ids: [Int32]) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.removeMessages(ids.map({ MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: $0) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.removeMessages(ids.map({ MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: $0) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) var operations: [ChatListOperation] = [] self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) } - private func fillHole(peerId: Int32, _ id: Int32, _ fillType: HoleFillType, _ messages: [(Int32, Int32, String, [Media])]) { + private func fillHole(peerId: Int32, _ id: Int32, _ fillType: HoleFill, _ messages: [(Int32, Int32, String, [Media])]) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.fillHole(MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id), fillType: fillType, tagMask: nil, messages: messages.map({ StoreMessage(id: MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: $0.0), timestamp: $0.1, flags: [], tags: [], forwardInfo: nil, authorId: authorPeerId, text: $0.2, attributes: [], media: $0.3) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.fillHole(MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id), fillType: fillType, tagMask: nil, messages: messages.map({ StoreMessage(id: MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: $0.0), timestamp: $0.1, flags: [], tags: [], forwardInfo: nil, authorId: authorPeerId, text: $0.2, attributes: [], media: $0.3) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) var operations: [ChatListOperation] = [] self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) } diff --git a/PostboxTests/MessageHistoryIndexTableTests.swift b/PostboxTests/MessageHistoryIndexTableTests.swift index 17d46e1a54..6b18fa2a88 100644 --- a/PostboxTests/MessageHistoryIndexTableTests.swift +++ b/PostboxTests/MessageHistoryIndexTableTests.swift @@ -9,6 +9,12 @@ import Postbox private let peerId = PeerId(namespace: 1, id: 1) private let namespace: Int32 = 1 +private extension MessageIndex { + init(id: Int32, timestamp: Int32) { + self.init(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp) + } +} + private enum Item: Equatable, CustomStringConvertible { case Message(Int32, Int32) case Hole(Int32, Int32, Int32) @@ -103,11 +109,8 @@ class MessageHistoryIndexTableTests: XCTestCase { }, location: .UpperHistoryBlock, operations: &operations) } - func fillHole(id: Int32, _ fillType: HoleFillType, _ messages: [(Int32, Int32)], _ tagMask: MessageTags? = nil) { + func fillHole(id: Int32, _ fillType: HoleFill, _ messages: [(Int32, Int32)], _ tagMask: MessageTags? = nil) { var operations: [MessageHistoryIndexOperation] = [] - let zeroData = WriteBuffer() - var zero: Int32 = 0 - zeroData.write(&zero, offset: 0, length: 4) self.indexTable!.fillHole(MessageId(peerId: peerId, namespace: namespace, id: id), fillType: fillType, tagMask: tagMask, messages: messages.map({ return InternalStoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: $0.0), timestamp: $0.1, flags: [], tags: [], forwardInfo: nil, authorId: peerId, text: "", attributes: [], media: []) @@ -265,42 +268,42 @@ class MessageHistoryIndexTableTests: XCTestCase { } func testFillHoleEmpty() { - fillHole(1, .Complete, []) + fillHole(1, HoleFill(complete: true, direction: .UpperToLower), []) expect([]) } func testFillHoleComplete() { addHole(100) - fillHole(1, .Complete, [(100, 100), (200, 200)]) + fillHole(1, HoleFill(complete: true, direction: .UpperToLower), [(100, 100), (200, 200)]) expect([.Message(100, 100), .Message(200, 200)]) } func testFillHoleUpperToLowerPartial() { addHole(100) - fillHole(1, .UpperToLower, [(100, 100), (200, 200)]) + fillHole(1, HoleFill(complete: false, direction: .UpperToLower), [(100, 100), (200, 200)]) expect([.Hole(1, 99, 100), .Message(100, 100), .Message(200, 200)]) } func testFillHoleUpperToLowerToBounds() { addHole(100) - fillHole(1, .UpperToLower, [(1, 1), (200, 200)]) + fillHole(1, HoleFill(complete: false, direction: .UpperToLower), [(1, 1), (200, 200)]) expect([.Message(1, 1), .Message(200, 200)]) } func testFillHoleLowerToUpperToBounds() { addHole(100) - fillHole(1, .LowerToUpper, [(100, 100), (Int32.max, 200)]) + fillHole(1, HoleFill(complete: false, direction: .LowerToUpper), [(100, 100), (Int32.max, 200)]) expect([.Message(100, 100), .Message(Int32.max, 200)]) } func testFillHoleLowerToUpperPartial() { addHole(100) - fillHole(1, .LowerToUpper, [(100, 100), (200, 200)]) + fillHole(1, HoleFill(complete: false, direction: .LowerToUpper), [(100, 100), (200, 200)]) expect([.Message(100, 100), .Message(200, 200), .Hole(201, Int32.max, Int32.max)]) } @@ -310,7 +313,7 @@ class MessageHistoryIndexTableTests: XCTestCase { addMessage(100, 100) addMessage(200, 200) - fillHole(199, .UpperToLower, [(150, 150)]) + fillHole(199, HoleFill(complete: false, direction: .UpperToLower), [(150, 150)]) expect([.Hole(1, 99, 100), .Message(100, 100), .Hole(101, 149, 150), .Message(150, 150), .Message(200, 200), .Hole(201, Int32.max, Int32.max)]) } @@ -321,7 +324,7 @@ class MessageHistoryIndexTableTests: XCTestCase { addMessage(100, 100) addMessage(200, 200) - fillHole(199, .LowerToUpper, [(150, 150)]) + fillHole(199, HoleFill(complete: false, direction: .LowerToUpper), [(150, 150)]) expect([.Hole(1, 99, 100), .Message(100, 100), .Message(150, 150), .Hole(151, 199, 200), .Message(200, 200), .Hole(201, Int32.max, Int32.max)]) } @@ -332,7 +335,7 @@ class MessageHistoryIndexTableTests: XCTestCase { addMessage(100, 100) addMessage(200, 200) - fillHole(199, .Complete, [(150, 150)]) + fillHole(199, HoleFill(complete: true, direction: .UpperToLower), [(150, 150)]) expect([.Hole(1, 99, 100), .Message(100, 100), .Message(150, 150), .Message(200, 200), .Hole(201, Int32.max, Int32.max)]) } @@ -350,7 +353,7 @@ class MessageHistoryIndexTableTests: XCTestCase { addMessage(100, 100) addHole(1) - fillHole(99, .Complete, []) + fillHole(99, HoleFill(complete: true, direction: .UpperToLower), []) expect([.Message(100, 100)]) } @@ -359,7 +362,7 @@ class MessageHistoryIndexTableTests: XCTestCase { addMessage(100, 100) addMessage(101, 101) - fillHole(100, .Complete, [(90, 90)]) + fillHole(100, HoleFill(complete: true, direction: .UpperToLower), [(90, 90)]) expect([.Message(90, 90), .Message(100, 100), .Message(101, 101)]) } @@ -369,7 +372,7 @@ class MessageHistoryIndexTableTests: XCTestCase { addMessage(200, 200) addHole(150) - fillHole(199, .UpperToLower, [(150, 150), (300, 300)]) + fillHole(199, HoleFill(complete: false, direction: .UpperToLower), [(150, 150), (300, 300)]) expect([.Message(100, 100), .Hole(101, 149, 150), .Message(150, 150), .Message(200, 200), .Message(300, 300)]) } @@ -502,4 +505,18 @@ class MessageHistoryIndexTableTests: XCTestCase { addMessagesUpperBlock([(10, 11)]) expect([.Message(10, 10)]) } + + func testFillHoleAtIndex() { + addHole(1) + expect([.Hole(1, Int32.max, Int32.max)]) + fillHole(1, HoleFill(complete: false, direction: .AroundIndex(MessageIndex(id: 10, timestamp: 10))), [(5, 5), (10, 10)]) + expect([.Hole(1, 4, 5), .Message(5, 5), .Message(10, 10), .Hole(11, Int32.max, Int32.max)]) + } + + func testFillHoleAtIndexComplete() { + addHole(1) + expect([.Hole(1, Int32.max, Int32.max)]) + fillHole(1, HoleFill(complete: true, direction: .AroundIndex(MessageIndex(id: 10, timestamp: 10))), [(5, 5), (10, 10)]) + expect([.Message(5, 5), .Message(10, 10)]) + } } diff --git a/PostboxTests/MessageHistoryTableTests.swift b/PostboxTests/MessageHistoryTableTests.swift index ddef648dab..8a8726aa73 100644 --- a/PostboxTests/MessageHistoryTableTests.swift +++ b/PostboxTests/MessageHistoryTableTests.swift @@ -203,7 +203,7 @@ class MessageHistoryTableTests: XCTestCase { var unsentTable: MessageHistoryUnsentTable? var tagsTable: MessageHistoryTagsTable? var readStateTable: MessageHistoryReadStateTable? - var invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable? + var synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable? override class func setUp() { super.setUp() @@ -231,8 +231,8 @@ class MessageHistoryTableTests: XCTestCase { self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox!, tableId: 3) self.mediaTable = MessageMediaTable(valueBox: self.valueBox!, tableId: 2, mediaCleanupTable: self.mediaCleanupTable!) self.readStateTable = MessageHistoryReadStateTable(valueBox: self.valueBox!, tableId: 10) - self.invalidatedReadStateTable = MessageHistoryInvalidatedReadStateTable(valueBox: self.valueBox!, tableId: 11) - self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!, readStateTable: self.readStateTable!, invalidatedReadStateTable: invalidatedReadStateTable) + self.synchronizeReadStateTable = MessageHistorySynchronizeReadStateTable(valueBox: self.valueBox!, tableId: 11) + self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!, readStateTable: self.readStateTable!, synchronizeReadStateTable: self.synchronizeReadStateTable!) self.peerTable = PeerTable(valueBox: self.valueBox!, tableId: 6) self.peerTable!.set(peer) } @@ -255,31 +255,36 @@ class MessageHistoryTableTests: XCTestCase { private func addMessage(id: Int32, _ timestamp: Int32, _ text: String = "", _ media: [Media] = [], _ flags: StoreMessageFlags = [], _ tags: MessageTags = []) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, flags: flags, tags: tags, forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media)], location: .Random, operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, flags: flags, tags: tags, forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media)], location: .Random, operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } private func updateMessage(previousId: Int32, _ id: Int32, _ timestamp: Int32, _ text: String = "", _ media: [Media] = [], _ flags: StoreMessageFlags, _ tags: MessageTags) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.updateMessage(MessageId(peerId: peerId, namespace: namespace, id: previousId), message: StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, flags: flags, tags: tags, forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.updateMessage(MessageId(peerId: peerId, namespace: namespace, id: previousId), message: StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, flags: flags, tags: tags, forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } private func addHole(id: Int32) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.addHoles([MessageId(peerId: peerId, namespace: namespace, id: id)], operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.addHoles([MessageId(peerId: peerId, namespace: namespace, id: id)], operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } private func removeMessages(ids: [Int32]) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.removeMessages(ids.map({ MessageId(peerId: peerId, namespace: namespace, id: $0) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.removeMessages(ids.map({ MessageId(peerId: peerId, namespace: namespace, id: $0) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } - private func fillHole(id: Int32, _ fillType: HoleFillType, _ messages: [(Int32, Int32, String, [Media])], _ tagMask: MessageTags? = nil) { + private func fillHole(id: Int32, _ fillType: HoleFill, _ messages: [(Int32, Int32, String, [Media])], _ tagMask: MessageTags? = nil) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] - self.historyTable!.fillHole(MessageId(peerId: peerId, namespace: namespace, id: id), fillType: fillType, tagMask: tagMask, messages: messages.map({ StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: $0.0), timestamp: $0.1, flags: [], tags: [], forwardInfo: nil, authorId: authorPeerId, text: $0.2, attributes: [], media: $0.3) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.fillHole(MessageId(peerId: peerId, namespace: namespace, id: id), fillType: fillType, tagMask: tagMask, messages: messages.map({ StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: $0.0), timestamp: $0.1, flags: [], tags: [], forwardInfo: nil, authorId: authorPeerId, text: $0.2, attributes: [], media: $0.3) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) } private func expectEntries(entries: [Entry], tagMask: MessageTags? = nil) { @@ -578,42 +583,42 @@ class MessageHistoryTableTests: XCTestCase { } func testFillHoleEmpty() { - fillHole(1, .Complete, []) + fillHole(1, HoleFill(complete: true, direction: .UpperToLower), []) expectEntries([]) } func testFillHoleComplete() { addHole(100) - fillHole(1, .Complete, [(100, 100, "m100", []), (200, 200, "m200", [])]) + fillHole(1, HoleFill(complete: true, direction: .UpperToLower), [(100, 100, "m100", []), (200, 200, "m200", [])]) expectEntries([.Message(100, 100, "m100", [], []), .Message(200, 200, "m200", [], [])]) } func testFillHoleUpperToLowerPartial() { addHole(100) - fillHole(1, .UpperToLower, [(100, 100, "m100", []), (200, 200, "m200", [])]) + fillHole(1, HoleFill(complete: false, direction: .UpperToLower), [(100, 100, "m100", []), (200, 200, "m200", [])]) expectEntries([.Hole(1, 99, 100), .Message(100, 100, "m100", [], []), .Message(200, 200, "m200", [], [])]) } func testFillHoleUpperToLowerToBounds() { addHole(100) - fillHole(1, .UpperToLower, [(1, 1, "m1", []), (200, 200, "m200", [])]) + fillHole(1, HoleFill(complete: false, direction: .UpperToLower), [(1, 1, "m1", []), (200, 200, "m200", [])]) expectEntries([.Message(1, 1, "m1", [], []), .Message(200, 200, "m200", [], [])]) } func testFillHoleLowerToUpperToBounds() { addHole(100) - fillHole(1, .LowerToUpper, [(100, 100, "m100", []), (Int32.max, 200, "m200", [])]) + fillHole(1, HoleFill(complete: false, direction: .LowerToUpper), [(100, 100, "m100", []), (Int32.max, 200, "m200", [])]) expectEntries([.Message(100, 100, "m100", [], []), .Message(Int32.max, 200, "m200", [], [])]) } func testFillHoleLowerToUpperPartial() { addHole(100) - fillHole(1, .LowerToUpper, [(100, 100, "m100", []), (200, 200, "m200", [])]) + fillHole(1, HoleFill(complete: false, direction: .LowerToUpper), [(100, 100, "m100", []), (200, 200, "m200", [])]) expectEntries([.Message(100, 100, "m100", [], []), .Message(200, 200, "m200", [], []), .Hole(201, Int32.max, Int32.max)]) } @@ -623,7 +628,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "m100") addMessage(200, 200, "m200") - fillHole(199, .UpperToLower, [(150, 150, "m150", [])]) + fillHole(199, HoleFill(complete: false, direction: .UpperToLower), [(150, 150, "m150", [])]) expectEntries([.Hole(1, 99, 100), .Message(100, 100, "m100", [], []), .Hole(101, 149, 150), .Message(150, 150, "m150", [], []), .Message(200, 200, "m200", [], []), .Hole(201, Int32.max, Int32.max)]) } @@ -634,7 +639,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "m100") addMessage(200, 200, "m200") - fillHole(199, .LowerToUpper, [(150, 150, "m150", [])]) + fillHole(199, HoleFill(complete: false, direction: .LowerToUpper), [(150, 150, "m150", [])]) expectEntries([.Hole(1, 99, 100), .Message(100, 100, "m100", [], []), .Message(150, 150, "m150", [], []), .Hole(151, 199, 200), .Message(200, 200, "m200", [], []), .Hole(201, Int32.max, Int32.max)]) } @@ -645,7 +650,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "m100") addMessage(200, 200, "m200") - fillHole(199, .Complete, [(150, 150, "m150", [])]) + fillHole(199, HoleFill(complete: true, direction: .UpperToLower), [(150, 150, "m150", [])]) expectEntries([.Hole(1, 99, 100), .Message(100, 100, "m100", [], []), .Message(150, 150, "m150", [], []), .Message(200, 200, "m200", [], []), .Hole(201, Int32.max, Int32.max)]) } @@ -663,7 +668,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "m100") addHole(1) - fillHole(99, .Complete, []) + fillHole(99, HoleFill(complete: true, direction: .UpperToLower), []) expectEntries([.Message(100, 100, "m100", [], [])]) } @@ -672,7 +677,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "m100") addMessage(101, 101, "m101") - fillHole(100, .Complete, [(90, 90, "m90", [])]) + fillHole(100, HoleFill(complete: true, direction: .UpperToLower), [(90, 90, "m90", [])]) expectEntries([.Message(90, 90, "m90", [], []), .Message(100, 100, "m100", [], []), .Message(101, 101, "m101", [], [])]) } @@ -682,7 +687,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200") addHole(150) - fillHole(199, .UpperToLower, [(150, 150, "m150", []), (300, 300, "m300", [])]) + fillHole(199, HoleFill(complete: false, direction: .UpperToLower), [(150, 150, "m150", []), (300, 300, "m300", [])]) expectEntries([.Message(100, 100, "m100", [], []), .Hole(101, 149, 150), .Message(150, 150, "m150", [], []), .Message(200, 200, "m200", [], []), .Message(300, 300, "m300", [], [])]) } @@ -874,7 +879,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .UpperToLower, [(180, 180, "m180", [])]) + fillHole(199, HoleFill(complete: false, direction: .UpperToLower), [(180, 180, "m180", [])]) expectEntries([.Message(100, 100, "m100", [], []), .Hole(101, 179, 180)], tagMask: [.First]) expectEntries([.Hole(101, 179, 180), .Message(200, 200, "m200", [], [])], tagMask: [.Second]) @@ -886,7 +891,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .LowerToUpper, [(180, 180, "m180", [])]) + fillHole(199, HoleFill(complete: false, direction: .LowerToUpper), [(180, 180, "m180", [])]) expectEntries([.Message(100, 100, "m100", [], []), .Hole(181, 199, 200)], tagMask: [.First]) expectEntries([.Hole(181, 199, 200), .Message(200, 200, "m200", [], [])], tagMask: [.Second]) @@ -898,7 +903,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .Complete, [(180, 180, "m180", [])]) + fillHole(199, HoleFill(complete: true, direction: .UpperToLower), [(180, 180, "m180", [])]) expectEntries([.Message(100, 100, "m100", [], [])], tagMask: [.First]) expectEntries([.Message(200, 200, "m200", [], [])], tagMask: [.Second]) @@ -910,7 +915,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .UpperToLower, [(180, 180, "m180", [])], [.First]) + fillHole(199, HoleFill(complete: false, direction: .UpperToLower), [(180, 180, "m180", [])], [.First]) expectEntries([.Message(100, 100, "m100", [], []), .Hole(101, 179, 180)], tagMask: [.First]) expectEntries([.Hole(101, 179, 180), .Hole(181, 199, 200), .Message(200, 200, "m200", [], [])], tagMask: [.Second]) @@ -922,7 +927,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .LowerToUpper, [(180, 180, "m180", [])], [.First]) + fillHole(199, HoleFill(complete: false, direction: .LowerToUpper), [(180, 180, "m180", [])], [.First]) expectEntries([.Message(100, 100, "m100", [], []), .Hole(181, 199, 200)], tagMask: [.First]) expectEntries([.Hole(101, 179, 180), .Hole(181, 199, 200), .Message(200, 200, "m200", [], [])], tagMask: [.Second]) @@ -934,7 +939,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .Complete, [(180, 180, "m180", [])], [.First]) + fillHole(199, HoleFill(complete: true, direction: .UpperToLower), [(180, 180, "m180", [])], [.First]) expectEntries([.Message(100, 100, "m100", [], [])], tagMask: [.First]) expectEntries([.Hole(101, 179, 180), .Hole(181, 199, 200), .Message(200, 200, "m200", [], [])], tagMask: [.Second]) @@ -946,7 +951,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .UpperToLower, [], [.First]) + fillHole(199, HoleFill(complete: false, direction: .UpperToLower), [], [.First]) expectEntries([.Message(100, 100, "m100", [], [])], tagMask: [.First]) expectEntries([.Hole(101, 199, 200), .Message(200, 200, "m200", [], [])], tagMask: [.Second]) @@ -958,7 +963,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .LowerToUpper, [], [.First]) + fillHole(199, HoleFill(complete: false, direction: .LowerToUpper), [], [.First]) expectEntries([.Message(100, 100, "m100", [], [])], tagMask: [.First]) expectEntries([.Hole(101, 199, 200), .Message(200, 200, "m200", [], [])], tagMask: [.Second]) @@ -970,7 +975,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "m200", [], [], [.Second]) addHole(150) - fillHole(199, .Complete, [], [.First]) + fillHole(199, HoleFill(complete: true, direction: .UpperToLower), [], [.First]) expectEntries([.Message(100, 100, "m100", [], [])], tagMask: [.First]) expectEntries([.Hole(101, 199, 200), .Message(200, 200, "m200", [], [])], tagMask: [.Second]) diff --git a/PostboxTests/ReadStateTableTests.swift b/PostboxTests/ReadStateTableTests.swift new file mode 100644 index 0000000000..bd7e2e1688 --- /dev/null +++ b/PostboxTests/ReadStateTableTests.swift @@ -0,0 +1,244 @@ +import Foundation + +import UIKit +import XCTest + +import Postbox +@testable import Postbox + +private let peerId = PeerId(namespace: 1, id: 1) +private let namespace: Int32 = 1 +private let authorPeerId = PeerId(namespace: 1, id: 6) + +private func ==(lhs: [Media], rhs: [Media]) -> Bool { + if lhs.count != rhs.count { + return false + } + + for i in 0 ..< lhs.count { + if !lhs[i].isEqual(rhs[i]) { + return false + } + } + return true +} + +private enum Entry: Equatable, CustomStringConvertible { + case Message(Int32, Int32, String, [Media], MessageFlags) + case Hole(Int32, Int32, Int32) + + var description: String { + switch self { + case let .Message(id, timestamp, text, media, flags): + return "Message(\(id), \(timestamp), \(text), \(media), \(flags))" + case let .Hole(min, max, timestamp): + return "Hole(\(min), \(max), \(timestamp))" + } + } +} + +private func ==(lhs: Entry, rhs: Entry) -> Bool { + switch lhs { + case let .Message(lhsId, lhsTimestamp, lhsText, lhsMedia, lhsFlags): + switch rhs { + case let .Message(rhsId, rhsTimestamp, rhsText, rhsMedia, rhsFlags): + return lhsId == rhsId && lhsTimestamp == rhsTimestamp && lhsText == rhsText && lhsMedia == rhsMedia && lhsFlags == rhsFlags + case .Hole: + return false + } + case let .Hole(lhsMin, lhsMax, lhsMaxTimestamp): + switch rhs { + case .Message: + return false + case let .Hole(rhsMin, rhsMax, rhsMaxTimestamp): + return lhsMin == rhsMin && lhsMax == rhsMax && lhsMaxTimestamp == rhsMaxTimestamp + } + } +} + +private extension MessageTags { + static let First = MessageTags(rawValue: 1 << 0) + static let Second = MessageTags(rawValue: 1 << 1) +} + +class ReadStateTableTests: XCTestCase { + var valueBox: ValueBox? + var path: String? + + var peerTable: PeerTable? + var globalMessageIdsTable: GlobalMessageIdsTable? + var indexTable: MessageHistoryIndexTable? + var mediaTable: MessageMediaTable? + var mediaCleanupTable: MediaCleanupTable? + var historyTable: MessageHistoryTable? + var historyMetadataTable: MessageHistoryMetadataTable? + var unsentTable: MessageHistoryUnsentTable? + var tagsTable: MessageHistoryTagsTable? + var readStateTable: MessageHistoryReadStateTable? + var synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable? + + override func setUp() { + super.setUp() + + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + path = NSTemporaryDirectory().stringByAppendingString("\(randomId)") + self.valueBox = SqliteValueBox(basePath: path!) + + let seedConfiguration = SeedConfiguration(initializeChatListWithHoles: [], initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second]) + + self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox!, tableId: 5, namespace: namespace) + self.historyMetadataTable = MessageHistoryMetadataTable(valueBox: self.valueBox!, tableId: 7) + self.unsentTable = MessageHistoryUnsentTable(valueBox: self.valueBox!, tableId: 8) + self.tagsTable = MessageHistoryTagsTable(valueBox: self.valueBox!, tableId: 9) + self.indexTable = MessageHistoryIndexTable(valueBox: self.valueBox!, tableId: 1, globalMessageIdsTable: self.globalMessageIdsTable!, metadataTable: self.historyMetadataTable!, seedConfiguration: seedConfiguration) + self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox!, tableId: 3) + self.mediaTable = MessageMediaTable(valueBox: self.valueBox!, tableId: 2, mediaCleanupTable: self.mediaCleanupTable!) + self.readStateTable = MessageHistoryReadStateTable(valueBox: self.valueBox!, tableId: 10) + self.synchronizeReadStateTable = MessageHistorySynchronizeReadStateTable(valueBox: self.valueBox!, tableId: 11) + self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!, readStateTable: self.readStateTable!, synchronizeReadStateTable: self.synchronizeReadStateTable!) + } + + override func tearDown() { + super.tearDown() + + self.historyTable = nil + self.indexTable = nil + self.mediaTable = nil + self.mediaCleanupTable = nil + self.peerTable = nil + self.historyMetadataTable = nil + + self.valueBox = nil + let _ = try? NSFileManager.defaultManager().removeItemAtPath(path!) + self.path = nil + } + + private func addMessage(id: Int32, _ timestamp: Int32, _ text: String = "", _ media: [Media] = [], _ flags: StoreMessageFlags = [], _ tags: MessageTags = []) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, flags: flags, tags: tags, forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media)], location: .Random, operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + } + + private func updateMessage(previousId: Int32, _ id: Int32, _ timestamp: Int32, _ text: String = "", _ media: [Media] = [], _ flags: StoreMessageFlags, _ tags: MessageTags) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.updateMessage(MessageId(peerId: peerId, namespace: namespace, id: previousId), message: StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, flags: flags, tags: tags, forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + } + + private func addHole(id: Int32) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.addHoles([MessageId(peerId: peerId, namespace: namespace, id: id)], operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + } + + private func removeMessages(ids: [Int32]) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.removeMessages(ids.map({ MessageId(peerId: peerId, namespace: namespace, id: $0) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + } + + private func expectApplyRead(messageId: Int32, _ expectInvalidate: Bool) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.applyIncomingReadMaxId(MessageId(peerId: peerId, namespace: namespace, id: messageId), operationsByPeerId: &operationsByPeerId, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + + let invalidated = updatedPeerReadStateOperations.count != 0 + if expectInvalidate != invalidated { + XCTFail("applyRead: invalidated expected \(expectInvalidate), actual: \(invalidated)") + } + } + + private func expectReadState(maxReadId: Int32, _ maxKnownId: Int32, _ count: Int32) { + if let state = self.readStateTable!.getCombinedState(peerId)?.states.first?.1 { + if state.maxReadId != maxReadId || state.maxKnownId != maxKnownId || state.count != count { + XCTFail("Expected\nmaxReadId: \(maxReadId), maxKnownId: \(maxKnownId), count: \(count)\nActual\nmaxReadId: \(state.maxReadId), maxKnownId: \(state.maxKnownId), count: \(state.count)") + } + } else { + XCTFail("Expected\nmaxReadId: (maxReadId), maxKnownId: \(maxKnownId), count: \(count)\nActual\nnil") + } + } + + func testResetState() { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.resetIncomingReadStates([peerId: [namespace: PeerReadState(maxReadId: 100, maxKnownId: 120, count: 130)]], operationsByPeerId: &operationsByPeerId, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + + expectReadState(100, 120, 130) + } + + func testAddIncomingBeforeKnown() { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.resetIncomingReadStates([peerId: [namespace: PeerReadState(maxReadId: 100, maxKnownId: 120, count: 130)]], operationsByPeerId: &operationsByPeerId, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + + self.addMessage(99, 99, "", [], [.Incoming]) + + expectReadState(100, 120, 130) + } + + func testAddIncomingAfterKnown() { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.resetIncomingReadStates([peerId: [namespace: PeerReadState(maxReadId: 100, maxKnownId: 120, count: 130)]], operationsByPeerId: &operationsByPeerId, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + + self.addMessage(130, 130, "", [], [.Incoming]) + + expectReadState(100, 120, 131) + } + + func testApplyReadThenAddIncoming() { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.resetIncomingReadStates([peerId: [namespace: PeerReadState(maxReadId: 100, maxKnownId: 100, count: 0)]], operationsByPeerId: &operationsByPeerId, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + + self.expectApplyRead(200, false) + + self.addMessage(130, 130, "", [], [.Incoming]) + + expectReadState(200, 200, 0) + } + + func testApplyAddIncomingThenRead() { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.resetIncomingReadStates([peerId: [namespace: PeerReadState(maxReadId: 100, maxKnownId: 100, count: 0)]], operationsByPeerId: &operationsByPeerId, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + + self.addMessage(130, 130, "", [], [.Incoming]) + + expectReadState(100, 100, 1) + + self.expectApplyRead(200, false) + + expectReadState(200, 200, 0) + } + + func testIgnoreOldRead() { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.resetIncomingReadStates([peerId: [namespace: PeerReadState(maxReadId: 100, maxKnownId: 100, count: 0)]], operationsByPeerId: &operationsByPeerId, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + + self.expectApplyRead(90, false) + + expectReadState(100, 100, 0) + } + + func testInvalidateReadHole() { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + self.historyTable!.resetIncomingReadStates([peerId: [namespace: PeerReadState(maxReadId: 100, maxKnownId: 100, count: 0)]], operationsByPeerId: &operationsByPeerId, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) + + self.addMessage(200, 200) + self.addHole(1) + + self.expectApplyRead(200, true) + + expectReadState(200, 200, 0) + } + + +}