diff --git a/Postbox.xcodeproj/project.pbxproj b/Postbox.xcodeproj/project.pbxproj index dec81ee69a..dd12187c67 100644 --- a/Postbox.xcodeproj/project.pbxproj +++ b/Postbox.xcodeproj/project.pbxproj @@ -35,13 +35,12 @@ D0977F9C1B822DB4009994B2 /* ValueBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0977F9B1B822DB4009994B2 /* ValueBox.swift */; }; D0977F9E1B8234DF009994B2 /* ValueBoxKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0977F9D1B8234DF009994B2 /* ValueBoxKey.swift */; }; D0977FA01B8244D7009994B2 /* SqliteValueBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0977F9F1B8244D7009994B2 /* SqliteValueBox.swift */; }; - D0977FA21B82930C009994B2 /* PostboxCodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0977FA11B82930C009994B2 /* PostboxCodingUtils.swift */; }; - D0977FA41B829F87009994B2 /* PostboxTables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0977FA31B829F87009994B2 /* PostboxTables.swift */; }; D0A7D9451C556CFE0016A115 /* MessageHistoryIndexTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7D9441C556CFE0016A115 /* MessageHistoryIndexTableTests.swift */; }; D0B76BE71B66639F0095CF45 /* DeferredString.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B76BE61B66639F0095CF45 /* DeferredString.swift */; }; D0C07F6A1B67DB4800966E43 /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C07F691B67DB4800966E43 /* SwiftSignalKit.framework */; }; + D0C8FCB71C5C2D200028C27F /* MessageHistoryViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C8FCB61C5C2D200028C27F /* MessageHistoryViewTests.swift */; }; D0D224F21B4D6ABD0085E26D /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D224ED1B4D6ABD0085E26D /* Functions.swift */; }; - D0D225261B4D84930085E26D /* PeerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D225251B4D84930085E26D /* PeerView.swift */; }; + D0E1DE151C5E1C6900C7826E /* ViewTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1DE141C5E1C6900C7826E /* ViewTracker.swift */; }; D0E3A7501B28A7E300A402D9 /* Postbox.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E3A74F1B28A7E300A402D9 /* Postbox.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0E3A7561B28A7E300A402D9 /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E3A74A1B28A7E300A402D9 /* Postbox.framework */; }; D0E3A7821B28ADD000A402D9 /* Postbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E3A7811B28ADD000A402D9 /* Postbox.swift */; }; @@ -52,6 +51,15 @@ D0F9E85B1C565EBB00037222 /* MessageMediaTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E85A1C565EBB00037222 /* MessageMediaTable.swift */; }; D0F9E8611C57766A00037222 /* MessageHistoryTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8601C57766A00037222 /* MessageHistoryTableTests.swift */; }; D0F9E8631C579F0200037222 /* MediaCleanupTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8621C579F0200037222 /* MediaCleanupTable.swift */; }; + D0F9E8651C58CB7F00037222 /* ChatListTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8641C58CB7F00037222 /* ChatListTable.swift */; }; + D0F9E8671C58D08900037222 /* ChatListIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8661C58D08900037222 /* ChatListIndexTable.swift */; }; + D0F9E8691C58FA9300037222 /* ChatListTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8681C58FA9300037222 /* ChatListTableTests.swift */; }; + D0F9E86B1C59719800037222 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E86A1C59719800037222 /* ChatListView.swift */; }; + D0F9E86D1C5A0E5D00037222 /* MetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E86C1C5A0E5D00037222 /* MetadataTable.swift */; }; + D0F9E86F1C5A0E7600037222 /* KeychainTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E86E1C5A0E7600037222 /* KeychainTable.swift */; }; + D0F9E8711C5A0E9B00037222 /* PeerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8701C5A0E9B00037222 /* PeerTable.swift */; }; + D0F9E8731C5A1EE500037222 /* GlobalMessageIdsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8721C5A1EE500037222 /* GlobalMessageIdsTable.swift */; }; + D0F9E8751C5A334100037222 /* SimpleDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8741C5A334100037222 /* SimpleDictionary.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -94,13 +102,12 @@ D0977F9B1B822DB4009994B2 /* ValueBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueBox.swift; sourceTree = ""; }; D0977F9D1B8234DF009994B2 /* ValueBoxKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueBoxKey.swift; sourceTree = ""; }; D0977F9F1B8244D7009994B2 /* SqliteValueBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SqliteValueBox.swift; sourceTree = ""; }; - D0977FA11B82930C009994B2 /* PostboxCodingUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxCodingUtils.swift; sourceTree = ""; }; - D0977FA31B829F87009994B2 /* PostboxTables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxTables.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 = ""; }; + D0C8FCB61C5C2D200028C27F /* MessageHistoryViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryViewTests.swift; sourceTree = ""; }; D0D224ED1B4D6ABD0085E26D /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Functions.swift; path = submodules/sqlite.swift/SQLite/Functions.swift; sourceTree = SOURCE_ROOT; }; - D0D225251B4D84930085E26D /* PeerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerView.swift; sourceTree = ""; }; + D0E1DE141C5E1C6900C7826E /* ViewTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTracker.swift; sourceTree = ""; }; D0E3A74A1B28A7E300A402D9 /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D0E3A74E1B28A7E300A402D9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D0E3A74F1B28A7E300A402D9 /* Postbox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Postbox.h; sourceTree = ""; }; @@ -114,6 +121,15 @@ D0F9E85A1C565EBB00037222 /* MessageMediaTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageMediaTable.swift; sourceTree = ""; }; D0F9E8601C57766A00037222 /* MessageHistoryTableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTableTests.swift; sourceTree = ""; }; D0F9E8621C579F0200037222 /* MediaCleanupTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaCleanupTable.swift; sourceTree = ""; }; + D0F9E8641C58CB7F00037222 /* ChatListTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListTable.swift; sourceTree = ""; }; + D0F9E8661C58D08900037222 /* ChatListIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListIndexTable.swift; sourceTree = ""; }; + D0F9E8681C58FA9300037222 /* ChatListTableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListTableTests.swift; sourceTree = ""; }; + D0F9E86A1C59719800037222 /* ChatListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = ""; }; + D0F9E86C1C5A0E5D00037222 /* MetadataTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataTable.swift; sourceTree = ""; }; + D0F9E86E1C5A0E7600037222 /* KeychainTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainTable.swift; sourceTree = ""; }; + D0F9E8701C5A0E9B00037222 /* PeerTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerTable.swift; sourceTree = ""; }; + D0F9E8721C5A1EE500037222 /* GlobalMessageIdsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalMessageIdsTable.swift; sourceTree = ""; }; + D0F9E8741C5A334100037222 /* SimpleDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleDictionary.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -190,6 +206,64 @@ name = sqlite.swift; sourceTree = ""; }; + D0E1DE161C5EB06000C7826E /* Tables */ = { + isa = PBXGroup; + children = ( + D0F9E8721C5A1EE500037222 /* GlobalMessageIdsTable.swift */, + D08C713B1C51283C00779C0F /* MessageHistoryIndexTable.swift */, + D08C713D1C512EA500779C0F /* MessageHistoryTable.swift */, + D0F9E8641C58CB7F00037222 /* ChatListTable.swift */, + D0F9E8661C58D08900037222 /* ChatListIndexTable.swift */, + D0F9E85A1C565EBB00037222 /* MessageMediaTable.swift */, + D0F9E8621C579F0200037222 /* MediaCleanupTable.swift */, + D0F9E86C1C5A0E5D00037222 /* MetadataTable.swift */, + D0F9E86E1C5A0E7600037222 /* KeychainTable.swift */, + D0F9E8701C5A0E9B00037222 /* PeerTable.swift */, + ); + name = Tables; + sourceTree = ""; + }; + D0E1DE171C5EB06B00C7826E /* Objects */ = { + isa = PBXGroup; + children = ( + D0E3A7831B28AE0900A402D9 /* Peer.swift */, + D0E3A79D1B28B50400A402D9 /* Message.swift */, + D0E3A7A11B28B7DC00A402D9 /* Media.swift */, + D08C71391C501F0700779C0F /* MessageHistoryHole.swift */, + ); + name = Objects; + sourceTree = ""; + }; + D0E1DE181C5EB09300C7826E /* Utils */ = { + isa = PBXGroup; + children = ( + D0E3A7871B28AE9C00A402D9 /* Coding.swift */, + D0B76BE61B66639F0095CF45 /* DeferredString.swift */, + D0F9E8741C5A334100037222 /* SimpleDictionary.swift */, + ); + name = Utils; + sourceTree = ""; + }; + D0E1DE191C5EB0B500C7826E /* Value Box */ = { + isa = PBXGroup; + children = ( + D0977F9D1B8234DF009994B2 /* ValueBoxKey.swift */, + D0977F9B1B822DB4009994B2 /* ValueBox.swift */, + D0977F9F1B8244D7009994B2 /* SqliteValueBox.swift */, + D00E0FBD1B85D1B5002E4EB5 /* LmdbValueBox.swift */, + ); + name = "Value Box"; + sourceTree = ""; + }; + D0E1DE1A1C5EB0DB00C7826E /* Views */ = { + isa = PBXGroup; + children = ( + D003E4E51B38DBDB00C22CBC /* MessageHistoryView.swift */, + D0F9E86A1C59719800037222 /* ChatListView.swift */, + ); + name = Views; + sourceTree = ""; + }; D0E3A7401B28A7E300A402D9 = { isa = PBXGroup; children = ( @@ -217,26 +291,14 @@ D075165B1B2EC5B000AE42E0 /* module.private.modulemap */, D075163D1B2D9CEF00AE42E0 /* PostboxPrivate */, D07515FC1B2C44A200AE42E0 /* thirdparty */, - D0E3A7871B28AE9C00A402D9 /* Coding.swift */, - D0B76BE61B66639F0095CF45 /* DeferredString.swift */, - D0E3A7831B28AE0900A402D9 /* Peer.swift */, - D0E3A79D1B28B50400A402D9 /* Message.swift */, - D08C71391C501F0700779C0F /* MessageHistoryHole.swift */, - D0E3A7A11B28B7DC00A402D9 /* Media.swift */, - D003E4E51B38DBDB00C22CBC /* MessageHistoryView.swift */, - D0D225251B4D84930085E26D /* PeerView.swift */, - D0977FA11B82930C009994B2 /* PostboxCodingUtils.swift */, + D0E1DE181C5EB09300C7826E /* Utils */, + D0E1DE171C5EB06B00C7826E /* Objects */, + D0E1DE191C5EB0B500C7826E /* Value Box */, + D0E1DE161C5EB06000C7826E /* Tables */, + D0E1DE1A1C5EB0DB00C7826E /* Views */, D0E3A7811B28ADD000A402D9 /* Postbox.swift */, - D0977FA31B829F87009994B2 /* PostboxTables.swift */, + D0E1DE141C5E1C6900C7826E /* ViewTracker.swift */, D055BD321B7D3D2D00F06C0A /* MediaBox.swift */, - D0977F9D1B8234DF009994B2 /* ValueBoxKey.swift */, - D0977F9B1B822DB4009994B2 /* ValueBox.swift */, - D0977F9F1B8244D7009994B2 /* SqliteValueBox.swift */, - D00E0FBD1B85D1B5002E4EB5 /* LmdbValueBox.swift */, - D08C713B1C51283C00779C0F /* MessageHistoryIndexTable.swift */, - D08C713D1C512EA500779C0F /* MessageHistoryTable.swift */, - D0F9E85A1C565EBB00037222 /* MessageMediaTable.swift */, - D0F9E8621C579F0200037222 /* MediaCleanupTable.swift */, D0E3A74D1B28A7E300A402D9 /* Supporting Files */, ); path = Postbox; @@ -260,6 +322,8 @@ D044E15D1B2ACB9C001EE087 /* CodingTests.swift */, D0A7D9441C556CFE0016A115 /* MessageHistoryIndexTableTests.swift */, D0F9E8601C57766A00037222 /* MessageHistoryTableTests.swift */, + D0F9E8681C58FA9300037222 /* ChatListTableTests.swift */, + D0C8FCB61C5C2D200028C27F /* MessageHistoryViewTests.swift */, ); path = PostboxTests; sourceTree = ""; @@ -390,6 +454,7 @@ files = ( D0F9E8631C579F0200037222 /* MediaCleanupTable.swift in Sources */, D075165C1B2EC5B000AE42E0 /* module.private.modulemap in Sources */, + D0F9E86F1C5A0E7600037222 /* KeychainTable.swift in Sources */, D0E3A7821B28ADD000A402D9 /* Postbox.swift in Sources */, D0E3A79E1B28B50400A402D9 /* Message.swift in Sources */, D044E1631B2AD677001EE087 /* MurMurHash32.m in Sources */, @@ -397,24 +462,29 @@ D07516721B2EC7FE00AE42E0 /* Value.swift in Sources */, D08C713A1C501F0700779C0F /* MessageHistoryHole.swift in Sources */, D0F9E85B1C565EBB00037222 /* MessageMediaTable.swift in Sources */, + D0E1DE151C5E1C6900C7826E /* ViewTracker.swift in Sources */, + D0F9E8751C5A334100037222 /* SimpleDictionary.swift in Sources */, D08C713E1C512EA500779C0F /* MessageHistoryTable.swift in Sources */, - D0977FA41B829F87009994B2 /* PostboxTables.swift in Sources */, + D0F9E8671C58D08900037222 /* ChatListIndexTable.swift in Sources */, D0E3A7A21B28B7DC00A402D9 /* Media.swift in Sources */, D0E3A7881B28AE9C00A402D9 /* Coding.swift in Sources */, D0977F9E1B8234DF009994B2 /* ValueBoxKey.swift in Sources */, D00E0FB91B85D192002E4EB5 /* mdb.c in Sources */, + D0F9E86B1C59719800037222 /* ChatListView.swift in Sources */, D003E4E61B38DBDB00C22CBC /* MessageHistoryView.swift in Sources */, D075165E1B2EC5B500AE42E0 /* module.modulemap in Sources */, D07516791B2EC90400AE42E0 /* SQLite-Bridging.m in Sources */, D0977FA01B8244D7009994B2 /* SqliteValueBox.swift in Sources */, D0D224F21B4D6ABD0085E26D /* Functions.swift in Sources */, D00E0FBA1B85D192002E4EB5 /* midl.c in Sources */, + D0F9E8651C58CB7F00037222 /* ChatListTable.swift in Sources */, + D0F9E8711C5A0E9B00037222 /* PeerTable.swift in Sources */, D07516711B2EC7FE00AE42E0 /* Statement.swift in Sources */, D08C713C1C51283C00779C0F /* MessageHistoryIndexTable.swift in Sources */, - D0D225261B4D84930085E26D /* PeerView.swift in Sources */, + D0F9E86D1C5A0E5D00037222 /* MetadataTable.swift in Sources */, D0E3A7841B28AE0900A402D9 /* Peer.swift in Sources */, + D0F9E8731C5A1EE500037222 /* GlobalMessageIdsTable.swift in Sources */, D055BD331B7D3D2D00F06C0A /* MediaBox.swift in Sources */, - D0977FA21B82930C009994B2 /* PostboxCodingUtils.swift in Sources */, D0977F9C1B822DB4009994B2 /* ValueBox.swift in Sources */, D0B76BE71B66639F0095CF45 /* DeferredString.swift in Sources */, D075166A1B2EC7FE00AE42E0 /* Database.swift in Sources */, @@ -428,6 +498,8 @@ files = ( D044E15E1B2ACB9C001EE087 /* CodingTests.swift in Sources */, D0F9E8611C57766A00037222 /* MessageHistoryTableTests.swift in Sources */, + D0C8FCB71C5C2D200028C27F /* MessageHistoryViewTests.swift in Sources */, + D0F9E8691C58FA9300037222 /* ChatListTableTests.swift in Sources */, D0A7D9451C556CFE0016A115 /* MessageHistoryIndexTableTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Postbox/ChatListIndexTable.swift b/Postbox/ChatListIndexTable.swift new file mode 100644 index 0000000000..f25db86b63 --- /dev/null +++ b/Postbox/ChatListIndexTable.swift @@ -0,0 +1,45 @@ +import Foundation + +final class ChatListIndexTable { + let valueBox: ValueBox + let tableId: Int32 + + init(valueBox: ValueBox, tableId: Int32) { + self.valueBox = valueBox + self.tableId = tableId + } + + private func key(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + return key + } + + func set(index: MessageIndex) { + let writeBuffer = WriteBuffer() + var idNamespace: Int32 = index.id.namespace + var idId: Int32 = index.id.id + var idTimestamp: Int32 = index.timestamp + writeBuffer.write(&idNamespace, offset: 0, length: 4) + writeBuffer.write(&idId, offset: 0, length: 4) + writeBuffer.write(&idTimestamp, offset: 0, length: 4) + self.valueBox.set(self.tableId, key: self.key(index.id.peerId), value: writeBuffer.readBufferNoCopy()) + } + + func remove(peerId: PeerId) { + self.valueBox.remove(self.tableId, key: self.key(peerId)) + } + + func get(peerId: PeerId) -> MessageIndex? { + if let value = self.valueBox.get(self.tableId, key: self.key(peerId)) { + var idNamespace: Int32 = 0 + var idId: Int32 = 0 + var idTimestamp: Int32 = 0 + value.read(&idNamespace, offset: 0, length: 4) + value.read(&idId, offset: 0, length: 4) + value.read(&idTimestamp, offset: 0, length: 4) + return MessageIndex(id: MessageId(peerId: peerId, namespace: idNamespace, id: idId), timestamp: idTimestamp) + } + return nil + } +} diff --git a/Postbox/ChatListTable.swift b/Postbox/ChatListTable.swift new file mode 100644 index 0000000000..59734c80d1 --- /dev/null +++ b/Postbox/ChatListTable.swift @@ -0,0 +1,156 @@ +import Foundation + +enum ChatListOperation { + case InsertMessage(IntermediateMessage) + case InsertNothing(MessageIndex) + case Remove([MessageIndex]) +} + +enum ChatListIntermediateEntry { + case Message(IntermediateMessage) + case Nothing(MessageIndex) +} + +final class ChatListTable { + let valueBox: ValueBox + let tableId: Int32 + let indexTable: ChatListIndexTable + + init(valueBox: ValueBox, tableId: Int32, indexTable: ChatListIndexTable) { + self.valueBox = valueBox + self.tableId = tableId + self.indexTable = indexTable + } + + private func key(index: MessageIndex) -> ValueBoxKey { + let key = ValueBoxKey(length: 4 + 4 + 4 + 8) + key.setInt32(0, value: index.timestamp) + key.setInt32(4, value: index.id.namespace) + key.setInt32(4 + 4, value: index.id.id) + key.setInt64(4 + 4 + 4, value: index.id.peerId.toInt64()) + return key + } + + private func lowerBound() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: 0) + return key + } + + private func upperBound() -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: Int32.max) + return key + } + + func replay(historyOperationsByPeerId: [PeerId : [MessageHistoryOperation]], messageHistoryTable: MessageHistoryTable, inout operations: [ChatListOperation]) { + for (peerId, _) in historyOperationsByPeerId { + let currentIndex: MessageIndex? = self.indexTable.get(peerId) + if let topMessage = messageHistoryTable.topMessage(peerId) { + let index = MessageIndex(id: topMessage.id, timestamp: topMessage.timestamp) + + if let currentIndex = currentIndex where currentIndex != index { + self.justRemove(currentIndex) + } + if let currentIndex = currentIndex { + operations.append(.Remove([currentIndex])) + } + self.indexTable.set(index) + self.justInsert(index) + operations.append(.InsertMessage(topMessage)) + } else { + if let currentIndex = currentIndex { + operations.append(.Remove([currentIndex])) + operations.append(.InsertNothing(currentIndex)) + } + } + } + } + + func justInsert(index: MessageIndex) { + self.valueBox.set(self.tableId, key: self.key(index), value: MemoryBuffer(memory: nil, capacity: 0, length: 0, freeWhenDone: false)) + } + + func justRemove(index: MessageIndex) { + self.valueBox.remove(self.tableId, key: self.key(index)) + } + + func entriesAround(index: MessageIndex, messageHistoryTable: MessageHistoryTable, count: Int) -> [ChatListIntermediateEntry] { + var lowerEntries: [ChatListIntermediateEntry] = [] + + self.valueBox.range(self.tableId, start: self.key(index).successor, end: self.lowerBound(), keys: { key in + let index = MessageIndex(id: MessageId(peerId: PeerId(key.getInt64(4 + 4 + 4)), namespace: key.getInt32(4), id: key.getInt32(4 + 4)), timestamp: key.getInt32(0)) + if let message = messageHistoryTable.getMessage(index) { + lowerEntries.append(.Message(message)) + } else { + lowerEntries.append(.Nothing(index)) + } + return true + }, limit: count) + + var upperEntries: [ChatListIntermediateEntry] = [] + self.valueBox.range(self.tableId, start: self.key(index), end: self.upperBound(), keys: { key in + let index = MessageIndex(id: MessageId(peerId: PeerId(key.getInt64(4 + 4 + 4)), namespace: key.getInt32(4), id: key.getInt32(4 + 4)), timestamp: key.getInt32(0)) + if let message = messageHistoryTable.getMessage(index) { + upperEntries.append(.Message(message)) + } else { + upperEntries.append(.Nothing(index)) + } + return true + }, limit: count) + + var entries: [ChatListIntermediateEntry] = [] + for entry in lowerEntries.reverse() { + entries.append(entry) + } + entries.appendContentsOf(upperEntries) + + return entries + } + + func earlierEntries(index: MessageIndex?, messageHistoryTable: MessageHistoryTable, count: Int) -> [ChatListIntermediateEntry] { + var entries: [ChatListIntermediateEntry] = [] + let key: ValueBoxKey + if let index = index { + key = self.key(index) + } else { + key = self.upperBound() + } + + self.valueBox.range(self.tableId, start: key, end: self.lowerBound(), keys: { key in + let index = MessageIndex(id: MessageId(peerId: PeerId(key.getInt64(4 + 4 + 4)), namespace: key.getInt32(4), id: key.getInt32(4 + 4)), timestamp: key.getInt32(0)) + if let message = messageHistoryTable.getMessage(index) { + entries.append(.Message(message)) + } else { + entries.append(.Nothing(index)) + } + return true + }, limit: count) + return entries + } + + func laterEntries(index: MessageIndex?, messageHistoryTable: MessageHistoryTable, count: Int) -> [ChatListIntermediateEntry] { + var entries: [ChatListIntermediateEntry] = [] + let key: ValueBoxKey + if let index = index { + key = self.key(index) + } else { + key = self.lowerBound() + } + + self.valueBox.range(self.tableId, start: key, end: self.upperBound(), keys: { key in + let index = MessageIndex(id: MessageId(peerId: PeerId(key.getInt64(4 + 4 + 4)), namespace: key.getInt32(4), id: key.getInt32(4 + 4)), timestamp: key.getInt32(0)) + if let message = messageHistoryTable.getMessage(index) { + entries.append(.Message(message)) + } else { + entries.append(.Nothing(index)) + } + return true + }, limit: count) + return entries + } + + func debugList(messageHistoryTable: MessageHistoryTable) -> [ChatListIntermediateEntry] { + return self.entriesAround(MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: 0), timestamp: 0), messageHistoryTable: messageHistoryTable, count: 1000) + } +} diff --git a/Postbox/ChatListView.swift b/Postbox/ChatListView.swift new file mode 100644 index 0000000000..f8a197bb38 --- /dev/null +++ b/Postbox/ChatListView.swift @@ -0,0 +1,312 @@ +import Foundation + +public enum ChatListEntry: Comparable { + case MessageEntry(Message) + case Nothing(MessageIndex) + + public var index: MessageIndex { + switch self { + case let .MessageEntry(message): + return MessageIndex(message) + case let .Nothing(index): + return index + } + } +} + +public func ==(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool { + return lhs.index == rhs.index +} + +public func <(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool { + return lhs.index < rhs.index +} + +enum MutableChatListEntry { + case IntermediateMessageEntry(IntermediateMessage) + case MessageEntry(Message) + case Nothing(MessageIndex) + + var index: MessageIndex { + switch self { + case let .IntermediateMessageEntry(message): + return MessageIndex(id: message.id, timestamp: message.timestamp) + case let .MessageEntry(message): + return MessageIndex(message) + case let .Nothing(index): + return index + } + } +} + +final class MutableChatListViewReplayContext { + var invalidEarlier: Bool = false + var invalidLater: Bool = false + var removedEntries: Bool = false + + func empty() -> Bool { + return !self.removedEntries && !invalidEarlier && !invalidLater + } +} + +final class MutableChatListView { + private var earlier: MutableChatListEntry? + private var later: MutableChatListEntry? + private var entries: [MutableChatListEntry] + private var count: Int + + init(earlier: MutableChatListEntry?, entries: [MutableChatListEntry], later: MutableChatListEntry?, count: Int) { + self.earlier = earlier + self.entries = entries + self.later = later + self.count = count + } + + func replay(operations: [ChatListOperation], context: MutableChatListViewReplayContext) -> Bool { + var hasChanges = false + for operation in operations { + switch operation { + case let .InsertMessage(message): + if self.add(.IntermediateMessageEntry(message)) { + hasChanges = true + } + case let .InsertNothing(index): + if self.add(.Nothing(index)) { + hasChanges = true + } + case let .Remove(indices): + if self.remove(Set(indices), context: context) { + hasChanges = true + } + } + } + return hasChanges + } + + func add(entry: MutableChatListEntry) -> 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 + + var next: MessageIndex? + if let later = self.later { + next = later.index + } + + let index = entry.index + + if index < last { + if self.earlier == nil || self.earlier!.index < index { + if self.entries.count < self.count { + self.entries.insert(entry, atIndex: 0) + } else { + self.earlier = entry + } + 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 + } + 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 { + var i = self.entries.count + while i >= 1 { + if self.entries[i - 1].index < index { + break + } + i-- + } + 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 + } + } + } + + func remove(indices: Set, context: MutableChatListViewReplayContext) -> Bool { + var hasChanges = false + if let earlier = self.earlier where indices.contains(earlier.index) { + context.invalidEarlier = true + hasChanges = true + } + + if let later = self.later where indices.contains(later.index) { + context.invalidLater = true + hasChanges = true + } + + if self.entries.count != 0 { + var i = self.entries.count - 1 + while i >= 0 { + if indices.contains(self.entries[i].index) { + self.entries.removeAtIndex(i) + context.removedEntries = true + hasChanges = true + } + i-- + } + } + + return hasChanges + } + + func complete(context: MutableChatListViewReplayContext, fetchEarlier: (MessageIndex?, Int) -> [MutableChatListEntry], fetchLater: (MessageIndex?, Int) -> [MutableChatListEntry]) { + if context.removedEntries { + var addedEntries: [MutableChatListEntry] = [] + + 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-- + } + 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 + } + i-- + } + } + + 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-- + } + + self.earlier = nil + if anchorIndex - self.count >= 0 { + self.earlier = addedEntries[anchorIndex - self.count] + } + } else { + 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 + } + } + } + + func updatePeers(peers: [PeerId: Peer]) -> Bool { + var hasChanges = false + /*for i in 0 ..< self.entries.count { + switch self.entries[i] { + case let .MessageEntry(message): + var updatedAuthor: Peer? + if let author = message.author, let peer = peers[author.id] { + updatedAuthor = peer + } + + for peer in message.peers { + + } + + break + default: + break + } + }*/ + return hasChanges + } + + func render(renderMessage: IntermediateMessage -> Message) { + for i in 0 ..< self.entries.count { + if case let .IntermediateMessageEntry(message) = self.entries[i] { + self.entries[i] = .MessageEntry(renderMessage(message)) + } + } + } +} + +public final class ChatListView { + public let entries: [ChatListEntry] + + init(_ mutableView: MutableChatListView) { + var entries: [ChatListEntry] = [] + for entry in mutableView.entries { + switch entry { + case let .MessageEntry(message): + entries.append(.MessageEntry(message)) + case let .Nothing(index): + entries.append(.Nothing(index)) + case .IntermediateMessageEntry: + assertionFailure() + } + } + self.entries = entries + } +} diff --git a/Postbox/Coding.swift b/Postbox/Coding.swift index e084d99829..96db1c7b43 100644 --- a/Postbox/Coding.swift +++ b/Postbox/Coding.swift @@ -174,6 +174,10 @@ public final class Encoder { return self.buffer.makeReadBufferAndReset() } + public func readBufferNoCopy() -> ReadBuffer { + return self.buffer.readBufferNoCopy() + } + public func makeData() -> NSData { return self.buffer.makeData() } diff --git a/Postbox/GlobalMessageIdsTable.swift b/Postbox/GlobalMessageIdsTable.swift new file mode 100644 index 0000000000..a95d66f88e --- /dev/null +++ b/Postbox/GlobalMessageIdsTable.swift @@ -0,0 +1,45 @@ +import Foundation + +final class GlobalMessageIdsTable { + let valueBox: ValueBox + let tableId: Int32 + let namespace: Int32 + + let sharedKey = ValueBoxKey(length: 4) + let sharedBuffer = WriteBuffer() + + init(valueBox: ValueBox, tableId: Int32, namespace: Int32) { + self.valueBox = valueBox + self.tableId = tableId + self.namespace = namespace + } + + private func key(id: Int32) -> ValueBoxKey { + self.sharedKey.setInt32(0, value: id) + return self.sharedKey + } + + func set(globalId: Int32, id: MessageId) { + self.sharedBuffer.reset() + var idPeerId: Int64 = id.peerId.toInt64() + var idNamespace: Int32 = id.namespace + self.sharedBuffer.write(&idPeerId, offset: 0, length: 8) + self.sharedBuffer.write(&idNamespace, offset: 0, length: 4) + self.valueBox.set(self.tableId, key: self.key(globalId), value: self.sharedBuffer) + } + + func get(globalId: Int32) -> MessageId? { + if let value = self.valueBox.get(self.tableId, key: self.key(globalId)) { + var idPeerId: Int64 = 0 + var idNamespace: Int32 = 0 + value.read(&idPeerId, offset: 0, length: 8) + value.read(&idNamespace, offset: 0, length: 4) + return MessageId(peerId: PeerId(idPeerId), namespace: idNamespace, id: globalId) + } + return nil + } + + func remove(globalId: Int32) { + self.valueBox.remove(self.tableId, key: self.key(globalId)) + } +} \ No newline at end of file diff --git a/Postbox/KeychainTable.swift b/Postbox/KeychainTable.swift new file mode 100644 index 0000000000..34da757a82 --- /dev/null +++ b/Postbox/KeychainTable.swift @@ -0,0 +1,30 @@ +import Foundation + +final class KeychainTable { + let valueBox: ValueBox + let tableId: Int32 + + init(valueBox: ValueBox, tableId: Int32) { + self.valueBox = valueBox + self.tableId = tableId + } + + private func key(string: String) -> ValueBoxKey { + return ValueBoxKey(string) + } + + func get(key: String) -> NSData? { + if let value = self.valueBox.get(self.tableId, key: self.key(key)) { + return NSData(bytes: value.memory, length: value.length) + } + return nil + } + + func set(key: String, value: NSData) { + self.valueBox.set(self.tableId, key: self.key(key), value: MemoryBuffer(data: value)) + } + + func remove(key: String) { + self.valueBox.remove(self.tableId, key: self.key(key)) + } +} diff --git a/Postbox/Media.swift b/Postbox/Media.swift index 4cd9848945..4d7d771546 100644 --- a/Postbox/Media.swift +++ b/Postbox/Media.swift @@ -70,6 +70,7 @@ public func ==(lhs: MediaId, rhs: MediaId) -> Bool { public protocol Media: Coding { var id: MediaId? { get } + var peerIds: [PeerId] { get } func isEqual(other: Media) -> Bool } diff --git a/Postbox/Message.swift b/Postbox/Message.swift index 54ac816893..472182614a 100644 --- a/Postbox/Message.swift +++ b/Postbox/Message.swift @@ -89,7 +89,7 @@ public func <(lhs: MessageId, rhs: MessageId) -> Bool { } } -public struct MessageIndex: Equatable, Comparable { +public struct MessageIndex: Equatable, Comparable, Hashable { public let id: MessageId public let timestamp: Int32 @@ -115,6 +115,14 @@ public struct MessageIndex: Equatable, Comparable { public func successor() -> MessageIndex { return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace, id: self.id.id + 1), timestamp: self.timestamp) } + + public var hashValue: Int { + return self.id.hashValue + } + + static func upperBound(peerId: PeerId) -> MessageIndex { + return MessageIndex(id: MessageId(peerId: peerId, namespace: Int32.max, id: Int32.max), timestamp: Int32.max) + } } public func ==(lhs: MessageIndex, rhs: MessageIndex) -> Bool { @@ -134,30 +142,36 @@ public func <(lhs: MessageIndex, rhs: MessageIndex) -> Bool { } public class Message { - let id: MessageId - let timestamp: Int32 - let text: String - let attributes: [Coding] - let media: [Media] + public let id: MessageId + public let timestamp: Int32 + public let author: Peer? + public let text: String + public let attributes: [Coding] + public let media: [Media] + public let peers: SimpleDictionary - init(id: MessageId, timestamp: Int32, text: String, attributes: [Coding], media: [Media]) { + init(id: MessageId, timestamp: Int32, author: Peer?, text: String, attributes: [Coding], media: [Media], peers: SimpleDictionary) { self.id = id self.timestamp = timestamp + self.author = author self.text = text self.attributes = attributes self.media = media + self.peers = peers } } -class StoreMessage { - let id: MessageId - let timestamp: Int32 - let text: String - let attributes: [Coding] - let media: [Media] +public final class StoreMessage { + public let id: MessageId + public let timestamp: Int32 + public let authorId: PeerId? + public let text: String + public let attributes: [Coding] + public let media: [Media] - init(id: MessageId, timestamp: Int32, text: String, attributes: [Coding], media: [Media]) { + public init(id: MessageId, timestamp: Int32, authorId: PeerId?, text: String, attributes: [Coding], media: [Media]) { self.id = id + self.authorId = authorId self.timestamp = timestamp self.text = text self.attributes = attributes @@ -168,47 +182,19 @@ class StoreMessage { class IntermediateMessage { let id: MessageId let timestamp: Int32 + let authorId: PeerId? let text: String let attributesData: ReadBuffer let embeddedMediaData: ReadBuffer let referencedMedia: [MediaId] - init(id: MessageId, timestamp: Int32, text: String, attributesData: ReadBuffer, embeddedMediaData: ReadBuffer, referencedMedia: [MediaId]) { + init(id: MessageId, timestamp: Int32, authorId: PeerId?, text: String, attributesData: ReadBuffer, embeddedMediaData: ReadBuffer, referencedMedia: [MediaId]) { self.id = id self.timestamp = timestamp + self.authorId = authorId self.text = text self.attributesData = attributesData self.embeddedMediaData = embeddedMediaData self.referencedMedia = referencedMedia } } - -public struct RenderedMessage: Equatable, Comparable { - public let message: Message - - internal let incomplete: Bool - public let peers: [Peer] - public let media: [Media] - - internal init(message: Message) { - self.message = message - self.peers = [] - self.media = [] - self.incomplete = true - } - - internal init(message: Message, peers: [Peer], media: [Media]) { - self.message = message - self.peers = peers - self.media = media - self.incomplete = false - } -} - -public func ==(lhs: RenderedMessage, rhs: RenderedMessage) -> Bool { - return lhs.message.id == rhs.message.id -} - -public func <(lhs: RenderedMessage, rhs: RenderedMessage) -> Bool { - return MessageIndex(lhs.message) < MessageIndex(rhs.message) -} diff --git a/Postbox/MessageHistoryHole.swift b/Postbox/MessageHistoryHole.swift index e43822bba9..b360771793 100644 --- a/Postbox/MessageHistoryHole.swift +++ b/Postbox/MessageHistoryHole.swift @@ -1,12 +1,16 @@ import Foundation -public struct MessageHistoryHole: Equatable { - let maxIndex: MessageIndex - let min: MessageId.Id +public struct MessageHistoryHole: Equatable, CustomStringConvertible { + public let maxIndex: MessageIndex + public let min: MessageId.Id var id: MessageId { return maxIndex.id } + + public var description: String { + return "MessageHistoryHole(peerId: \(self.maxIndex.id.peerId), min: \(self.min), max: \(self.maxIndex.id.id))" + } } public func ==(lhs: MessageHistoryHole, rhs: MessageHistoryHole) -> Bool { diff --git a/Postbox/MessageHistoryIndexTable.swift b/Postbox/MessageHistoryIndexTable.swift index feb548012c..228e1f7d84 100644 --- a/Postbox/MessageHistoryIndexTable.swift +++ b/Postbox/MessageHistoryIndexTable.swift @@ -3,14 +3,34 @@ import Foundation enum HistoryIndexEntry { case Message(MessageIndex) case Hole(MessageHistoryHole) + + var index: MessageIndex { + switch self { + case let .Message(index): + return index + case let .Hole(hole): + return hole.maxIndex + } + } } -enum HoleFillType { +public enum HoleFillType { case UpperToLower case LowerToUpper case Complete } +public enum AddMessagesLocation { + case Random + case UpperHistoryBlock +} + +enum MessageHistoryIndexOperation { + case InsertMessage(StoreMessage) + case InsertHole(MessageHistoryHole) + case Remove(MessageIndex) +} + private func readHistoryIndexEntry(peerId: PeerId, namespace: MessageId.Namespace, key: ValueBoxKey, value: ReadBuffer) -> HistoryIndexEntry { var type: Int8 = 0 value.read(&type, offset: 0, length: 1) @@ -30,13 +50,17 @@ private func readHistoryIndexEntry(peerId: PeerId, namespace: MessageId.Namespac final class MessageHistoryIndexTable { let valueBox: ValueBox let tableId: Int32 + let globalMessageIdsNamespace: Int32 + let globalMessageIdsTable: GlobalMessageIdsTable - init(valueBox: ValueBox, tableId: Int32) { + init(valueBox: ValueBox, tableId: Int32, globalMessageIdsTable: GlobalMessageIdsTable) { self.valueBox = valueBox self.tableId = tableId + self.globalMessageIdsTable = globalMessageIdsTable + self.globalMessageIdsNamespace = globalMessageIdsTable.namespace } - func key(id: MessageId) -> ValueBoxKey { + private func key(id: MessageId) -> ValueBoxKey { let key = ValueBoxKey(length: 8 + 4 + 4) key.setInt64(0, value: id.peerId.toInt64()) key.setInt32(8, value: id.namespace) @@ -44,35 +68,23 @@ final class MessageHistoryIndexTable { return key } - func lowerBound(peerId: PeerId, namespace: MessageId.Namespace) -> ValueBoxKey { + private func lowerBound(peerId: PeerId, namespace: MessageId.Namespace) -> ValueBoxKey { let key = ValueBoxKey(length: 8 + 4) key.setInt64(0, value: peerId.toInt64()) key.setInt32(8, value: namespace) return key } - func upperBound(peerId: PeerId, namespace: MessageId.Namespace) -> ValueBoxKey { + private func upperBound(peerId: PeerId, namespace: MessageId.Namespace) -> ValueBoxKey { let key = ValueBoxKey(length: 8 + 4) key.setInt64(0, value: peerId.toInt64()) key.setInt32(8, value: namespace) return key.successor } - func addHole(id: MessageId) { + func addHole(id: MessageId, inout operations: [MessageHistoryIndexOperation]) { let adjacent = self.adjacentItems(id) - /* - - 1. [x] nothing - 2. [x] message - * - nothing - 3. [x] nothing - * - message - 4. [x] nothing - * - hole - 5. [x] message - * - message - 6. [x] hole - * - message - 7. [x] message - * - hole - - */ - if let lowerItem = adjacent.lower, upperItem = adjacent.upper { switch lowerItem { case .Hole: @@ -83,7 +95,7 @@ final class MessageHistoryIndexTable { break case let .Message(upperMessage): if lowerMessage.id.id < upperMessage.id.id - 1 { - self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: upperMessage.id.id - 1), timestamp: upperMessage.timestamp), min: lowerMessage.id.id + 1)) + self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: upperMessage.id.id - 1), timestamp: upperMessage.timestamp), min: lowerMessage.id.id + 1), operations: &operations) } break } @@ -91,7 +103,7 @@ final class MessageHistoryIndexTable { } else if let lowerItem = adjacent.lower { switch lowerItem { case let .Message(lowerMessage): - self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: Int32.max), timestamp: Int32.max), min: lowerMessage.id.id + 1)) + self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: Int32.max), timestamp: Int32.max), min: lowerMessage.id.id + 1), operations: &operations) case .Hole: break } @@ -99,46 +111,104 @@ final class MessageHistoryIndexTable { switch upperItem { case let .Message(upperMessage): if upperMessage.id.id > 1 { - self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: upperMessage.id.id - 1), timestamp: upperMessage.timestamp), min: 1)) + self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: upperMessage.id.id - 1), timestamp: upperMessage.timestamp), min: 1), operations: &operations) } case .Hole: break } } else { - self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: Int32.max), timestamp: Int32.max), min: 1)) + self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: Int32.max), timestamp: Int32.max), min: 1), operations: &operations) } } - func addMessage(index: MessageIndex) { - var upperItem: HistoryIndexEntry? - self.valueBox.range(self.tableId, start: self.key(index.id).predecessor, end: self.upperBound(index.id.peerId, namespace: index.id.namespace), values: { key, value in - upperItem = readHistoryIndexEntry(index.id.peerId, namespace: index.id.namespace, key: key, value: value) - return true - }, limit: 1) + func addMessages(messages: [StoreMessage], location: AddMessagesLocation, inout operations: [MessageHistoryIndexOperation]) { + if messages.count == 0 { + return + } - if let upperItem = upperItem { - switch upperItem { - case let .Hole(upperHole): - self.justRemove(upperHole.id) - if upperHole.maxIndex.id.id > index.id.id + 1 { - self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: index.id.id + 1)) + switch location { + case .UpperHistoryBlock: + var lowerIds = SimpleDictionary>() + for message in messages { + if lowerIds[message.id.peerId] == nil { + var dict = SimpleDictionary() + dict[message.id.namespace] = MessageIndex(id: message.id, timestamp: message.timestamp) + lowerIds[message.id.peerId] = dict + } else { + let lowerIndex = lowerIds[message.id.peerId]![message.id.namespace] + if lowerIndex == nil || lowerIndex!.id.id > message.id.id { + lowerIds[message.id.peerId]![message.id.namespace] = MessageIndex(id: message.id, timestamp: message.timestamp) + } } - if upperHole.min <= index.id.id - 1 { - self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: index.id.peerId, namespace: index.id.namespace, id: index.id.id - 1), timestamp: index.timestamp), min: upperHole.min)) + } + + for (peerId, lowerIdsByNamespace) in lowerIds { + for (namespace, lowerIndex) in lowerIdsByNamespace { + var removeHoles: [MessageIndex] = [] + var modifyHole: (MessageIndex, MessageHistoryHole)? + self.valueBox.range(self.tableId, start: self.key(MessageId(peerId: peerId, namespace: namespace, id: lowerIndex.id.id)), end: self.upperBound(peerId, namespace: namespace), values: { key, value in + let entry = readHistoryIndexEntry(peerId, namespace: namespace, key: key, value: value) + if case let .Hole(hole) = entry { + if lowerIndex.id.id <= hole.min { + removeHoles.append(hole.maxIndex) + } else { + modifyHole = (hole.maxIndex, MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: lowerIndex.id.id - 1), timestamp: lowerIndex.timestamp), min: hole.min)) + } + } + return true + }, limit: 0) + + for index in removeHoles { + self.justRemove(index, operations: &operations) + } + + if let modifyHole = modifyHole { + self.justRemove(modifyHole.0, operations: &operations) + self.justInsertHole(modifyHole.1, operations: &operations) + } } - break - case .Message: - break + } + case .Random: + break + } + + for message in messages { + let index = MessageIndex(id: message.id, timestamp: message.timestamp) + + var upperItem: HistoryIndexEntry? + self.valueBox.range(self.tableId, start: self.key(index.id).predecessor, end: self.upperBound(index.id.peerId, namespace: index.id.namespace), values: { key, value in + upperItem = readHistoryIndexEntry(index.id.peerId, namespace: index.id.namespace, key: key, value: value) + return true + }, limit: 1) + + var exists = false + + if let upperItem = upperItem { + switch upperItem { + case let .Hole(upperHole): + self.justRemove(upperHole.maxIndex, operations: &operations) + if upperHole.maxIndex.id.id > index.id.id + 1 { + self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: index.id.id + 1), operations: &operations) + } + if upperHole.min <= index.id.id - 1 { + self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: index.id.peerId, namespace: index.id.namespace, id: index.id.id - 1), timestamp: index.timestamp), min: upperHole.min), operations: &operations) + } + case let .Message(messageIndex): + if messageIndex.id == index.id { + exists = true + } + } + } + + if !exists { + self.justInsertMessage(message, operations: &operations) } } - - self.justInsertMessage(index) } - func removeMessage(id: MessageId) { - let key = self.key(id) - if self.valueBox.exists(self.tableId, key: key) { - self.justRemove(id) + func removeMessage(id: MessageId, inout operations: [MessageHistoryIndexOperation]) { + if let existingEntry = self.get(id) { + self.justRemove(existingEntry.index, operations: &operations) let adjacent = self.adjacentItems(id) @@ -147,27 +217,27 @@ final class MessageHistoryIndexTable { case let .Message(lowerMessage): switch upperItem { case let .Hole(upperHole): - self.justRemove(upperHole.id) - self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: lowerMessage.id.id + 1)) + self.justRemove(upperHole.maxIndex, operations: &operations) + self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: lowerMessage.id.id + 1), operations: &operations) case .Message: break } case let .Hole(lowerHole): switch upperItem { case let .Hole(upperHole): - self.justRemove(lowerHole.id) - self.justRemove(upperHole.id) - self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: lowerHole.min)) + self.justRemove(lowerHole.maxIndex, operations: &operations) + self.justRemove(upperHole.maxIndex, operations: &operations) + self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: lowerHole.min), operations: &operations) case let .Message(upperMessage): - self.justRemove(lowerHole.id) - self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: upperMessage.id.id - 1), timestamp: upperMessage.timestamp), min: lowerHole.min)) + self.justRemove(lowerHole.maxIndex, operations: &operations) + self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: upperMessage.id.id - 1), timestamp: upperMessage.timestamp), min: lowerHole.min), operations: &operations) } } } else if let lowerItem = adjacent.lower { switch lowerItem { case let .Hole(lowerHole): - self.justRemove(lowerHole.id) - self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: Int32.max), timestamp: Int32.max), min: lowerHole.min)) + self.justRemove(lowerHole.maxIndex, operations: &operations) + self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: Int32.max), timestamp: Int32.max), min: lowerHole.min), operations: &operations) break case .Message: break @@ -175,8 +245,8 @@ final class MessageHistoryIndexTable { } else if let upperItem = adjacent.upper { switch upperItem { case let .Hole(upperHole): - self.justRemove(upperHole.id) - self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: 1)) + self.justRemove(upperHole.maxIndex, operations: &operations) + self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: 1), operations: &operations) break case .Message: break @@ -185,42 +255,42 @@ final class MessageHistoryIndexTable { } } - func fillHole(id: MessageId, fillType: HoleFillType, indices: [MessageIndex]) { + func fillHole(id: MessageId, fillType: HoleFillType, messages: [StoreMessage], inout operations: [MessageHistoryIndexOperation]) { var upperItem: HistoryIndexEntry? self.valueBox.range(self.tableId, start: self.key(id).predecessor, 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) - let sortedByIdIndices = indices.sort({$0.id < $1.id}) - var remainingIndices = sortedByIdIndices + let sortedByIdMessages = messages.sort({$0.id < $1.id}) + var remainingMessages = sortedByIdMessages if let upperItem = upperItem { switch upperItem { case let .Hole(upperHole): var i = 0 - var minIndexInRange: MessageIndex? - var maxIndexInRange: MessageIndex? + var minMessageInRange: StoreMessage? + var maxMessageInRange: StoreMessage? var removedHole = false - while i < remainingIndices.count { - let index = remainingIndices[i] - if index.id.id >= upperHole.min && index.id.id <= upperHole.maxIndex.id.id { - if (fillType == .UpperToLower || fillType == .Complete) && (minIndexInRange == nil || minIndexInRange!.id > index.id) { - minIndexInRange = index + 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) { + minMessageInRange = message if !removedHole { removedHole = true - self.justRemove(upperHole.id) + self.justRemove(upperHole.maxIndex, operations: &operations) } } - if (fillType == .LowerToUpper || fillType == .Complete) && (maxIndexInRange == nil || maxIndexInRange!.id < index.id) { - maxIndexInRange = index + if (fillType == .LowerToUpper || fillType == .Complete) && (maxMessageInRange == nil || maxMessageInRange!.id < message.id) { + maxMessageInRange = message if !removedHole { removedHole = true - self.justRemove(upperHole.id) + self.justRemove(upperHole.maxIndex, operations: &operations) } } - self.justInsertMessage(index) - remainingIndices.removeAtIndex(i) + self.justInsertMessage(message, operations: &operations) + remainingMessages.removeAtIndex(i) } else { i++ } @@ -228,15 +298,15 @@ final class MessageHistoryIndexTable { switch fillType { case .Complete: if !removedHole { - self.justRemove(upperHole.id) + self.justRemove(upperHole.maxIndex, operations: &operations) } case .LowerToUpper: - if let maxIndexInRange = maxIndexInRange where maxIndexInRange.id.id != Int32.max && maxIndexInRange.id.id + 1 <= upperHole.maxIndex.id.id { - self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: maxIndexInRange.id.id + 1)) + if let maxMessageInRange = maxMessageInRange where maxMessageInRange.id.id != Int32.max && maxMessageInRange.id.id + 1 <= upperHole.maxIndex.id.id { + self.justInsertHole(MessageHistoryHole(maxIndex: upperHole.maxIndex, min: maxMessageInRange.id.id + 1), operations: &operations) } case .UpperToLower: - if let minIndexInRange = minIndexInRange where minIndexInRange.id.id - 1 >= upperHole.min { - self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: minIndexInRange.id.id - 1), timestamp: minIndexInRange.timestamp), min: upperHole.min)) + if let minMessageInRange = minMessageInRange where minMessageInRange.id.id - 1 >= upperHole.min { + self.justInsertHole(MessageHistoryHole(maxIndex: MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: minMessageInRange.id.id - 1), timestamp: minMessageInRange.timestamp), min: upperHole.min), operations: &operations) } } break @@ -245,12 +315,12 @@ final class MessageHistoryIndexTable { } } - for index in remainingIndices { - self.addMessage(index) + for message in remainingMessages { + self.addMessages([message], location: .Random, operations: &operations) } } - func justInsertHole(hole: MessageHistoryHole) { + func justInsertHole(hole: MessageHistoryHole, inout operations: [MessageHistoryIndexOperation]) { let value = WriteBuffer() var type: Int8 = 1 var timestamp: Int32 = hole.maxIndex.timestamp @@ -259,19 +329,33 @@ final class MessageHistoryIndexTable { value.write(×tamp, offset: 0, length: 4) value.write(&min, offset: 0, length: 4) self.valueBox.set(self.tableId, key: self.key(hole.id), value: value) + + operations.append(.InsertHole(hole)) } - func justInsertMessage(index: MessageIndex) { + func justInsertMessage(message: StoreMessage, inout operations: [MessageHistoryIndexOperation]) { + let index = MessageIndex(id: message.id, timestamp: message.timestamp) + let value = WriteBuffer() var type: Int8 = 0 var timestamp: Int32 = index.timestamp value.write(&type, offset: 0, length: 1) value.write(×tamp, offset: 0, length: 4) self.valueBox.set(self.tableId, key: self.key(index.id), value: value) + + operations.append(.InsertMessage(message)) + if index.id.namespace == self.globalMessageIdsNamespace { + self.globalMessageIdsTable.set(index.id.id, id: index.id) + } } - func justRemove(id: MessageId) { - self.valueBox.remove(self.tableId, key: self.key(id)) + func justRemove(index: MessageIndex, inout operations: [MessageHistoryIndexOperation]) { + self.valueBox.remove(self.tableId, key: self.key(index.id)) + + operations.append(.Remove(index)) + if index.id.namespace == self.globalMessageIdsNamespace { + self.globalMessageIdsTable.remove(index.id.id) + } } func adjacentItems(id: MessageId) -> (lower: HistoryIndexEntry?, upper: HistoryIndexEntry?) { @@ -300,16 +384,6 @@ final class MessageHistoryIndexTable { return nil } - func messageExists(id: MessageId) -> Bool { - if let entry = self.get(id) { - if case .Message = entry { - return true - } - } - - return false - } - func debugList(peerId: PeerId, namespace: MessageId.Namespace) -> [HistoryIndexEntry] { var list: [HistoryIndexEntry] = [] self.valueBox.range(self.tableId, start: self.lowerBound(peerId, namespace: namespace), end: self.upperBound(peerId, namespace: namespace), values: { key, value in diff --git a/Postbox/MessageHistoryTable.swift b/Postbox/MessageHistoryTable.swift index 9600d5705f..f649976f91 100644 --- a/Postbox/MessageHistoryTable.swift +++ b/Postbox/MessageHistoryTable.swift @@ -1,19 +1,39 @@ import Foundation -enum MessageHistoryEntry { - case Msg(Message) +enum MessageHistoryOperation { + case InsertMessage(IntermediateMessage) + case InsertHole(MessageHistoryHole) + case Remove([MessageIndex]) +} + +enum IntermediateMessageHistoryEntry { + case Message(IntermediateMessage) case Hole(MessageHistoryHole) var index: MessageIndex { switch self { - case let .Msg(message): - return MessageIndex(message) + case let .Message(message): + return MessageIndex(id: message.id, timestamp: message.timestamp) case let .Hole(hole): return hole.maxIndex } } } +enum RenderedMessageHistoryEntry { + case RenderedMessage(Message) + case Hole(MessageHistoryHole) + + var index: MessageIndex { + switch self { + case let .RenderedMessage(message): + return MessageIndex(id: message.id, timestamp: message.timestamp) + case let .Hole(hole): + return hole.maxIndex + } + } +} + final class MessageHistoryTable { let valueBox: ValueBox let tableId: Int32 @@ -48,7 +68,7 @@ final class MessageHistoryTable { return key.successor } - private func messagesByPeerId(messages: [StoreMessage]) -> [PeerId: [StoreMessage]] { + private func messagesGroupedByPeerId(messages: [StoreMessage]) -> [PeerId: [StoreMessage]] { var dict: [PeerId: [StoreMessage]] = [:] for message in messages { @@ -63,74 +83,154 @@ final class MessageHistoryTable { return dict } - func addMessages(messages: [StoreMessage]) { + private func messageIdsByPeerId(ids: [MessageId]) -> [PeerId: [MessageId]] { + var dict: [PeerId: [MessageId]] = [:] + + for id in ids { + let peerId = id.peerId + if dict[peerId] == nil { + dict[peerId] = [id] + } else { + dict[peerId]!.append(id) + } + } + + return dict + } + + private func processIndexOperations(peerId: PeerId, operations: [MessageHistoryIndexOperation], inout processedOperationsByPeerId: [PeerId: [MessageHistoryOperation]]) { 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() - let messagesByPeerId = self.messagesByPeerId(messages) - for (_, peerMessages) in messagesByPeerId { - for message in peerMessages { - if self.messageHistoryIndexTable.messageExists(message.id) { - continue + var outputOperations: [MessageHistoryOperation] = [] + var accumulatedRemoveIndices: [MessageIndex] = [] + for operation in operations { + switch operation { + case let .InsertHole(hole): + if accumulatedRemoveIndices.count != 0 { + outputOperations.append(.Remove(accumulatedRemoveIndices)) + accumulatedRemoveIndices.removeAll() } - - self.messageHistoryIndexTable.addMessage(MessageIndex(message)) - self.justInsert(message, sharedKey: sharedKey, sharedBuffer: sharedBuffer, sharedEncoder: sharedEncoder) + self.justInsertHole(hole) + outputOperations.append(.InsertHole(hole)) + case let .InsertMessage(storeMessage): + if accumulatedRemoveIndices.count != 0 { + outputOperations.append(.Remove(accumulatedRemoveIndices)) + accumulatedRemoveIndices.removeAll() + } + let message = self.justInsertMessage(storeMessage, sharedKey: sharedKey, sharedBuffer: sharedBuffer, sharedEncoder: sharedEncoder) + outputOperations.append(.InsertMessage(message)) + case let .Remove(index): + self.justRemove(index) + accumulatedRemoveIndices.append(index) } } + if accumulatedRemoveIndices.count != 0 { + outputOperations.append(.Remove(accumulatedRemoveIndices)) + } + + if processedOperationsByPeerId[peerId] == nil { + processedOperationsByPeerId[peerId] = outputOperations + } else { + processedOperationsByPeerId[peerId]!.appendContentsOf(outputOperations) + } } - func removeMessages(messageIds: [MessageId]) { - for messageId in messageIds { - if let entry = self.messageHistoryIndexTable.get(messageId) { - if case let .Message(index) = entry { - if let message = self.get(index) { - let embeddedMediaData = message.embeddedMediaData - if embeddedMediaData.length > 4 { - var embeddedMediaCount: Int32 = 0 - embeddedMediaData.read(&embeddedMediaCount, offset: 0, length: 4) - for _ in 0 ..< embeddedMediaCount { - var mediaLength: Int32 = 0 - embeddedMediaData.read(&mediaLength, offset: 0, length: 4) - if let media = Decoder(buffer: MemoryBuffer(memory: embeddedMediaData.memory + embeddedMediaData.offset, capacity: Int(mediaLength), length: Int(mediaLength), freeWhenDone: false)).decodeRootObject() as? Media { - self.messageMediaTable.removeEmbeddedMedia(media) - } - embeddedMediaData.skip(Int(mediaLength)) - } - } - - for mediaId in message.referencedMedia { - self.messageMediaTable.removeReference(mediaId) - } - } - - self.messageHistoryIndexTable.removeMessage(messageId) - self.valueBox.remove(self.tableId, key: self.key(index)) - } - } + func addMessages(messages: [StoreMessage], location: AddMessagesLocation, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]]) { + let messagesByPeerId = self.messagesGroupedByPeerId(messages) + for (peerId, peerMessages) in messagesByPeerId { + var operations: [MessageHistoryIndexOperation] = [] + self.messageHistoryIndexTable.addMessages(peerMessages, location: location, operations: &operations) + self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId) } } - private func justInsert(message: StoreMessage, sharedKey: ValueBoxKey, sharedBuffer: WriteBuffer, sharedEncoder: Encoder) { + func addHoles(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]]) { + 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) + } + } + + func removeMessages(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]]) { + for (peerId, messageIds) in self.messageIdsByPeerId(messageIds) { + var operations: [MessageHistoryIndexOperation] = [] + for id in messageIds { + self.messageHistoryIndexTable.removeMessage(id, operations: &operations) + } + self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId) + } + } + + func fillHole(id: MessageId, fillType: HoleFillType, messages: [StoreMessage], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]]) { + var operations: [MessageHistoryIndexOperation] = [] + self.messageHistoryIndexTable.fillHole(id, fillType: fillType, messages: messages, operations: &operations) + self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId) + } + + func topMessage(peerId: PeerId) -> IntermediateMessage? { + var currentKey = self.lowerBound(peerId) + while true { + var entry: IntermediateMessageHistoryEntry? + self.valueBox.range(self.tableId, start: self.upperBound(peerId), end: currentKey, values: { key, value in + entry = self.readIntermediateEntry(key, value: value) + return true + }, limit: 1) + + if let entry = entry { + switch entry { + case .Hole: + currentKey = self.key(entry.index).predecessor + case let .Message(message): + return message + } + } else { + break + } + } + return nil + } + + private func justInsertMessage(message: StoreMessage, sharedKey: ValueBoxKey, sharedBuffer: WriteBuffer, sharedEncoder: Encoder) -> IntermediateMessage { sharedBuffer.reset() + + var type: Int8 = 0 + sharedBuffer.write(&type, offset: 0, length: 1) + + if let authorId = message.authorId { + var varAuthorId: Int64 = authorId.toInt64() + var hasAuthor: Int8 = 1 + sharedBuffer.write(&hasAuthor, offset: 0, length: 1) + sharedBuffer.write(&varAuthorId, offset: 0, length: 8) + } else { + var hasAuthor: Int8 = 0 + sharedBuffer.write(&hasAuthor, offset: 0, length: 1) + } let data = message.text.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)! var length: Int32 = Int32(data.length) sharedBuffer.write(&length, offset: 0, length: 4) sharedBuffer.write(data.bytes, offset: 0, length: Int(length)) + let attributesBuffer = WriteBuffer() + var attributeCount: Int32 = Int32(message.attributes.count) - sharedBuffer.write(&attributeCount, offset: 0, length: 4) + attributesBuffer.write(&attributeCount, offset: 0, length: 4) for attribute in message.attributes { sharedEncoder.reset() sharedEncoder.encodeRootObject(attribute) let attributeBuffer = sharedEncoder.memoryBuffer() var attributeBufferLength = Int32(attributeBuffer.length) - sharedBuffer.write(&attributeBufferLength, offset: 0, length: 4) - sharedBuffer.write(attributeBuffer.memory, offset: 0, length: attributeBuffer.length) + attributesBuffer.write(&attributeBufferLength, offset: 0, length: 4) + attributesBuffer.write(attributeBuffer.memory, offset: 0, length: attributeBuffer.length) } + sharedBuffer.write(attributesBuffer.memory, offset: 0, length: attributesBuffer.length) + var embeddedMedia: [Media] = [] var referencedMedia: [MediaId] = [] for media in message.media { @@ -147,17 +247,20 @@ final class MessageHistoryTable { } } + let embeddedMediaBuffer = WriteBuffer() var embeddedMediaCount: Int32 = Int32(embeddedMedia.count) - sharedBuffer.write(&embeddedMediaCount, offset: 0, length: 4) + embeddedMediaBuffer.write(&embeddedMediaCount, offset: 0, length: 4) for media in embeddedMedia { sharedEncoder.reset() sharedEncoder.encodeRootObject(media) let mediaBuffer = sharedEncoder.memoryBuffer() var mediaBufferLength = Int32(mediaBuffer.length) - sharedBuffer.write(&mediaBufferLength, offset: 0, length: 4) - sharedBuffer.write(mediaBuffer.memory, offset: 0, length: mediaBuffer.length) + embeddedMediaBuffer.write(&mediaBufferLength, offset: 0, length: 4) + embeddedMediaBuffer.write(mediaBuffer.memory, offset: 0, length: mediaBuffer.length) } + sharedBuffer.write(embeddedMediaBuffer.memory, offset: 0, length: embeddedMediaBuffer.length) + var referencedMediaCount: Int32 = Int32(referencedMedia.count) sharedBuffer.write(&referencedMediaCount, offset: 0, length: 4) for mediaId in referencedMedia { @@ -168,10 +271,51 @@ final class MessageHistoryTable { } self.valueBox.set(self.tableId, key: self.key(MessageIndex(message), key: sharedKey), value: sharedBuffer) + + return IntermediateMessage(id: message.id, timestamp: message.timestamp, authorId: message.authorId, text: message.text, attributesData: attributesBuffer.makeReadBufferAndReset(), embeddedMediaData: embeddedMediaBuffer.makeReadBufferAndReset(), referencedMedia: referencedMedia) + } + + private func justInsertHole(hole: MessageHistoryHole, sharedBuffer: WriteBuffer = WriteBuffer()) { + sharedBuffer.reset() + var type: Int8 = 1 + sharedBuffer.write(&type, offset: 0, length: 1) + var minId: Int32 = hole.min + sharedBuffer.write(&minId, offset: 0, length: 4) + self.valueBox.set(self.tableId, key: self.key(hole.maxIndex), value: sharedBuffer.readBufferNoCopy()) + } + + private func justRemove(index: MessageIndex) { + let key = self.key(index) + if let value = self.valueBox.get(self.tableId, key: key) { + switch self.readIntermediateEntry(key, value: value) { + case let .Message(message): + let embeddedMediaData = message.embeddedMediaData + if embeddedMediaData.length > 4 { + var embeddedMediaCount: Int32 = 0 + embeddedMediaData.read(&embeddedMediaCount, offset: 0, length: 4) + for _ in 0 ..< embeddedMediaCount { + var mediaLength: Int32 = 0 + embeddedMediaData.read(&mediaLength, offset: 0, length: 4) + if let media = Decoder(buffer: MemoryBuffer(memory: embeddedMediaData.memory + embeddedMediaData.offset, capacity: Int(mediaLength), length: Int(mediaLength), freeWhenDone: false)).decodeRootObject() as? Media { + self.messageMediaTable.removeEmbeddedMedia(media) + } + embeddedMediaData.skip(Int(mediaLength)) + } + } + + for mediaId in message.referencedMedia { + self.messageMediaTable.removeReference(mediaId) + } + case .Hole: + break + } + + self.valueBox.remove(self.tableId, key: key) + } } func unembedMedia(index: MessageIndex, id: MediaId) -> Media? { - if let message = self.get(index) where message.embeddedMediaData.length > 4 { + if let message = self.getMessage(index) where message.embeddedMediaData.length > 4 { var embeddedMediaCount: Int32 = 0 message.embeddedMediaData.read(&embeddedMediaCount, offset: 0, length: 4) @@ -202,7 +346,7 @@ final class MessageHistoryTable { if let extractedMedia = extractedMedia { var updatedReferencedMedia = message.referencedMedia updatedReferencedMedia.append(extractedMedia.id!) - self.storeIntermediateMessage(IntermediateMessage(id: message.id, timestamp: message.timestamp, text: message.text, attributesData: message.attributesData, embeddedMediaData: updatedEmbeddedMediaBuffer.readBufferNoCopy(), referencedMedia: updatedReferencedMedia), sharedKey: self.key(index)) + self.storeIntermediateMessage(IntermediateMessage(id: message.id, timestamp: message.timestamp, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: updatedEmbeddedMediaBuffer.readBufferNoCopy(), referencedMedia: updatedReferencedMedia), sharedKey: self.key(index)) return extractedMedia } @@ -213,6 +357,19 @@ final class MessageHistoryTable { func storeIntermediateMessage(message: IntermediateMessage, sharedKey: ValueBoxKey, sharedBuffer: WriteBuffer = WriteBuffer()) { sharedBuffer.reset() + var type: Int8 = 0 + sharedBuffer.write(&type, offset: 0, length: 1) + + if let authorId = message.authorId { + var varAuthorId: Int64 = authorId.toInt64() + var hasAuthor: Int8 = 1 + sharedBuffer.write(&hasAuthor, offset: 0, length: 1) + sharedBuffer.write(&varAuthorId, offset: 0, length: 8) + } else { + var hasAuthor: Int8 = 0 + sharedBuffer.write(&hasAuthor, offset: 0, length: 1) + } + let data = message.text.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)! var length: Int32 = Int32(data.length) sharedBuffer.write(&length, offset: 0, length: 4) @@ -233,63 +390,83 @@ final class MessageHistoryTable { self.valueBox.set(self.tableId, key: self.key(MessageIndex(id: message.id, timestamp: message.timestamp), key: sharedKey), value: sharedBuffer) } - private func readIntermediateMessage(key: ValueBoxKey, value: ReadBuffer) -> IntermediateMessage { + private func readIntermediateEntry(key: ValueBoxKey, value: ReadBuffer) -> IntermediateMessageHistoryEntry { let index = MessageIndex(id: MessageId(peerId: PeerId(key.getInt64(0)), namespace: key.getInt32(8 + 4), id: key.getInt32(8 + 4 + 4)), timestamp: key.getInt32(8)) - var textLength: Int32 = 0 - value.read(&textLength, offset: 0, length: 4) - let text = String(data: NSData(bytes: value.memory + value.offset, length: Int(textLength)), encoding: NSUTF8StringEncoding) ?? "" - value.skip(Int(textLength)) - - let attributesOffset = value.offset - var attributeCount: Int32 = 0 - value.read(&attributeCount, offset: 0, length: 4) - for _ in 0 ..< attributeCount { - var attributeLength: Int32 = 0 - value.read(&attributeLength, offset: 0, length: 4) - value.skip(Int(attributeLength)) + var type: Int8 = 0 + value.read(&type, offset: 0, length: 1) + if type == 0 { + var hasAuthor: Int8 = 0 + value.read(&hasAuthor, offset: 0, length: 1) + var authorId: PeerId? + if hasAuthor == 1 { + var varAuthorId: Int64 = 0 + value.read(&varAuthorId, offset: 0, length: 8) + authorId = PeerId(varAuthorId) + } + + var textLength: Int32 = 0 + value.read(&textLength, offset: 0, length: 4) + let text = String(data: NSData(bytes: value.memory + value.offset, length: Int(textLength)), encoding: NSUTF8StringEncoding) ?? "" + value.skip(Int(textLength)) + + let attributesOffset = value.offset + var attributeCount: Int32 = 0 + value.read(&attributeCount, offset: 0, length: 4) + for _ in 0 ..< attributeCount { + var attributeLength: Int32 = 0 + value.read(&attributeLength, offset: 0, length: 4) + value.skip(Int(attributeLength)) + } + let attributesLength = value.offset - attributesOffset + let attributesBytes = malloc(attributesLength) + memcpy(attributesBytes, value.memory + attributesOffset, attributesLength) + let attributesData = ReadBuffer(memory: attributesBytes, length: attributesLength, freeWhenDone: true) + + let embeddedMediaOffset = value.offset + var embeddedMediaCount: Int32 = 0 + value.read(&embeddedMediaCount, offset: 0, length: 4) + for _ in 0 ..< embeddedMediaCount { + var mediaLength: Int32 = 0 + value.read(&mediaLength, offset: 0, length: 4) + value.skip(Int(mediaLength)) + } + let embeddedMediaLength = value.offset - embeddedMediaOffset + let embeddedMediaBytes = malloc(embeddedMediaLength) + memcpy(embeddedMediaBytes, value.memory + embeddedMediaOffset, embeddedMediaLength) + let embeddedMediaData = ReadBuffer(memory: embeddedMediaBytes, length: embeddedMediaLength, freeWhenDone: true) + + var referencedMediaIds: [MediaId] = [] + var referencedMediaIdsCount: Int32 = 0 + value.read(&referencedMediaIdsCount, offset: 0, length: 4) + for _ in 0 ..< referencedMediaIdsCount { + var idNamespace: Int32 = 0 + var idId: Int64 = 0 + value.read(&idNamespace, offset: 0, length: 4) + value.read(&idId, offset: 0, length: 8) + referencedMediaIds.append(MediaId(namespace: idNamespace, id: idId)) + } + + return .Message(IntermediateMessage(id: index.id, timestamp: index.timestamp, authorId: authorId, text: text, attributesData: attributesData, embeddedMediaData: embeddedMediaData, referencedMedia: referencedMediaIds)) + } else { + var minId: Int32 = 0 + value.read(&minId, offset: 0, length: 4) + + return .Hole(MessageHistoryHole(maxIndex: index, min: minId)) } - let attributesLength = value.offset - attributesOffset - let attributesBytes = malloc(attributesLength) - memcpy(attributesBytes, value.memory + attributesOffset, attributesLength) - let attributesData = ReadBuffer(memory: attributesBytes, length: attributesLength, freeWhenDone: true) - - let embeddedMediaOffset = value.offset - var embeddedMediaCount: Int32 = 0 - value.read(&embeddedMediaCount, offset: 0, length: 4) - for _ in 0 ..< embeddedMediaCount { - var mediaLength: Int32 = 0 - value.read(&mediaLength, offset: 0, length: 4) - value.skip(Int(mediaLength)) - } - let embeddedMediaLength = value.offset - embeddedMediaOffset - let embeddedMediaBytes = malloc(embeddedMediaLength) - memcpy(embeddedMediaBytes, value.memory + embeddedMediaOffset, embeddedMediaLength) - let embeddedMediaData = ReadBuffer(memory: embeddedMediaBytes, length: embeddedMediaLength, freeWhenDone: true) - - var referencedMediaIds: [MediaId] = [] - var referencedMediaIdsCount: Int32 = 0 - value.read(&referencedMediaIdsCount, offset: 0, length: 4) - for _ in 0 ..< referencedMediaIdsCount { - var idNamespace: Int32 = 0 - var idId: Int64 = 0 - value.read(&idNamespace, offset: 0, length: 4) - value.read(&idId, offset: 0, length: 8) - referencedMediaIds.append(MediaId(namespace: idNamespace, id: idId)) - } - - return IntermediateMessage(id: index.id, timestamp: index.timestamp, text: text, attributesData: attributesData, embeddedMediaData: embeddedMediaData, referencedMedia: referencedMediaIds) } - func get(index: MessageIndex) -> IntermediateMessage? { + func getMessage(index: MessageIndex) -> IntermediateMessage? { let key = self.key(index) if let value = self.valueBox.get(self.tableId, key: key) { - return self.readIntermediateMessage(key, value: value) + if case let .Message(message) = self.readIntermediateEntry(key, value: value) { + return message + } } return nil } - func renderMessage(message: IntermediateMessage) -> Message { + func renderMessage(message: IntermediateMessage, peerTable: PeerTable) -> Message { var parsedAttributes: [Coding] = [] var parsedMedia: [Media] = [] @@ -327,33 +504,95 @@ final class MessageHistoryTable { } } - return Message(id: message.id, timestamp: message.timestamp, text: message.text, attributes: parsedAttributes, media: parsedMedia) + var author: Peer? + if let authorId = message.authorId { + author = peerTable.get(authorId) + } + + var peers = SimpleDictionary() + if let chatPeer = peerTable.get(message.id.peerId) { + peers[chatPeer.id] = chatPeer + } + + for media in parsedMedia { + for peerId in media.peerIds { + if let peer = peerTable.get(peerId) { + peers[peer.id] = peer + } + } + } + + return Message(id: message.id, timestamp: message.timestamp, author: author, text: message.text, attributes: parsedAttributes, media: parsedMedia, peers: peers) } - func messagesAround(index: MessageIndex, count: Int) -> [IntermediateMessage] { - var lowerMessages: [IntermediateMessage] = [] - var upperMessages: [IntermediateMessage] = [] + func entriesAround(index: MessageIndex, count: Int) -> [IntermediateMessageHistoryEntry] { + var lowerEntries: [IntermediateMessageHistoryEntry] = [] + var upperEntries: [IntermediateMessageHistoryEntry] = [] self.valueBox.range(self.tableId, start: self.key(index), end: self.lowerBound(index.id.peerId), values: { key, value in - lowerMessages.append(self.readIntermediateMessage(key, value: value)) + lowerEntries.append(self.readIntermediateEntry(key, value: value)) return true - }, limit: count) + }, limit: count / 2) self.valueBox.range(self.tableId, start: self.key(index).predecessor, end: self.upperBound(index.id.peerId), values: { key, value in - upperMessages.append(self.readIntermediateMessage(key, value: value)) + upperEntries.append(self.readIntermediateEntry(key, value: value)) return true - }, limit: count) + }, limit: count - lowerEntries.count) - var messages: [IntermediateMessage] = [] - for message in lowerMessages.reverse() { - messages.append(message) + if lowerEntries.count != 0 && lowerEntries.count + upperEntries.count < count { + self.valueBox.range(self.tableId, start: self.key(lowerEntries.last!.index), end: self.lowerBound(index.id.peerId), values: { key, value in + lowerEntries.append(self.readIntermediateEntry(key, value: value)) + return true + }, limit: count - (lowerEntries.count + upperEntries.count)) } - messages.appendContentsOf(upperMessages) - return messages + var entries: [IntermediateMessageHistoryEntry] = [] + for entry in lowerEntries.reverse() { + entries.append(entry) + } + entries.appendContentsOf(upperEntries) + + return entries } - func debugList(peerId: PeerId) -> [Message] { - return self.messagesAround(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: 0), count: 1000).map({self.renderMessage($0)}) + func earlierEntries(peerId: PeerId, index: MessageIndex?, count: Int) -> [IntermediateMessageHistoryEntry] { + var entries: [IntermediateMessageHistoryEntry] = [] + let key: ValueBoxKey + if let index = index { + key = self.key(index) + } else { + key = self.upperBound(peerId) + } + self.valueBox.range(self.tableId, start: key, end: self.lowerBound(peerId), values: { key, value in + entries.append(self.readIntermediateEntry(key, value: value)) + return true + }, limit: count) + return entries + } + + func laterEntries(peerId: PeerId, index: MessageIndex?, count: Int) -> [IntermediateMessageHistoryEntry] { + var entries: [IntermediateMessageHistoryEntry] = [] + let key: ValueBoxKey + if let index = index { + key = self.key(index) + } else { + key = self.lowerBound(peerId) + } + self.valueBox.range(self.tableId, start: key, end: self.upperBound(peerId), values: { key, value in + entries.append(self.readIntermediateEntry(key, value: value)) + return true + }, limit: count) + return entries + } + + func debugList(peerId: PeerId, peerTable: PeerTable) -> [RenderedMessageHistoryEntry] { + return self.entriesAround(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: 0), count: 1000).map({ entry -> RenderedMessageHistoryEntry in + switch entry { + case let .Hole(hole): + return .Hole(hole) + case let .Message(message): + return .RenderedMessage(self.renderMessage(message, peerTable: peerTable)) + } + }) } } diff --git a/Postbox/MessageHistoryView.swift b/Postbox/MessageHistoryView.swift index 0efad9cde0..e3dabce69f 100644 --- a/Postbox/MessageHistoryView.swift +++ b/Postbox/MessageHistoryView.swift @@ -1,54 +1,109 @@ import Foundation -public final class MutableMessageHistoryView: CustomStringConvertible { - public struct RemoveContext { - var invalidEarlier: Bool = false - var invalidLater: Bool = false - var invalidEarlierHole: Bool = false - var invalidLaterHole: Bool = false - var removedMessages: Bool = false - var removedHole: Bool = false - - func empty() -> Bool { - return !self.removedMessages && !invalidEarlier && !invalidLater +enum MutableMessageHistoryEntry { + case IntermediateMessageEntry(IntermediateMessage) + case MessageEntry(Message) + case HoleEntry(MessageHistoryHole) + + var index: MessageIndex { + switch self { + case let .IntermediateMessageEntry(message): + return MessageIndex(id: message.id, timestamp: message.timestamp) + case let .MessageEntry(message): + return MessageIndex(id: message.id, timestamp: message.timestamp) + case let .HoleEntry(hole): + return hole.maxIndex } } +} + +public enum MessageHistoryEntry: Comparable { + case MessageEntry(Message) + case HoleEntry(MessageHistoryHole) - let count: Int - var earlierMessage: RenderedMessage? - var laterMessage: RenderedMessage? - var messages: [RenderedMessage] + public var index: MessageIndex { + switch self { + case let .MessageEntry(message): + return MessageIndex(id: message.id, timestamp: message.timestamp) + case let .HoleEntry(hole): + return hole.maxIndex + } + } +} + +public func ==(lhs: MessageHistoryEntry, rhs: MessageHistoryEntry) -> Bool { + return lhs.index == rhs.index +} + +public func <(lhs: MessageHistoryEntry, rhs: MessageHistoryEntry) -> Bool { + return lhs.index < rhs.index +} + +final class MutableMessageHistoryViewReplayContext { + var invalidEarlier: Bool = false + var invalidLater: Bool = false + var removedEntries: Bool = false - public init(count: Int, earlierMessage: RenderedMessage?, messages: [RenderedMessage], laterMessage: RenderedMessage?) { + func empty() -> Bool { + return !self.removedEntries && !invalidEarlier && !invalidLater + } +} + +final class MutableMessageHistoryView { + private let count: Int + private var earlier: MutableMessageHistoryEntry? + private var later: MutableMessageHistoryEntry? + private var entries: [MutableMessageHistoryEntry] + + init(earlier: MutableMessageHistoryEntry?, entries: [MutableMessageHistoryEntry], later: MutableMessageHistoryEntry?, count: Int) { + self.earlier = earlier + self.entries = entries + self.later = later self.count = count - self.earlierMessage = earlierMessage - self.laterMessage = laterMessage - self.messages = messages } - public func add(message: RenderedMessage) -> Bool { - if self.messages.count == 0 { - self.messages.append(message) + func replay(operations: [MessageHistoryOperation], context: MutableMessageHistoryViewReplayContext) -> Bool { + var hasChanges = false + for operation in operations { + switch operation { + case let .InsertHole(hole): + if self.add(.HoleEntry(hole)) { + hasChanges = true + } + case let .InsertMessage(intermediateMessage): + if self.add(.IntermediateMessageEntry(intermediateMessage)) { + hasChanges = true + } + case let .Remove(indices): + if self.remove(Set(indices), context: context) { + hasChanges = true + } + } + } + return hasChanges + } + + private func add(entry: MutableMessageHistoryEntry) -> Bool { + if self.entries.count == 0 { + self.entries.append(entry) return true } else { - let first = MessageIndex(self.messages[self.messages.count - 1].message) - let last = MessageIndex(self.messages[0].message) + let first = self.entries[self.entries.count - 1].index + let last = self.entries[0].index var next: MessageIndex? - if let message = laterMessage { - let messageIndex = MessageIndex(message.message) - next = messageIndex + if let later = self.later { + next = later.index } - let index = MessageIndex(message.message) + let index = entry.index if index < last { - let earlierMessage = self.earlierMessage - if earlierMessage == nil || MessageIndex(earlierMessage!.message) < index { - if self.messages.count < self.count { - self.messages.insert(message, atIndex: 0) + if self.earlier == nil || self.earlier!.index < index { + if self.entries.count < self.count { + self.entries.insert(entry, atIndex: 0) } else { - self.earlierMessage = message + self.earlier = entry } return true } else { @@ -56,39 +111,36 @@ public final class MutableMessageHistoryView: CustomStringConvertible { } } else if index > first { if next != nil && index > next! { - let laterMessage = self.laterMessage - if laterMessage == nil || MessageIndex(laterMessage!.message) > index { - if self.messages.count < self.count { - self.messages.append(message) + if self.later == nil || self.later!.index > index { + if self.entries.count < self.count { + self.entries.append(entry) } else { - self.laterMessage = message + self.later = entry } return true } else { return false } } else { - self.messages.append(message) - if self.messages.count > self.count { - let earliest = self.messages[0] - self.earlierMessage = earliest - self.messages.removeAtIndex(0) + 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 { - var i = self.messages.count + var i = self.entries.count while i >= 1 { - if MessageIndex(self.messages[i - 1].message) < index { + if self.entries[i - 1].index < index { break } i-- } - self.messages.insert(message, atIndex: i) - if self.messages.count > self.count { - let earliest = self.messages[0] - self.earlierMessage = earliest - self.messages.removeAtIndex(0) + self.entries.insert(entry, atIndex: i) + if self.entries.count > self.count { + self.earlier = self.entries[0] + self.entries.removeAtIndex(0) } return true } else { @@ -97,72 +149,75 @@ public final class MutableMessageHistoryView: CustomStringConvertible { } } - public func remove(ids: Set, context: RemoveContext? = nil) -> RemoveContext { - var updatedContext = RemoveContext() - if let context = context { - updatedContext = context + private func remove(indices: Set, context: MutableMessageHistoryViewReplayContext) -> Bool { + var hasChanges = false + if let earlier = self.earlier where indices.contains(earlier.index) { + context.invalidEarlier = true + hasChanges = true } - if let earlierMessage = self.earlierMessage where ids.contains(earlierMessage.message.id) { - updatedContext.invalidEarlier = true + if let later = self.later where indices.contains(later.index) { + context.invalidLater = true + hasChanges = true } - if let laterMessage = self.laterMessage where ids.contains(laterMessage.message.id) { - updatedContext.invalidLater = true - } - - if self.messages.count != 0 { - var i = self.messages.count - 1 + if self.entries.count != 0 { + var i = self.entries.count - 1 while i >= 0 { - if ids.contains(self.messages[i].message.id) { - self.messages.removeAtIndex(i) - updatedContext.removedMessages = true + if indices.contains(self.entries[i].index) { + self.entries.removeAtIndex(i) + context.removedEntries = true + hasChanges = true } i-- } } - return updatedContext + return hasChanges } - public func complete(context: RemoveContext, fetchEarlier: (MessageIndex?, Int) -> [RenderedMessage], fetchLater: (MessageIndex?, Int) -> [RenderedMessage]) { - if context.removedMessages { - var addedMessages: [RenderedMessage] = [] + func updatePeers(peers: [PeerId: Peer]) -> Bool { + return false + } + + func complete(context: MutableMessageHistoryViewReplayContext, fetchEarlier: (MessageIndex?, Int) -> [MutableMessageHistoryEntry], fetchLater: (MessageIndex?, Int) -> [MutableMessageHistoryEntry]) { + if context.removedEntries { + var addedEntries: [MutableMessageHistoryEntry] = [] var latestAnchor: MessageIndex? - if let lastMessage = self.messages.last { - latestAnchor = MessageIndex(lastMessage.message) + if let last = self.entries.last { + latestAnchor = last.index } if latestAnchor == nil { - if let laterMessage = self.laterMessage { - latestAnchor = MessageIndex(laterMessage.message) + if let later = self.later { + latestAnchor = later.index } } - if let laterMessage = self.laterMessage { - addedMessages += fetchLater(MessageIndex(laterMessage.message).predecessor(), self.count) + if let later = self.later { + addedEntries += fetchLater(later.index.predecessor(), self.count) } - if let earlierMessage = self.earlierMessage { - addedMessages += fetchEarlier(MessageIndex(earlierMessage.message).successor(), self.count) + if let earlier = self.earlier { + addedEntries += fetchEarlier(earlier.index.successor(), self.count) } - addedMessages += self.messages - addedMessages.sortInPlace({ MessageIndex($0.message) < MessageIndex($1.message) }) - var i = addedMessages.count - 1 + addedEntries += self.entries + addedEntries.sortInPlace({ $0.index < $1.index }) + var i = addedEntries.count - 1 while i >= 1 { - if addedMessages[i].message.id == addedMessages[i - 1].message.id { - addedMessages.removeAtIndex(i) + if addedEntries[i].index.id == addedEntries[i - 1].index.id { + addedEntries.removeAtIndex(i) } i-- } - self.messages = [] + self.entries = [] - var anchorIndex = addedMessages.count - 1 + var anchorIndex = addedEntries.count - 1 if let latestAnchor = latestAnchor { - var i = addedMessages.count - 1 + var i = addedEntries.count - 1 while i >= 0 { - if MessageIndex(addedMessages[i].message) <= latestAnchor { + if addedEntries[i].index <= latestAnchor { anchorIndex = i break } @@ -170,172 +225,92 @@ public final class MutableMessageHistoryView: CustomStringConvertible { } } - self.laterMessage = nil - if anchorIndex + 1 < addedMessages.count { - self.laterMessage = addedMessages[anchorIndex + 1] + self.later = nil + if anchorIndex + 1 < addedEntries.count { + self.later = addedEntries[anchorIndex + 1] } i = anchorIndex while i >= 0 && i > anchorIndex - self.count { - self.messages.insert(addedMessages[i], atIndex: 0) + self.entries.insert(addedEntries[i], atIndex: 0) i-- } - self.earlierMessage = nil + self.earlier = nil if anchorIndex - self.count >= 0 { - self.earlierMessage = addedMessages[anchorIndex - self.count] + self.earlier = addedEntries[anchorIndex - self.count] } - } - else { + } else { if context.invalidEarlier { var earlyId: MessageIndex? let i = 0 - if i < self.messages.count { - earlyId = MessageIndex(self.messages[i].message) + if i < self.entries.count { + earlyId = self.entries[i].index } - let earlierMessages = fetchEarlier(earlyId, 1) - self.earlierMessage = earlierMessages.first + let earlierEntries = fetchEarlier(earlyId, 1) + self.earlier = earlierEntries.first } if context.invalidLater { var laterId: MessageIndex? - let i = self.messages.count - 1 + let i = self.entries.count - 1 if i >= 0 { - laterId = MessageIndex(self.messages[i].message) + laterId = self.entries[i].index } - let laterMessages = fetchLater(laterId, 1) - self.laterMessage = laterMessages.first + let laterEntries = fetchLater(laterId, 1) + self.later = laterEntries.first } } } - - public func incompleteMessages() -> [Message] { - var result: [Message] = [] - - if let earlierMessage = self.earlierMessage where earlierMessage.incomplete { - result.append(earlierMessage.message) + func render(renderIntermediateMessage: IntermediateMessage -> Message) { + if let earlier = self.earlier, case let .IntermediateMessageEntry(intermediateMessage) = earlier { + self.earlier = .MessageEntry(renderIntermediateMessage(intermediateMessage)) } - - if let laterMessage = self.laterMessage where laterMessage.incomplete { - result.append(laterMessage.message) + if let later = self.later, case let .IntermediateMessageEntry(intermediateMessage) = later { + self.later = .MessageEntry(renderIntermediateMessage(intermediateMessage)) } - for message in self.messages { - if message.incomplete { - result.append(message.message) + for i in 0 ..< self.entries.count { + if case let .IntermediateMessageEntry(intermediateMessage) = self.entries[i] { + self.entries[i] = .MessageEntry(renderIntermediateMessage(intermediateMessage)) } } - - return result - } - - public func completeMessages(messages: [MessageId : RenderedMessage]) { - if let earlierMessage = self.earlierMessage { - if let renderedMessage = messages[earlierMessage.message.id] { - self.earlierMessage = renderedMessage - } - } - - if let laterMessage = self.laterMessage { - if let renderedMessage = messages[laterMessage.message.id] { - self.laterMessage = renderedMessage - } - } - - var i = 0 - while i < self.messages.count { - if let message = messages[self.messages[i].message.id] { - self.messages[i] = message - } - i++ - } } - public var description: String { - var string = "" - string += "...(" - if let value = self.earlierMessage { - string += "\(value.message.id.namespace): \(value.message.id.id)—\(value.message.timestamp)" - } - string += ") —— " - - string += "[" - var first = true - for message in self.messages { - if first { - first = false - } else { - string += ", " + func firstHole() -> MessageHistoryHole? { + for entry in self.entries { + if case let .HoleEntry(hole) = entry { + return hole } - string += "\(message.message.id.namespace): \(message.message.id.id)—\(message.message.timestamp)" } - string += "]" - string += " —— (" - if let value = self.laterMessage { - string += "\(value.message.id.namespace): \(value.message.id.id)—\(value.message.timestamp)" - } - string += ")..." - - return string + return nil } } -public final class MessageHistoryView: CustomStringConvertible { - public let hasEarlier: Bool - private let earlierId: MessageIndex? - public let hasLater: Bool - private let laterId: MessageIndex? - public let messages: [RenderedMessage] +public final class MessageHistoryView { + public let earlierId: MessageIndex? + public let laterId: MessageIndex? + public let entries: [MessageHistoryEntry] init(_ mutableView: MutableMessageHistoryView) { - self.hasEarlier = mutableView.earlierMessage != nil - self.hasLater = mutableView.laterMessage != nil - self.messages = mutableView.messages + var entries: [MessageHistoryEntry] = [] + for entry in mutableView.entries { + switch entry { + case let .HoleEntry(hole): + entries.append(.HoleEntry(hole)) + case let .MessageEntry(message): + entries.append(.MessageEntry(message)) + case .IntermediateMessageEntry: + assertionFailure("got IntermediateMessageEntry") + } + } + self.entries = entries - if let earlierMessage = mutableView.earlierMessage { - self.earlierId = MessageIndex(earlierMessage.message) - } else { - self.earlierId = nil - } - - if let laterMessage = mutableView.laterMessage { - self.laterId = MessageIndex(laterMessage.message) - } else { - self.laterId = nil - } - } - - public var description: String { - var string = "" - if self.hasEarlier { - string += "more(" - if let earlierId = self.earlierId { - string += "\(earlierId.id.namespace): \(earlierId.id.id)—\(earlierId.timestamp)" - } - string += ") " - } - string += "[" - var first = true - for message in self.messages { - if first { - first = false - } else { - string += ", " - } - string += "\(message.message.id.namespace): \(message.message.id.id)—\(message.message.timestamp)" - } - string += "]" - if self.hasLater { - string += " more(" - if let laterId = self.laterId { - string += "\(laterId.id.namespace): \(laterId.id.id)—\(laterId.timestamp)" - } - string += ")" - } - return string + self.earlierId = mutableView.earlier?.index + self.laterId = mutableView.later?.index } } diff --git a/Postbox/MetadataTable.swift b/Postbox/MetadataTable.swift new file mode 100644 index 0000000000..5dfb6f4e0f --- /dev/null +++ b/Postbox/MetadataTable.swift @@ -0,0 +1,53 @@ +import Foundation + +private enum MetadataKey: Int32 { + case UserVersion = 1 + case State = 2 +} + +final class MetadataTable { + let valueBox: ValueBox + let tableId: Int32 + + init(valueBox: ValueBox, tableId: Int32) { + self.valueBox = valueBox + self.tableId = tableId + } + + private func key(key: MetadataKey) -> ValueBoxKey { + let valueBoxKey = ValueBoxKey(length: 4) + valueBoxKey.setInt32(0, value: key.rawValue) + return valueBoxKey + } + + func userVersion() -> Int32? { + if let value = self.valueBox.get(self.tableId, key: self.key(.UserVersion)) { + var version: Int32 = 0 + value.read(&version, offset: 0, length: 4) + return version + } + return nil + } + + func setUserVersion(version: Int32) { + let buffer = WriteBuffer() + var varVersion: Int32 = version + buffer.write(&varVersion, offset: 0, length: 4) + self.valueBox.set(self.tableId, key: self.key(.UserVersion), value: buffer) + } + + func state() -> Coding? { + if let value = self.valueBox.get(self.tableId, key: self.key(.State)) { + if let state = Decoder(buffer: value).decodeRootObject() { + return state + } + } + return nil + } + + func setState(state: Coding) { + let encoder = Encoder() + encoder.encodeRootObject(state) + self.valueBox.set(self.tableId, key: self.key(.State), value: encoder.readBufferNoCopy()) + } +} \ No newline at end of file diff --git a/Postbox/Peer.swift b/Postbox/Peer.swift index 04bf16f2d9..f9d741f6f2 100644 --- a/Postbox/Peer.swift +++ b/Postbox/Peer.swift @@ -95,5 +95,5 @@ public func <(lhs: PeerId, rhs: PeerId) -> Bool { public protocol Peer: Coding { var id: PeerId { get } - func equalsTo(other: Peer) -> Bool + func isEqual(other: Peer) -> Bool } diff --git a/Postbox/PeerTable.swift b/Postbox/PeerTable.swift new file mode 100644 index 0000000000..6f3aee5056 --- /dev/null +++ b/Postbox/PeerTable.swift @@ -0,0 +1,40 @@ +import Foundation + +final class PeerTable { + let valueBox: ValueBox + let tableId: Int32 + + private let sharedEncoder = Encoder() + private let sharedKey = ValueBoxKey(length: 8) + private var cachedPeers: [PeerId: Peer] = [:] + + init(valueBox: ValueBox, tableId: Int32) { + self.valueBox = valueBox + self.tableId = tableId + } + + private func key(id: PeerId) -> ValueBoxKey { + self.sharedKey.setInt64(0, value: id.toInt64()) + return self.sharedKey + } + + func set(peer: Peer) { + self.sharedEncoder.reset() + self.sharedEncoder.encodeRootObject(peer) + + self.valueBox.set(self.tableId, key: self.key(peer.id), value: self.sharedEncoder.readBufferNoCopy()) + } + + func get(id: PeerId) -> Peer? { + if let peer = self.cachedPeers[id] { + return peer + } + if let value = self.valueBox.get(self.tableId, key: self.key(id)) { + if let peer = Decoder(buffer: value).decodeRootObject() as? Peer { + self.cachedPeers[id] = peer + return peer + } + } + return nil + } +} \ No newline at end of file diff --git a/Postbox/PeerView.swift b/Postbox/PeerView.swift deleted file mode 100644 index 74b96c743f..0000000000 --- a/Postbox/PeerView.swift +++ /dev/null @@ -1,454 +0,0 @@ -import Foundation - -public final class PeerViewEntry: Equatable, Comparable { - public let peerId: PeerId - public let peer: Peer? - public let messageIndex: MessageIndex - public let message: RenderedMessage? - - public init(peer: Peer, message: RenderedMessage) { - self.peerId = peer.id - self.peer = peer - self.message = message - self.messageIndex = MessageIndex(message.message) - } - - public init(peerId: PeerId, message: RenderedMessage) { - self.peerId = peerId - self.peer = nil - self.message = message - self.messageIndex = MessageIndex(message.message) - } - - private init(peer: Peer?, peerId: PeerId, message: RenderedMessage) { - self.peer = peer - self.peerId = peerId - self.message = message - self.messageIndex = MessageIndex(message.message) - } - - private init(peer: Peer?, peerId: PeerId, message: RenderedMessage?, messageIndex: MessageIndex) { - self.peer = peer - self.peerId = peerId - self.message = message - self.messageIndex = messageIndex - } - - init(peer: Peer?, peerId: PeerId, messageIndex: MessageIndex) { - self.peer = peer - self.peerId = peerId - self.messageIndex = messageIndex - self.message = nil - } -} - -public func ==(lhs: PeerViewEntry, rhs: PeerViewEntry) -> Bool { - return PeerViewEntryIndex(lhs) == PeerViewEntryIndex(rhs) -} - -public func <(lhs: PeerViewEntry, rhs: PeerViewEntry) -> Bool { - return PeerViewEntryIndex(lhs) < PeerViewEntryIndex(rhs) -} - -public struct PeerViewEntryIndex: Equatable, Comparable { - public let peerId: PeerId - public let messageIndex: MessageIndex - - public init(_ entry: PeerViewEntry) { - self.peerId = entry.peerId - self.messageIndex = entry.messageIndex - } - - public init(peerId: PeerId, messageIndex: MessageIndex) { - self.peerId = peerId - self.messageIndex = messageIndex - } - - public func earlier() -> PeerViewEntryIndex { - return PeerViewEntryIndex(peerId: self.peerId, messageIndex: MessageIndex(id: MessageId(peerId: self.messageIndex.id.peerId, namespace: self.messageIndex.id.namespace, id: self.messageIndex.id.id - 1), timestamp: self.messageIndex.timestamp)) - } - - public func later() -> PeerViewEntryIndex { - return PeerViewEntryIndex(peerId: self.peerId, messageIndex: MessageIndex(id: MessageId(peerId: self.messageIndex.id.peerId, namespace: self.messageIndex.id.namespace, id: self.messageIndex.id.id + 1), timestamp: self.messageIndex.timestamp)) - } -} - -public func ==(lhs: PeerViewEntryIndex, rhs: PeerViewEntryIndex) -> Bool { - return lhs.peerId == rhs.peerId && lhs.messageIndex == rhs.messageIndex -} - -public func <(lhs: PeerViewEntryIndex, rhs: PeerViewEntryIndex) -> Bool { - if lhs.messageIndex != rhs.messageIndex { - return lhs.messageIndex < rhs.messageIndex - } - - return lhs.peerId < rhs.peerId -} - -public final class MutablePeerView: CustomStringConvertible { - public struct RemoveContext { - var invalidEarlier = false - var invalidLater = false - var removedEntries = false - } - - let count: Int - var earlier: PeerViewEntry? - var later: PeerViewEntry? - var entries: [PeerViewEntry] - - public init(count: Int, earlier: PeerViewEntry?, entries: [PeerViewEntry], later: PeerViewEntry?) { - self.count = count - self.earlier = earlier - self.entries = entries - self.later = later - } - - public func removeEntry(context: RemoveContext?, peerId: PeerId) -> RemoveContext { - var invalidationContext = context ?? RemoveContext() - - if let earlier = self.earlier { - if peerId == earlier.peerId { - invalidationContext.invalidEarlier = true - } - } - - if let later = self.later { - if peerId == later.peerId { - invalidationContext.invalidLater = true - } - } - - var i = 0 - while i < self.entries.count { - if self.entries[i].peerId == peerId { - self.entries.removeAtIndex(i) - invalidationContext.removedEntries = true - break - } - i++ - } - - return invalidationContext - } - - public func addEntry(entry: PeerViewEntry) { - if self.entries.count == 0 { - self.entries.append(entry) - } else { - let first = PeerViewEntryIndex(self.entries[self.entries.count - 1]) - let last = PeerViewEntryIndex(self.entries[0]) - - let index = PeerViewEntryIndex(entry) - - var next: PeerViewEntryIndex? - if let later = self.later { - next = PeerViewEntryIndex(later) - } - - if index < last { - let earlierEntry = self.earlier - if earlierEntry == nil || PeerViewEntryIndex(earlierEntry!) < index { - if self.entries.count < self.count { - self.entries.insert(entry, atIndex: 0) - } else { - self.earlier = entry - } - } - } else if index > first { - if next != nil && index > next! { - let laterEntry = self.later - if laterEntry == nil || PeerViewEntryIndex(laterEntry!) > index { - if self.entries.count < self.count { - self.entries.append(entry) - } else { - self.later = entry - } - } - } else { - self.entries.append(entry) - if self.entries.count > self.count { - let earliest = self.entries[0] - self.earlier = earliest - self.entries.removeAtIndex(0) - } - } - } else if index != last && index != first { - var i = self.entries.count - while i >= 1 { - if PeerViewEntryIndex(self.entries[i - 1]) < index { - break - } - i-- - } - self.entries.insert(entry, atIndex: i) - if self.entries.count > self.count { - let earliest = self.entries[0] - self.earlier = earliest - self.entries.removeAtIndex(0) - } - } - } - } - - public func complete(context: RemoveContext, fetchEarlier: (PeerViewEntryIndex?, Int) -> [PeerViewEntry], fetchLater: (PeerViewEntryIndex?, Int) -> [PeerViewEntry]) { - if context.removedEntries && self.entries.count != self.count { - var addedEntries: [PeerViewEntry] = [] - - var latestAnchor: PeerViewEntryIndex? - - if self.entries.count != 0 { - latestAnchor = PeerViewEntryIndex(self.entries[self.entries.count - 1]) - } else if let later = self.later { - latestAnchor = PeerViewEntryIndex(later) - } - - if let later = self.later { - addedEntries += fetchLater(PeerViewEntryIndex(later).earlier(), self.count) - } - if let earlier = self.earlier { - addedEntries += fetchEarlier(PeerViewEntryIndex(earlier).later(), self.count) - } - - addedEntries += self.entries - addedEntries.sortInPlace({ PeerViewEntryIndex($0) < PeerViewEntryIndex($1) }) - - var i = addedEntries.count - 1 - while i >= 1 { - if PeerViewEntryIndex(addedEntries[i]) == PeerViewEntryIndex(addedEntries[i - 1]) { - addedEntries.removeAtIndex(i) - } - i-- - } - self.entries = [] - - var anchorIndex = addedEntries.count - 1 - if let latestAnchor = latestAnchor { - var i = addedEntries.count - 1 - while i >= 0 { - if PeerViewEntryIndex(addedEntries[i]) <= latestAnchor { - anchorIndex = i - break - } - i-- - } - } - - self.later = nil - if anchorIndex + 1 < addedEntries.count { - let i = anchorIndex + 1 - while i < addedEntries.count { - self.later = addedEntries[i] - break - } - } - - i = anchorIndex - while i >= 0 && i > anchorIndex - self.count { - self.entries.insert(addedEntries[i], atIndex: 0) - i-- - } - - self.earlier = nil - if anchorIndex - self.count >= 0 { - i = anchorIndex - self.count - while i >= 0 { - self.earlier = addedEntries[i] - break - } - } - } else { - var earlyIndex: PeerViewEntryIndex? - if self.entries.count != 0 { - earlyIndex = PeerViewEntryIndex(self.entries[0]) - } - - let earlierEntries = fetchEarlier(earlyIndex, 1) - if earlierEntries.count == 0 { - self.earlier = nil - } else { - self.earlier = earlierEntries[0] - } - - var lateIndex: PeerViewEntryIndex? - if self.entries.count != 0 { - lateIndex = PeerViewEntryIndex(self.entries[self.entries.count - 1]) - } - - let laterEntries = fetchLater(lateIndex, 1) - if laterEntries.count == 0 { - self.later = nil - } else { - self.later = laterEntries[0] - } - } - } - - public func updatePeers(peers: [PeerId : Peer]) -> Bool { - var updated = false - - if let earlier = self.earlier { - if let peer = peers[earlier.peerId] { - self.earlier = PeerViewEntry(peer: peer, peerId: earlier.peerId, message: earlier.message, messageIndex: earlier.messageIndex) - updated = true - } - } - - if let later = self.later { - if let peer = peers[later.peerId] { - self.later = PeerViewEntry(peer: peer, peerId: later.peerId, message: later.message, messageIndex: later.messageIndex) - updated = true - } - } - - var i = 0 - while i < self.entries.count { - if let peer = peers[self.entries[i].peerId] { - let entry = self.entries[i] - self.entries[i] = PeerViewEntry(peer: peer, peerId: entry.peerId, message: entry.message, messageIndex: entry.messageIndex) - updated = true - } - i++ - } - - return updated - } - - public func incompleteMessages() -> [Message] { - var result: [Message] = [] - - if let earlier = self.earlier { - if let message = earlier.message { - if message.incomplete { - result.append(message.message) - } - } - } - if let later = self.later { - if let message = later.message { - if message.incomplete { - result.append(message.message) - } - } - } - - for entry in self.entries { - if let message = entry.message { - if message.incomplete { - result.append(message.message) - } - } - } - - return result - } - - public func completeMessages(messages: [MessageId : RenderedMessage]) { - if let earlier = self.earlier { - if let earlierMessage = earlier.message { - if let message = messages[earlierMessage.message.id] { - self.earlier = PeerViewEntry(peer: earlier.peer, peerId: earlier.peerId, message: message) - } - } - } - if let later = self.later { - if let laterMessage = later.message { - if let message = messages[laterMessage.message.id] { - self.later = PeerViewEntry(peer: later.peer, peerId: later.peerId, message: message) - } - } - } - - var i = 0 - while i < self.entries.count { - if let entryMessage = self.entries[i].message { - if let message = messages[entryMessage.message.id] { - self.entries[i] = PeerViewEntry(peer: self.entries[i].peer, peerId: self.entries[i].peerId, message: message) - } - } - i++ - } - } - - public var description: String { - var string = "" - - if let earlier = self.earlier { - string += "more(" - string += "(p \(earlier.peerId.namespace):\(earlier.peerId.id), m \(earlier.message?.message.id.namespace ?? 0):\(earlier.message?.message.id.id ?? 0)—\(earlier.message?.message.timestamp ?? 0)" - string += ") " - } - - string += "[" - var first = true - for entry in self.entries { - if first { - first = false - } else { - string += ", " - } - string += "(p \(entry.peerId.namespace):\(entry.peerId.id), m \(entry.message?.message.id.namespace ?? 0):\(entry.message?.message.id.id ?? 0)—\(entry.message?.message.timestamp ?? 0))" - } - string += "]" - - if let later = self.later { - string += " more(" - string += "(p \(later.peerId.namespace):\(later.peerId), m \(later.message?.message.id.namespace ?? 0):\(later.message?.message.id.id ?? 0)—\(later.message?.message.timestamp ?? 0)" - string += ")" - } - - return string - } -} - -public final class PeerView: CustomStringConvertible { - let earlier: PeerViewEntryIndex? - let later: PeerViewEntryIndex? - public let entries: [PeerViewEntry] - - init(_ mutableView: MutablePeerView) { - if let earlier = mutableView.earlier { - self.earlier = PeerViewEntryIndex(earlier) - } else { - self.earlier = nil - } - - if let later = mutableView.later { - self.later = PeerViewEntryIndex(later) - } else { - self.later = nil - } - - self.entries = mutableView.entries - } - - public var description: String { - var string = "" - - if let earlier = self.earlier { - string += "more(" - string += "(p \(earlier.peerId.namespace):\(earlier.peerId.id), m \(earlier.messageIndex.id.namespace):\(earlier.messageIndex.id.id)—\(earlier.messageIndex.timestamp)" - string += ") " - } - - string += "[" - var first = true - for entry in self.entries { - if first { - first = false - } else { - string += ", " - } - string += "(p \(entry.peerId.namespace):\(entry.peerId.id), m \(entry.message?.message.id.namespace ?? 0):\(entry.message?.message.id.id ?? 0)—\(entry.message?.message.timestamp ?? 0))" - } - string += "]" - - if let later = self.later { - string += " more(" - string += "(p \(later.peerId.namespace):\(later.peerId), m \(later.messageIndex.id.namespace):\(later.messageIndex.id.id)—\(later.messageIndex.timestamp)" - string += ")" - } - - return string - } -} diff --git a/Postbox/Postbox.swift b/Postbox/Postbox.swift index 9ddd12ec63..f2dea6d270 100644 --- a/Postbox/Postbox.swift +++ b/Postbox/Postbox.swift @@ -2,75 +2,91 @@ import Foundation import SwiftSignalKit import sqlcipher -public protocol PostboxState: Coding { +public final class Modifier { + private weak var postbox: Postbox? -} - -public final class Modifier { - private weak var postbox: Postbox? - - private init(postbox: Postbox) { + private init(postbox: Postbox) { self.postbox = postbox } - public func addMessages(messages: [Message], medias: [Media]) { - self.postbox?.addMessages(messages, medias: medias) + public func addMessages(messages: [StoreMessage], location: AddMessagesLocation) { + self.postbox?.addMessages(messages, location: location) } - public func deleteMessagesWithIds(ids: [MessageId]) { - self.postbox?.deleteMessagesWithIds(ids) + public func initializeHole(peerId: PeerId, namespace: MessageId.Namespace) { + self.postbox?.addHole(MessageId(peerId: peerId, namespace: namespace, id: 1)) } - public func deleteMessagesWithAbsoluteIndexedIds(ids: [Int32]) { + public func fillHole(hole: MessageHistoryHole, fillType: HoleFillType, messages: [StoreMessage]) { + self.postbox?.fillHole(hole, fillType: fillType, messages: messages) + } + + public func deleteMessages(messageIds: [MessageId]) { + self.postbox?.deleteMessages(messageIds) + } + + public func deleteMessagesWithGlobalIds(ids: [Int32]) { if let postbox = self.postbox { - let messageIds = postbox.messageIdsForAbsoluteIndexedIds(ids) - postbox.deleteMessagesWithIds(messageIds) + let messageIds = postbox.messageIdsForGlobalIds(ids) + postbox.deleteMessages(messageIds) } } - public func getState() -> State? { + public func getState() -> Coding? { return self.postbox?.getState() } - public func setState(state: State) { + public func setState(state: Coding) { self.postbox?.setState(state) } - public func updatePeers(peers: [Peer], update: (Peer, Peer) -> Peer) { + public func updatePeers(peers: [Peer], update: (Peer, Peer) -> Peer?) { self.postbox?.updatePeers(peers, update: update) } - public func peersWithIds(ids: [PeerId]) -> [PeerId : Peer] { - return self.postbox?.peersWithIds(ids) ?? [:] + public func knownPeerIds(ids: Set) -> Set { + return self.postbox?.knownPeerIds(ids) ?? Set() } } -public final class Postbox { +public final class Postbox { private let basePath: String - private let messageNamespaces: [MessageId.Namespace] - private let absoluteIndexedMessageNamespace: MessageId.Namespace + private let globalMessageIdsNamespace: MessageId.Namespace private let queue = Queue(name: "org.telegram.postbox.Postbox") private var valueBox: ValueBox! - private var peerMessageHistoryViews: [PeerId : Bag<(MutableMessageHistoryView, Pipe)>] = [:] - private var deferredMessageHistoryViewsToUpdate: [(MutableMessageHistoryView, Pipe)] = [] - private var peerViews: Bag<(MutablePeerView, Pipe)> = Bag() - private var deferredPeerViewsToUpdate: [(MutablePeerView, Pipe)] = [] - private var peerPipes: [PeerId : Pipe] = [:] + private var viewTracker: ViewTracker! - private var statePipe: Pipe = Pipe() + private var peerPipes: [PeerId: Pipe] = [:] + + private var currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + private var currentFilledHolesByPeerId = Set() + private var currentUpdatedPeers: [PeerId: Peer] = [:] + + private var statePipe: Pipe = Pipe() + + private let fetchMessageHistoryHoleImpl = Promise Signal>() + public func setFetchMessageHistoryHole(fetch: MessageHistoryHole -> Signal) { + self.fetchMessageHistoryHoleImpl.set(single(fetch, NoError.self)) + } public let mediaBox: MediaBox - public init(basePath: String, messageNamespaces: [MessageId.Namespace], absoluteIndexedMessageNamespace: MessageId.Namespace?) { + var metadataTable: MetadataTable! + var keychainTable: KeychainTable! + var peerTable: PeerTable! + var globalMessageIdsTable: GlobalMessageIdsTable! + var messageHistoryIndexTable: MessageHistoryIndexTable! + var messageHistoryTable: MessageHistoryTable! + var mediaTable: MessageMediaTable! + var mediaCleanupTable: MediaCleanupTable! + var chatListIndexTable: ChatListIndexTable! + var chatListTable: ChatListTable! + + public init(basePath: String, globalMessageIdsNamespace: MessageId.Namespace) { self.basePath = basePath - self.messageNamespaces = messageNamespaces - if let absoluteIndexedMessageNamespace = absoluteIndexedMessageNamespace { - self.absoluteIndexedMessageNamespace = absoluteIndexedMessageNamespace - } else { - self.absoluteIndexedMessageNamespace = MessageId.Namespace.max - } + self.globalMessageIdsNamespace = globalMessageIdsNamespace self.mediaBox = MediaBox(basePath: self.basePath + "/media") self.openDatabase() } @@ -85,58 +101,58 @@ public final class Postbox { } self.valueBox = SqliteValueBox(basePath: self.basePath + "/db") - //self.valueBox = LmdbValueBox(basePath: self.basePath + "/db") - var userVersion: Int32 = 0 - let currentUserVersion: Int32 = 4 + self.metadataTable = MetadataTable(valueBox: self.valueBox, tableId: 0) - if let value = self.valueBox.get(Table_Meta.id, key: Table_Meta.key()) { - value.read(&userVersion, offset: 0, length: 4) - } + let userVersion: Int32? = self.metadataTable.userVersion() + let currentUserVersion: Int32 = 5 if userVersion != currentUserVersion { self.valueBox.drop() - let buffer = WriteBuffer() - var currentVersion: Int32 = currentUserVersion - buffer.write(¤tVersion, offset: 0, length: 4) - self.valueBox.set(Table_Meta.id, key: Table_Meta.key(), value: buffer) + self.metadataTable.setUserVersion(currentUserVersion) } + self.keychainTable = KeychainTable(valueBox: self.valueBox, tableId: 1) + self.peerTable = PeerTable(valueBox: self.valueBox, tableId: 2) + self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox, tableId: 3, namespace: self.globalMessageIdsNamespace) + self.messageHistoryIndexTable = MessageHistoryIndexTable(valueBox: self.valueBox, tableId: 4, globalMessageIdsTable: self.globalMessageIdsTable) + self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox, tableId: 5) + self.mediaTable = MessageMediaTable(valueBox: self.valueBox, tableId: 6, mediaCleanupTable: self.mediaCleanupTable) + self.messageHistoryTable = MessageHistoryTable(valueBox: self.valueBox, tableId: 7, messageHistoryIndexTable: self.messageHistoryIndexTable, messageMediaTable: self.mediaTable) + self.chatListIndexTable = ChatListIndexTable(valueBox: self.valueBox, tableId: 8) + self.chatListTable = ChatListTable(valueBox: self.valueBox, tableId: 9, indexTable: self.chatListIndexTable) + + self.viewTracker = ViewTracker(queue: self.queue, fetchEarlierHistoryEntries: self.fetchEarlierHistoryEntries, fetchLaterHistoryEntries: self.fetchLaterHistoryEntries, fetchEarlierChatEntries: self.fetchEarlierChatEntries, fetchLaterChatEntries: self.fetchLaterChatEntries, renderMessage: self.renderIntermediateMessage, fetchMessageHistoryHole: self.fetchMessageHistoryHoleWrapper) + print("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") } } - private var cachedState: State? + private var cachedState: Coding? - private func setState(state: State) { + private func setState(state: Coding) { self.queue.dispatch { self.cachedState = state - let encoder = Encoder() - encoder.encodeRootObject(state) - - self.valueBox.set(Table_State.id, key: Table_State.key(), value: encoder.memoryBuffer()) + self.metadataTable.setState(state) self.statePipe.putNext(state) } } - private func getState() -> State? { + private func getState() -> Coding? { if let cachedState = self.cachedState { return cachedState } else { - if let value = self.valueBox.get(Table_State.id, key: Table_State.key()) { - let decoder = Decoder(buffer: value) - if let state = decoder.decodeRootObject() as? State { - self.cachedState = state - return state - } + if let state = self.metadataTable.state() { + self.cachedState = state + return state } return nil } } - public func state() -> Signal { + public func state() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.dispatch { @@ -151,290 +167,169 @@ public final class Postbox { } public func keychainEntryForKey(key: String) -> NSData? { - if let value = self.valueBox.get(Table_Keychain.id, key: Table_Keychain.key(key)) { - print("get \(key) -> \(value.length) bytes") - return NSData(bytes: value.memory, length: value.length) - } - print("get \(key) -> nil") - - return nil + return self.keychainTable.get(key) } public func setKeychainEntryForKey(key: String, value: NSData) { - print("set \(key) -> \(value.length) bytes") - self.valueBox.set(Table_Keychain.id, key: Table_Keychain.key(key), value: MemoryBuffer(data: value)) + self.keychainTable.set(key, value: value) } public func removeKeychainEntryForKey(key: String) { - self.valueBox.remove(Table_Keychain.id, key: Table_Keychain.key(key)) + self.keychainTable.remove(key) } - private func addMessages(messages: [Message], medias: [Media]) { - + private func addMessages(messages: [StoreMessage], location: AddMessagesLocation) { + self.messageHistoryTable.addMessages(messages, location: location, operationsByPeerId: &self.currentOperationsByPeerId) } - private func mediaWithIds(ids: [MediaId]) -> [MediaId : Media] { - return [:] + private func addHole(id: MessageId) { + self.messageHistoryTable.addHoles([id], operationsByPeerId: &self.currentOperationsByPeerId) } - var cachedPeers: [PeerId : Peer] = [:] - - private func peerWithId(peerId: PeerId) -> Peer? { - if let cachedPeer = cachedPeers[peerId] { - return cachedPeer - } else { - let peerKey = Table_Peer.emptyKey() - if let value = self.valueBox.get(Table_Peer.id, key: Table_Peer.key(peerId, key: peerKey)) { - let decoder = Decoder(buffer: value) - if let peer = decoder.decodeRootObject() as? Peer { - cachedPeers[peer.id] = peer - return peer - } else { - print("(PostBox: can't decode peer)") - } - } - - return nil - } + private func fillHole(hole: MessageHistoryHole, fillType: HoleFillType, messages: [StoreMessage]) { + self.messageHistoryTable.fillHole(hole.id, fillType: fillType, messages: messages, operationsByPeerId: &self.currentOperationsByPeerId) + self.currentFilledHolesByPeerId.insert(hole.id.peerId) } - private func peersWithIds(ids: [PeerId]) -> [PeerId : Peer] { - if ids.count == 0 { - return [:] - } else { - var peers: [PeerId : Peer] = [:] - - for id in ids { - if let peer: Peer = self.peerWithId(id) { - peers[id] = peer - } - } - - return peers - } + private func deleteMessages(messageIds: [MessageId]) { + self.messageHistoryTable.removeMessages(messageIds, operationsByPeerId: &self.currentOperationsByPeerId) } - private func deferPeerViewUpdate(view: MutablePeerView, pipe: Pipe) { - var i = 0 - var found = false - while i < self.deferredPeerViewsToUpdate.count { - if self.deferredPeerViewsToUpdate[i].1 === pipe { - self.deferredPeerViewsToUpdate[i] = (view, pipe) - found = true - break - } - i++ - } - if !found { - self.deferredPeerViewsToUpdate.append((view, pipe)) - } - } - - private func deferMessageHistoryViewUpdate(view: MutableMessageHistoryView, pipe: Pipe) { - var i = 0 - var found = false - while i < self.deferredMessageHistoryViewsToUpdate.count { - if self.deferredMessageHistoryViewsToUpdate[i].1 === pipe { - self.deferredMessageHistoryViewsToUpdate[i] = (view, pipe) - found = true - break - } - i++ - } - if !found { - self.deferredMessageHistoryViewsToUpdate.append((view, pipe)) - } - } - - private func performDeferredUpdates() { - let deferredPeerViewsToUpdate = self.deferredPeerViewsToUpdate - self.deferredPeerViewsToUpdate.removeAll() + private func knownPeerIds(ids: Set) -> Set { + var result = Set() - for entry in deferredPeerViewsToUpdate { - let viewRenderedMessages = self.renderedMessages(entry.0.incompleteMessages()) - if viewRenderedMessages.count != 0 { - var viewRenderedMessagesDict: [MessageId : RenderedMessage] = [:] - for message in viewRenderedMessages { - viewRenderedMessagesDict[message.message.id] = message - } - entry.0.completeMessages(viewRenderedMessagesDict) - } - - entry.1.putNext(PeerView(entry.0)) - } - - let deferredMessageHistoryViewsToUpdate = self.deferredMessageHistoryViewsToUpdate - self.deferredMessageHistoryViewsToUpdate.removeAll() - - for entry in deferredMessageHistoryViewsToUpdate { - let viewRenderedMessages = self.renderedMessages(entry.0.incompleteMessages()) - if viewRenderedMessages.count != 0 { - var viewRenderedMessagesDict: [MessageId : RenderedMessage] = [:] - for message in viewRenderedMessages { - viewRenderedMessagesDict[message.message.id] = message - } - entry.0.completeMessages(viewRenderedMessagesDict) - } - - entry.1.putNext(MessageHistoryView(entry.0)) - } - } - - private func updatePeerEntry(peerId: PeerId, message: RenderedMessage?, replace: Bool = false) { - var currentIndex: PeerViewEntryIndex? - - if let value = self.valueBox.get(Table_PeerEntry.id, key: Table_PeerEntry.key(peerId)) { - currentIndex = Table_PeerEntry.get(peerId, value: value) - } - - var updatedPeerMessage: RenderedMessage? - - if let currentIndex = currentIndex { - if let message = message { - let messageIndex = MessageIndex(message.message) - if replace || currentIndex.messageIndex < messageIndex { - let updatedIndex = PeerViewEntryIndex(peerId: peerId, messageIndex: messageIndex) - updatedPeerMessage = message - - self.valueBox.remove(Table_PeerEntry_Sorted.id, key: Table_PeerEntry_Sorted.key(currentIndex)) - self.valueBox.set(Table_PeerEntry_Sorted.id, key: Table_PeerEntry_Sorted.key(updatedIndex), value: MemoryBuffer()) - self.valueBox.set(Table_PeerEntry.id, key: Table_PeerEntry.key(peerId), value: Table_PeerEntry.set(updatedIndex)) - } - } else if replace { - //TODO: remove? - } - } else if let message = message { - updatedPeerMessage = message - let updatedIndex = PeerViewEntryIndex(peerId: peerId, messageIndex: MessageIndex(message.message)) - - self.valueBox.set(Table_PeerEntry_Sorted.id, key: Table_PeerEntry_Sorted.key(updatedIndex), value: MemoryBuffer()) - self.valueBox.set(Table_PeerEntry.id, key: Table_PeerEntry.key(peerId), value: Table_PeerEntry.set(updatedIndex)) - } - - if let updatedPeerMessage = updatedPeerMessage { - var peer: Peer? - for (view, pipe) in self.peerViews.copyItems() { - if peer == nil { - for entry in view.entries { - if entry.peerId == peerId { - peer = entry.peer - break - } - } - - if peer == nil { - peer = self.peerWithId(peerId) - } - } - - let entry: PeerViewEntry - if let peer = peer { - entry = PeerViewEntry(peer: peer, message: updatedPeerMessage) - } else { - entry = PeerViewEntry(peerId: peerId, message: updatedPeerMessage) - } - - let context = view.removeEntry(nil, peerId: peerId) - view.addEntry(entry) - view.complete(context, fetchEarlier: self.fetchPeerEntriesRelative(true), fetchLater: self.fetchPeerEntriesRelative(false)) - - self.deferPeerViewUpdate(view, pipe: pipe) - } - } - } - - private func messageIdsForAbsoluteIndexedIds(ids: [Int32]) -> [MessageId] { - if ids.count == 0 { - return [] - } - - var result: [MessageId] = [] - - let key = Table_GlobalMessageId.emptyKey() for id in ids { - if let value = self.valueBox.get(Table_GlobalMessageId.id, key: Table_GlobalMessageId.key(id, key: key)) { - result.append(Table_GlobalMessageId.get(id, value: value)) + if let _ = self.peerTable.get(id) { + result.insert(id) } } return result } - private func deleteMessagesWithIds(ids: [MessageId]) { - + private func fetchEarlierHistoryEntries(peerId: PeerId, index: MessageIndex?, count: Int) -> [MutableMessageHistoryEntry] { + let intermediateEntries = self.messageHistoryTable.earlierEntries(peerId, index: index, count: count) + var entries: [MutableMessageHistoryEntry] = [] + for entry in intermediateEntries { + switch entry { + case let .Message(message): + entries.append(.IntermediateMessageEntry(message)) + case let .Hole(index): + entries.append(.HoleEntry(index)) + } + } + return entries } - private func updatePeers(peers: [Peer], update: (Peer, Peer) -> Peer) { - if peers.count == 0 { - return + private func fetchAroundHistoryEntries(index: MessageIndex, count: Int) -> [MutableMessageHistoryEntry] { + let intermediateEntries = self.messageHistoryTable.entriesAround(index, count: count) + var entries: [MutableMessageHistoryEntry] = [] + for entry in intermediateEntries { + switch entry { + case let .Message(message): + entries.append(.IntermediateMessageEntry(message)) + case let .Hole(index): + entries.append(.HoleEntry(index)) + } } + return entries + } + + private func fetchLaterHistoryEntries(peerId: PeerId, index: MessageIndex?, count: Int) -> [MutableMessageHistoryEntry] { + let intermediateEntries = self.messageHistoryTable.laterEntries(peerId, index: index, count: count) + var entries: [MutableMessageHistoryEntry] = [] + for entry in intermediateEntries { + switch entry { + case let .Message(message): + entries.append(.IntermediateMessageEntry(message)) + case let .Hole(index): + entries.append(.HoleEntry(index)) + } + } + return entries + } + + private func fetchEarlierChatEntries(index: MessageIndex?, count: Int) -> [MutableChatListEntry] { + let intermediateEntries = self.chatListTable.earlierEntries(index, messageHistoryTable: self.messageHistoryTable, count: count) + var entries: [MutableChatListEntry] = [] + for entry in intermediateEntries { + switch entry { + case let .Message(message): + entries.append(.IntermediateMessageEntry(message)) + case let .Nothing(index): + entries.append(.Nothing(index)) + } + } + return entries + } + + private func fetchLaterChatEntries(index: MessageIndex?, count: Int) -> [MutableChatListEntry] { + let intermediateEntries = self.chatListTable.laterEntries(index, messageHistoryTable: self.messageHistoryTable, count: count) + var entries: [MutableChatListEntry] = [] + for entry in intermediateEntries { + switch entry { + case let .Message(message): + entries.append(.IntermediateMessageEntry(message)) + case let .Nothing(index): + entries.append(.Nothing(index)) + } + } + return entries + } + + private func renderIntermediateMessage(message: IntermediateMessage) -> Message { + return self.messageHistoryTable.renderMessage(message, peerTable: self.peerTable) + } + + private func fetchMessageHistoryHoleWrapper(hole: MessageHistoryHole) -> Disposable { + return (self.fetchMessageHistoryHoleImpl.get() |> mapToSignal { fetch in + return fetch(hole) + }).start() + } + + private func beforeCommit() { + var chatListOperations: [ChatListOperation] = [] + self.chatListTable.replay(self.currentOperationsByPeerId, messageHistoryTable: self.messageHistoryTable, operations: &chatListOperations) - if peers.count == -1 { - - } else { - var peerIds: [PeerId] = [] - for peer in peers { - peerIds.append(peer.id) + self.viewTracker.updateViews(currentOperationsByPeerId: self.currentOperationsByPeerId, peerIdsWithFilledHoles: self.currentFilledHolesByPeerId, chatListOperations: chatListOperations, currentUpdatedPeers: self.currentUpdatedPeers) + + self.currentOperationsByPeerId.removeAll() + self.currentFilledHolesByPeerId.removeAll() + self.currentUpdatedPeers.removeAll() + } + + private func messageIdsForGlobalIds(ids: [Int32]) -> [MessageId] { + var result: [MessageId] = [] + for globalId in ids { + if let id = self.globalMessageIdsTable.get(globalId) { + result.append(id) } - - let currentPeers = self.peersWithIds(peerIds) - - let encoder = Encoder() - - var updatedPeers: [PeerId : Peer] = [:] - - let peerKey = Table_Peer.emptyKey() - for updatedPeer in peers { - let currentPeer = currentPeers[updatedPeer.id] - - var finalPeer = updatedPeer - if let currentPeer = currentPeer { - finalPeer = update(currentPeer, updatedPeer) - } - - if currentPeer == nil || !finalPeer.equalsTo(currentPeer!) { - updatedPeers[finalPeer.id] = finalPeer - self.cachedPeers[finalPeer.id] = finalPeer - - encoder.reset() - encoder.encodeRootObject(finalPeer) - - peerKey.setInt64(0, value: updatedPeer.id.toInt64()) - self.valueBox.set(Table_Peer.id, key: Table_Peer.key(finalPeer.id, key: peerKey), value: encoder.memoryBuffer()) - } - } - - for record in self.peerViews.copyItems() { - if record.0.updatePeers(updatedPeers) { - deferPeerViewUpdate(record.0, pipe: record.1) + } + return result + } + + private func updatePeers(peers: [Peer], update: (Peer, Peer) -> Peer?) { + for peer in peers { + if let currentPeer = self.peerTable.get(peer.id) { + if let updatedPeer = update(currentPeer, peer) { + self.peerTable.set(updatedPeer) + self.currentUpdatedPeers[updatedPeer.id] = updatedPeer } + } else { + self.peerTable.set(peer) } } } - public func modify(f: Modifier -> T) -> Signal { + public func modify(f: Modifier -> T) -> Signal { return Signal { subscriber in self.queue.dispatch { - //#if DEBUG - //let startTime = CFAbsoluteTimeGetCurrent() - //#endif - //self.valueBox.beginStats() self.valueBox.begin() let result = f(Modifier(postbox: self)) - //print("(Postbox modify took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms)") - //#if DEBUG - //startTime = CFAbsoluteTimeGetCurrent() - //#endif + self.beforeCommit() self.valueBox.commit() - //#if DEBUG - //print("(Postbox commit took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms)") - //self.valueBox.endStats() - //#endif - - self.performDeferredUpdates() - subscriber.putNext(result) subscriber.putCompletion() } @@ -442,111 +337,43 @@ public final class Postbox { } } - private func fetchPeerEntryIndicesRelative(earlier: Bool)(index: PeerViewEntryIndex?, count: Int) -> [PeerViewEntryIndex] { - var entries: [PeerViewEntryIndex] = [] - - let lowerBound = Table_PeerEntry_Sorted.lowerBoundKey() - let upperBound = Table_PeerEntry_Sorted.upperBoundKey() - - let bound: ValueBoxKey - if let index = index { - bound = Table_PeerEntry_Sorted.key(index) - } else if earlier { - bound = upperBound - } else { - bound = lowerBound - } - - let keys: ValueBoxKey -> Bool = { key in - entries.append(Table_PeerEntry_Sorted.get(key)) - - return true - } - - if earlier { - self.valueBox.range(Table_PeerEntry_Sorted.id, start: bound, end: lowerBound, keys: keys, limit: count) - } else { - self.valueBox.range(Table_PeerEntry_Sorted.id, start: bound, end: upperBound, keys: keys, limit: count) - } - - return entries + public func tailMessageHistoryViewForPeerId(peerId: PeerId, count: Int) -> Signal<(MessageHistoryView, ViewUpdateType), NoError> { + return self.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId), count: count) } - private func fetchPeerEntriesRelative(earlier: Bool)(index: PeerViewEntryIndex?, count: Int) -> [PeerViewEntry] { - var entries: [PeerViewEntry] = [] - var peers: [PeerId : Peer] = [:] - for entryIndex in self.fetchPeerEntryIndicesRelative(earlier)(index: index, count: count) { - var peer: Peer? - - if let cachedPeer = peers[entryIndex.peerId] { - peer = cachedPeer - } else { - if let fetchedPeer: Peer = self.peerWithId(entryIndex.peerId) { - peer = fetchedPeer - peers[fetchedPeer.id] = fetchedPeer - } - } - - var message: Message? - if let value = self.valueBox.get(Table_Message.id, key: Table_Message.key(entryIndex.messageIndex)) { - message = Table_Message.get(value) - } - - if let message = message, renderedMessage = self.renderedMessages([message]).first { - let entry: PeerViewEntry - if let peer = peer { - entry = PeerViewEntry(peer: peer, message: renderedMessage) - } else { - entry = PeerViewEntry(peerId: entryIndex.peerId, message: renderedMessage) - } - - entries.append(entry) - } else { - let entry: PeerViewEntry - if let peer = peer { - entry = PeerViewEntry(peer: peer, peerId: entryIndex.peerId, messageIndex: entryIndex.messageIndex) - } else { - entry = PeerViewEntry(peer: nil, peerId: entryIndex.peerId, messageIndex: entryIndex.messageIndex) - } - - entries.append(entry) - } - } - - //entries.sortInPlace() - - return entries.sort({ PeerViewEntryIndex($0) < PeerViewEntryIndex($1) }) - } - - private func renderedMessages(messages: [Message]) -> [RenderedMessage] { - return [] - } - - public func tailMessageHistoryViewForPeerId(peerId: PeerId, count: Int) -> Signal { + public func aroundMessageHistoryViewForPeerId(peerId: PeerId, index: MessageIndex, count: Int) -> Signal<(MessageHistoryView, ViewUpdateType), NoError> { return Signal { subscriber in - let startTime = CFAbsoluteTimeGetCurrent() - let disposable = MetaDisposable() self.queue.dispatch { + let list = self.fetchAroundHistoryEntries(index, count: count + 2) - print("tailMessageHistoryViewForPeerId fetch: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + var entries: [MutableMessageHistoryEntry] = [] + var earlier: MutableMessageHistoryEntry? + var later: MutableMessageHistoryEntry? - let mutableView = MutableMessageHistoryView(count: count, earlierMessage: nil, messages: [], laterMessage: nil) - let record = (mutableView, Pipe()) - - let index: Bag<(MutableMessageHistoryView, Pipe)>.Index - if let bag = self.peerMessageHistoryViews[peerId] { - index = bag.add(record) + if list.count >= count + 2 { + earlier = list[0] + for i in 1 ..< count + 1 { + entries.append(list[i]) + } + later = list[count + 1] + } else if list.count >= count + 1 { + for i in 0 ..< count { + entries.append(list[i]) + } + later = list[count] } else { - let bag = Bag<(MutableMessageHistoryView, Pipe)>() - index = bag.add(record) - self.peerMessageHistoryViews[peerId] = bag + entries = list } - subscriber.putNext(MessageHistoryView(mutableView)) + let mutableView = MutableMessageHistoryView(earlier: earlier, entries: entries, later: later, count: count) + mutableView.render(self.renderIntermediateMessage) + subscriber.putNext((MessageHistoryView(mutableView), .Generic)) - let pipeDisposable = record.1.signal().start(next: { next in + let (index, signal) = self.viewTracker.addMessageHistoryView(peerId, view: mutableView) + + let pipeDisposable = signal.start(next: { next in subscriber.putNext(next) }) @@ -555,9 +382,7 @@ public final class Postbox { if let strongSelf = self { strongSelf.queue.dispatch { - if let bag = strongSelf.peerMessageHistoryViews[peerId] { - bag.remove(index) - } + strongSelf.viewTracker.removeMessageHistoryView(peerId, index: index) } } return @@ -568,88 +393,34 @@ public final class Postbox { } } - public func aroundMessageHistoryViewForPeerId(peerId: PeerId, index: MessageIndex, count: Int) -> Signal { + public func tailChatListView(count: Int) -> Signal<(ChatListView, ViewUpdateType), NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.queue.dispatch { - let mutableView: MutableMessageHistoryView + let tail = self.fetchEarlierChatEntries(nil, count: count + 1).reverse() + var entries: [MutableChatListEntry] = [] + var earlier: MutableChatListEntry? - let startTime = CFAbsoluteTimeGetCurrent() - - mutableView = MutableMessageHistoryView(count: count, earlierMessage: nil, messages: [], laterMessage: nil) - - print("aroundMessageViewForPeerId fetch: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") - - let record = (mutableView, Pipe()) - - let index: Bag<(MutableMessageHistoryView, Pipe)>.Index - if let bag = self.peerMessageHistoryViews[peerId] { - index = bag.add(record) - } else { - let bag = Bag<(MutableMessageHistoryView, Pipe)>() - index = bag.add(record) - self.peerMessageHistoryViews[peerId] = bag - } - - subscriber.putNext(MessageHistoryView(mutableView)) - - let pipeDisposable = record.1.signal().start(next: { next in - subscriber.putNext(next) - }) - - disposable.set(ActionDisposable { [weak self] in - pipeDisposable.dispose() - - if let strongSelf = self { - strongSelf.queue.dispatch { - if let bag = strongSelf.peerMessageHistoryViews[peerId] { - bag.remove(index) - } - } + var i = 0 + for entry in tail { + if i < count { + entries.append(entry) + } else if i < count + 1 { + earlier = entry + } else { + break } - return - }) - } - - return disposable - } - } - - public func tailPeerView(count: Int) -> Signal { - return Signal { subscriber in - let disposable = MetaDisposable() - - self.queue.dispatch { - let startTime = CFAbsoluteTimeGetCurrent() - - let tail = self.fetchPeerEntriesRelative(true)(index: nil, count: count + 1) - - print("(Postbox fetchPeerEntriesRelative took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms)") - - var entries: [PeerViewEntry] = [] - var i = tail.count - 1 - while i >= 0 && i >= tail.count - count { - entries.insert(tail[i], atIndex: 0) - i-- + i++ } - var earlier: PeerViewEntry? + let mutableView = MutableChatListView(earlier: earlier, entries: entries, later: nil, count: count) + mutableView.render(self.renderIntermediateMessage) + subscriber.putNext((ChatListView(mutableView), .Generic)) - i = tail.count - count - 1 - while i >= 0 { - earlier = tail[i] - break - } + let (index, signal) = self.viewTracker.addChatListView(mutableView) - let mutableView = MutablePeerView(count: count, earlier: earlier, entries: entries, later: nil) - let record = (mutableView, Pipe()) - - let index = self.peerViews.add(record) - - subscriber.putNext(PeerView(mutableView)) - - let pipeDisposable = record.1.signal().start(next: { next in + let pipeDisposable = signal.start(next: { next in subscriber.putNext(next) }) @@ -658,7 +429,7 @@ public final class Postbox { if let strongSelf = self { strongSelf.queue.dispatch { - strongSelf.peerViews.remove(index) + strongSelf.viewTracker.removeChatListView(index) } } return @@ -673,7 +444,7 @@ public final class Postbox { return Signal { subscriber in let disposable = MetaDisposable() self.queue.dispatch { - if let peer: Peer = self.peerWithId(id) { + if let peer: Peer = self.peerTable.get(id) { subscriber.putNext(peer) } } diff --git a/Postbox/PostboxCodingUtils.swift b/Postbox/PostboxCodingUtils.swift deleted file mode 100644 index 189cecc38e..0000000000 --- a/Postbox/PostboxCodingUtils.swift +++ /dev/null @@ -1,233 +0,0 @@ -import Foundation - -func peerViewEntryIndexForBuffer(buffer: ReadBuffer) -> PeerViewEntryIndex { - var timestamp: Int32 = 0 - buffer.read(×tamp, offset: 0, length: 4) - timestamp = Int32(bigEndian: timestamp) - - var namespace: Int32 = 0 - buffer.read(&namespace, offset: 0, length: 4) - namespace = Int32(bigEndian: namespace) - - var id: Int32 = 0 - buffer.read(&id, offset: 0, length: 4) - id = Int32(bigEndian: id) - - var peerIdRepresentation: Int64 = 0 - buffer.read(&peerIdRepresentation, offset: 0, length: 8) - peerIdRepresentation = Int64(bigEndian: peerIdRepresentation) - - let peerId = PeerId(peerIdRepresentation) - - return PeerViewEntryIndex(peerId: peerId, messageIndex: MessageIndex(id: MessageId(peerId:peerId, namespace: namespace, id: id), timestamp: timestamp)) -} - -func bufferForPeerViewEntryIndex(index: PeerViewEntryIndex) -> MemoryBuffer { - let buffer = WriteBuffer() - - var timestamp = Int32(bigEndian: index.messageIndex.timestamp) - buffer.write(×tamp, offset: 0, length: 4) - - var namespace = Int32(bigEndian: index.messageIndex.id.namespace) - buffer.write(&namespace, offset: 0, length: 4) - - var id = Int32(bigEndian: index.messageIndex.id.id) - buffer.write(&id, offset: 0, length: 4) - - var peerIdRepresentation = Int64(bigEndian: index.peerId.toInt64()) - buffer.write(&peerIdRepresentation, offset: 0, length: 8) - - return buffer -} - -func messageIdsGroupedByNamespace(ids: [MessageId]) -> [MessageId.Namespace : [MessageId]] { - var grouped: [MessageId.Namespace : [MessageId]] = [:] - - for id in ids { - if grouped[id.namespace] != nil { - grouped[id.namespace]!.append(id) - } else { - grouped[id.namespace] = [id] - } - } - - return grouped -} - -func mediaIdsGroupedByNamespaceFromMediaArray(mediaArray: [Media]) -> [MediaId.Namespace : [MediaId]] { - var grouped: [MediaId.Namespace : [MediaId]] = [:] - var seenMediaIds = Set() - - for media in mediaArray { - if let id = media.id { - if !seenMediaIds.contains(id) { - seenMediaIds.insert(id) - if grouped[id.namespace] != nil { - grouped[id.namespace]!.append(id) - } else { - grouped[id.namespace] = [id] - } - } - } - } - - return grouped -} - -func mediaIdsGroupedByNamespaceFromSet(ids: Set) -> [MediaId.Namespace : [MediaId]] { - var grouped: [MediaId.Namespace : [MediaId]] = [:] - - for id in ids { - if let _ = grouped[id.namespace] { - grouped[id.namespace]!.append(id) - } else { - grouped[id.namespace] = [id] - } - } - - return grouped -} - -func mediaIdsGroupedByNamespaceFromDictionaryKeys(dict: [MediaId : T]) -> [MediaId.Namespace : [MediaId]] { - var grouped: [MediaId.Namespace : [MediaId]] = [:] - - for (id, _) in dict { - if grouped[id.namespace] != nil { - grouped[id.namespace]!.append(id) - } else { - grouped[id.namespace] = [id] - } - } - - return grouped -} - -func messagesGroupedByPeerId(messages: [Message]) -> [(PeerId, [Message])] { - var grouped: [(PeerId, [Message])] = [] - - for message in messages { - var i = 0 - let count = grouped.count - var found = false - while i < count { - if grouped[i].0 == message.id.peerId { - grouped[i].1.append(message) - found = true - break - } - i++ - } - if !found { - grouped.append((message.id.peerId, [message])) - } - } - - return grouped -} - -func messageIdsGroupedByPeerId(messageIds: [MessageId]) -> [PeerId : [MessageId]] { - var grouped: [PeerId : [MessageId]] = [:] - - for id in messageIds { - if grouped[id.peerId] != nil { - grouped[id.peerId]!.append(id) - } else { - grouped[id.peerId] = [id] - } - } - - return grouped -} - -func blobForMediaIds(ids: [MediaId]) -> Blob { - let data = NSMutableData() - var version: Int8 = 1 - data.appendBytes(&version, length: 1) - - var count = Int32(ids.count) - data.appendBytes(&count, length:4) - - for id in ids { - var mNamespace = id.namespace - var mId = id.id - data.appendBytes(&mNamespace, length: 4) - data.appendBytes(&mId, length: 8) - } - - return Blob(data: data) -} - -func mediaIdsForBlob(blob: Blob) -> [MediaId] { - var ids: [MediaId] = [] - - var offset: Int = 0 - var version = 0 - blob.data.getBytes(&version, range: NSMakeRange(offset, 1)) - offset += 1 - - if version == 1 { - var count: Int32 = 0 - blob.data.getBytes(&count, range: NSMakeRange(offset, 4)) - offset += 4 - - var i = 0 - while i < Int(count) { - var mNamespace: Int32 = 0 - var mId: Int64 = 0 - blob.data.getBytes(&mNamespace, range: NSMakeRange(offset, 4)) - blob.data.getBytes(&mId, range: NSMakeRange(offset + 4, 8)) - ids.append(MediaId(namespace: mNamespace, id: mId)) - offset += 12 - i++ - } - } - - return ids -} - -func memoryBufferForMessageIds(ids: [MessageId]) -> MemoryBuffer { - let data = NSMutableData() - var version: Int8 = 1 - data.appendBytes(&version, length: 1) - - var count = Int32(ids.count) - data.appendBytes(&count, length:4) - - for id in ids { - var mPeerNamespace = id.peerId.namespace - var mPeerId = id.peerId.id - var mNamespace = id.namespace - var mId = id.id - data.appendBytes(&mPeerNamespace, length: 4) - data.appendBytes(&mPeerId, length: 4) - data.appendBytes(&mNamespace, length: 4) - data.appendBytes(&mId, length: 4) - } - - return MemoryBuffer(data: data) -} - -func messageIdsForMemoryBuffer(buffer: MemoryBuffer) -> [MessageId] { - var ids: [MessageId] = [] - - let readBuffer = ReadBuffer(memoryBufferNoCopy: buffer) - - var count: Int32 = 0 - readBuffer.read(&count, offset: 0, length: 4) - - var i = 0 - while i < Int(count) { - var mPeerNamespace: Int32 = 0 - var mPeerId: Int32 = 0 - var mNamespace: Int32 = 0 - var mId: Int32 = 0 - readBuffer.read(&mPeerNamespace, offset: 0, length: 4) - readBuffer.read(&mPeerId, offset: 0, length: 4) - readBuffer.read(&mNamespace, offset: 0, length: 4) - readBuffer.read(&mId, offset: 0, length: 4) - ids.append(MessageId(peerId: PeerId(namespace: mPeerNamespace, id: mPeerId), namespace: mNamespace, id: mId)) - i++ - } - - return ids -} diff --git a/Postbox/PostboxTables.swift b/Postbox/PostboxTables.swift deleted file mode 100644 index 8f048b3912..0000000000 --- a/Postbox/PostboxTables.swift +++ /dev/null @@ -1,300 +0,0 @@ -import Foundation - -struct Table_Meta { - static let id: Int32 = 0 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 4) - } - - static func key(key: ValueBoxKey = Table_Meta.emptyKey()) -> ValueBoxKey { - key.setInt32(0, value: 0) - return key - } -} - -struct Table_State { - static let id: Int32 = 1 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 4) - } - - static func key(key: ValueBoxKey = Table_State.emptyKey()) -> ValueBoxKey { - key.setInt32(0, value: 0) - return key - } -} - -struct Table_Keychain { - static let id: Int32 = 2 - - static func key(string: String) -> ValueBoxKey { - let data = string.dataUsingEncoding(NSUTF8StringEncoding) ?? NSData() - let key = ValueBoxKey(length: data.length) - memcpy(key.memory, data.bytes, data.length) - return key - } -} - -struct Table_Message { - static let id: Int32 = 4 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 8 + 4 + 4 + 4) - } - - static func lowerBoundKey(peerId: PeerId) -> ValueBoxKey { - let key = ValueBoxKey(length: 8) - key.setInt64(0, value: peerId.toInt64()) - return key - } - - static func upperBoundKey(peerId: PeerId) -> ValueBoxKey { - let key = ValueBoxKey(length: 8) - key.setInt64(0, value: peerId.toInt64()) - return key.successor - } - - static func key(index: MessageIndex, key: ValueBoxKey = Table_Message.emptyKey()) -> ValueBoxKey { - key.setInt64(0, value: index.id.peerId.toInt64()) - key.setInt32(8, value: index.timestamp) - key.setInt32(8 + 4, value: index.id.namespace) - key.setInt32(8 + 4 + 4, value: index.id.id) - return key - } - - static func set(message: Message, encoder: Encoder = Encoder()) -> MemoryBuffer { - encoder.reset() - return encoder.memoryBuffer() - } - - static func get(value: ReadBuffer) -> Message? { - if let message = Decoder(buffer: value).decodeRootObject() as? Message { - return message - } - return nil - } -} - -struct Table_GlobalMessageId { - static let id: Int32 = 5 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 4) - } - - static func key(id: Int32, key: ValueBoxKey = Table_GlobalMessageId.emptyKey()) -> ValueBoxKey { - key.setInt32(0, value: id) - return key - } - - static func set(messageId: MessageId) -> MemoryBuffer { - let buffer = WriteBuffer() - - var peerId: Int64 = messageId.peerId.toInt64() - buffer.write(&peerId, offset: 0, length: 8) - var id_namespace: Int32 = messageId.namespace - buffer.write(&id_namespace, offset: 0, length: 4) - - return buffer - } - - static func get(id: Int32, value: ReadBuffer) -> MessageId { - let offset = value.offset - - var peerId: Int64 = 0 - var id_namespace: Int32 = 0 - value.read(&peerId, offset: 0, length: 8) - value.read(&id_namespace, offset: 0, length: 4) - value.offset = offset - - return MessageId(peerId: PeerId(peerId), namespace: id_namespace, id: id) - } -} - -struct Table_Media { - static let id: Int32 = 6 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 4 + 8) - } - - static func key(id: MediaId, key: ValueBoxKey = Table_Media.emptyKey()) -> ValueBoxKey { - key.setInt32(0, value: id.namespace) - key.setInt64(4, value: id.id) - return key - } - - static func set(media: Media, encoder: Encoder = Encoder()) -> MemoryBuffer { - encoder.reset() - encoder.encodeRootObject(media) - return encoder.memoryBuffer() - } - - static func get(value: ReadBuffer) -> Media? { - let decoder = Decoder(buffer: value) - if let media = decoder.decodeRootObject() as? Media { - return media - } - - return nil - } -} - -struct Table_Media_MessageIds { - static let id: Int32 = 3 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 4 + 8 + 8 + 4 + 4) - } - - static func key(id: MediaId, messageId: MessageId, key: ValueBoxKey = Table_Media_MessageIds.emptyKey()) -> ValueBoxKey { - key.setInt32(0, value: id.namespace) - key.setInt64(4, value: id.id) - key.setInt64(4 + 8, value: messageId.peerId.toInt64()) - key.setInt32(4 + 8 + 8, value: messageId.namespace) - key.setInt32(4 + 8 + 8 + 4, value: messageId.id) - return key - } - - static func lowerBoundKey(id: MediaId) -> ValueBoxKey { - let key = ValueBoxKey(length: 4 + 8) - key.setInt32(0, value: id.namespace) - key.setInt64(4, value: id.id) - return key - } - - static func upperBoundKey(id: MediaId) -> ValueBoxKey { - let key = ValueBoxKey(length: 4 + 8) - key.setInt32(0, value: id.namespace) - key.setInt64(4, value: id.id) - return key.successor - } - - static func getMessageId(key: ValueBoxKey) -> MessageId { - let peerId = key.getInt64(4 + 8) - let messageId_namespace: Int32 = key.getInt32(4 + 8 + 8) - let messageId_id: Int32 = key.getInt32(4 + 8 + 8 + 4) - return MessageId(peerId: PeerId(peerId), namespace: messageId_namespace, id: messageId_id) - } -} - -struct Table_Peer { - static let id: Int32 = 7 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 4 + 4) - } - - static func key(id: PeerId, key: ValueBoxKey = Table_Peer.emptyKey()) -> ValueBoxKey { - key.setInt32(0, value: id.namespace) - key.setInt32(4, value: id.id) - return key - } -} - -struct Table_PeerEntry_Sorted { - static let id: Int32 = 8 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 4 + 4 + 4 + 8) - } - - static func lowerBoundKey() -> ValueBoxKey { - let key = ValueBoxKey(length: 4) - key.setInt32(0, value: 0) - return key - } - - static func upperBoundKey() -> ValueBoxKey { - let key = ValueBoxKey(length: 4) - key.setInt32(0, value: Int32.max) - return key - } - - static func key(index: PeerViewEntryIndex, key: ValueBoxKey = Table_PeerEntry_Sorted.emptyKey()) -> ValueBoxKey { - key.setInt32(0, value: index.messageIndex.timestamp) - key.setInt32(4, value: index.messageIndex.id.namespace) - key.setInt32(4 + 4, value: index.messageIndex.id.id) - key.setInt64(4 + 4 + 4, value: index.peerId.toInt64()) - return key - } - - static func get(key: ValueBoxKey) -> PeerViewEntryIndex { - let messageIndex_timestamp = key.getInt32(0) - let messageIndex_id_namespace = key.getInt32(4) - let messageIndex_id_id = key.getInt32(4 + 4) - let messageIndex_peerId = key.getInt64(4 + 4 + 4) - return PeerViewEntryIndex(peerId: PeerId(messageIndex_peerId), messageIndex: MessageIndex(id: MessageId(peerId: PeerId(messageIndex_peerId), namespace: messageIndex_id_namespace, id: messageIndex_id_id), timestamp: messageIndex_timestamp)) - } -} - -struct Table_PeerEntry { - static let id: Int32 = 9 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 8) - } - - static func key(peerId: PeerId, key: ValueBoxKey = Table_PeerEntry.emptyKey()) -> ValueBoxKey { - key.setInt64(0, value: peerId.toInt64()) - return key - } - - static func set(index: PeerViewEntryIndex) -> MemoryBuffer { - let buffer = WriteBuffer() - - var messageId_namespace: Int32 = index.messageIndex.id.namespace - var messageId_id: Int32 = index.messageIndex.id.id - var timestamp: Int32 = index.messageIndex.timestamp - buffer.write(&messageId_namespace, offset: 0, length: 4) - buffer.write(&messageId_id, offset: 0, length: 4) - buffer.write(×tamp, offset: 0, length: 4) - - return buffer - } - - static func get(peerId: PeerId, value: ReadBuffer) -> PeerViewEntryIndex { - let offset = value.offset - - var messageId_namespace: Int32 = 0 - var messageId_id: Int32 = 0 - var timestamp: Int32 = 0 - value.read(&messageId_namespace, offset: 0, length: 4) - value.read(&messageId_id, offset: 0, length: 4) - value.read(×tamp, offset: 0, length: 4) - let index = PeerViewEntryIndex(peerId: peerId, messageIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: messageId_namespace, id: messageId_id), timestamp: timestamp)) - value.offset = offset - - return index - } -} - -struct Table_MessageIndex { - static let id: Int32 = 11 - - static func emptyKey() -> ValueBoxKey { - return ValueBoxKey(length: 8 + 4 + 4) - } - - static func key(id: MessageId, key: ValueBoxKey = Table_MessageIndex.emptyKey()) -> ValueBoxKey { - key.setInt64(0, value: id.peerId.toInt64()) - key.setInt32(8, value: id.namespace) - key.setInt32(8 + 4, value: id.id) - return key - } - - static func set(index: MessageIndex) -> MemoryBuffer { - let buffer = WriteBuffer() - var timestamp = index.timestamp - buffer.write(×tamp, offset: 0, length: 4) - return buffer.readBufferNoCopy() - } - - static func get(id: MessageId, value: ReadBuffer) -> MessageIndex { - var timestamp: Int32 = 0 - value.read(×tamp, offset: 0, length: 4) - return MessageIndex(id: MessageId(peerId: id.peerId, namespace: id.namespace, id: id.id), timestamp: timestamp) - } -} diff --git a/Postbox/SimpleDictionary.swift b/Postbox/SimpleDictionary.swift new file mode 100644 index 0000000000..3ab962a23a --- /dev/null +++ b/Postbox/SimpleDictionary.swift @@ -0,0 +1,42 @@ +import Foundation + +public struct SimpleDictionary: SequenceType { + private var items: [(K, V)] = [] + + public subscript(key: K) -> V? { + get { + for (k, value) in self.items { + if k == key { + return value + } + } + return nil + } set(value) { + var index = 0 + for (k, _) in self.items { + if k == key { + if let value = value { + self.items[index] = (k, value) + } else { + self.items.removeAtIndex(index) + } + return + } + index++ + } + if let value = value { + self.items.append((key, value)) + } + } + } + + public func generate() -> AnyGenerator<(K, V)> { + var index = 0 + return anyGenerator { () -> (K, V)? in + if index < self.items.count { + return self.items[index++] + } + return nil + } + } +} \ No newline at end of file diff --git a/Postbox/SqliteValueBox.swift b/Postbox/SqliteValueBox.swift index e5a954326f..3e1be6dbdf 100644 --- a/Postbox/SqliteValueBox.swift +++ b/Postbox/SqliteValueBox.swift @@ -8,6 +8,10 @@ private struct SqlitePreparedStatement { sqlite3_bind_blob(statement, Int32(index), data, Int32(length), nil) } + func bindNull(index: Int) { + sqlite3_bind_null(statement, Int32(index)) + } + func bind(index: Int, number: Int32) { sqlite3_bind_int(statement, Int32(index), number) } @@ -366,7 +370,11 @@ public final class SqliteValueBox: ValueBox { resultStatement.reset() resultStatement.bind(1, data: key.memory, length: key.length) - resultStatement.bind(2, data: value.memory, length: value.length) + if value.memory == nil { + resultStatement.bindNull(2) + } else { + resultStatement.bind(2, data: value.memory, length: value.length) + } return resultStatement } diff --git a/Postbox/ViewTracker.swift b/Postbox/ViewTracker.swift new file mode 100644 index 0000000000..6ffcbfcf9b --- /dev/null +++ b/Postbox/ViewTracker.swift @@ -0,0 +1,199 @@ +import Foundation +import SwiftSignalKit + +public enum ViewUpdateType { + case Generic + case FillHole +} + +final class ViewTracker { + private let queue: Queue + private let fetchEarlierHistoryEntries: (PeerId, MessageIndex?, Int) -> [MutableMessageHistoryEntry] + private let fetchLaterHistoryEntries: (PeerId, MessageIndex?, Int) -> [MutableMessageHistoryEntry] + private let fetchEarlierChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry] + private let fetchLaterChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry] + private let renderMessage: IntermediateMessage -> Message + private let fetchMessageHistoryHole: MessageHistoryHole -> Disposable + + private var messageHistoryViews: [PeerId: Bag<(MutableMessageHistoryView, Pipe<(MessageHistoryView, ViewUpdateType)>)>] = [:] + private var chatListViews = Bag<(MutableChatListView, Pipe<(ChatListView, ViewUpdateType)>)>() + + private var holeDisposablesByPeerId: [PeerId: [(MessageHistoryHole, Disposable)]] = [:] + + init(queue: Queue, fetchEarlierHistoryEntries: (PeerId, MessageIndex?, Int) -> [MutableMessageHistoryEntry], fetchLaterHistoryEntries: (PeerId, MessageIndex?, Int) -> [MutableMessageHistoryEntry], fetchEarlierChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], fetchLaterChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], renderMessage: IntermediateMessage -> Message, fetchMessageHistoryHole: MessageHistoryHole -> Disposable) { + self.queue = queue + self.fetchEarlierHistoryEntries = fetchEarlierHistoryEntries + self.fetchLaterHistoryEntries = fetchLaterHistoryEntries + self.fetchEarlierChatEntries = fetchEarlierChatEntries + self.fetchLaterChatEntries = fetchLaterChatEntries + self.renderMessage = renderMessage + self.fetchMessageHistoryHole = fetchMessageHistoryHole + } + + deinit { + for (_, indexToDisposable) in holeDisposablesByPeerId { + for (_, disposable) in indexToDisposable { + disposable.dispose() + } + } + } + + func addMessageHistoryView(peerId: PeerId, view: MutableMessageHistoryView) -> (Bag<(MutableMessageHistoryView, Pipe<(MessageHistoryView, ViewUpdateType)>)>.Index, Signal<(MessageHistoryView, ViewUpdateType), NoError>) { + let record = (view, Pipe<(MessageHistoryView, ViewUpdateType)>()) + + let index: Bag<(MutableMessageHistoryView, Pipe<(MessageHistoryView, ViewUpdateType)>)>.Index + if let bag = self.messageHistoryViews[peerId] { + index = bag.add(record) + } else { + let bag = Bag<(MutableMessageHistoryView, Pipe<(MessageHistoryView, ViewUpdateType)>)>() + index = bag.add(record) + self.messageHistoryViews[peerId] = bag + } + + self.updateTrackedHoles(peerId) + + return (index, record.1.signal()) + } + + func removeMessageHistoryView(peerId: PeerId, index: Bag<(MutableMessageHistoryView, Pipe<(MessageHistoryView, ViewUpdateType)>)>.Index) { + if let bag = self.messageHistoryViews[peerId] { + bag.remove(index) + + self.updateTrackedHoles(peerId) + } + } + + func addChatListView(view: MutableChatListView) -> (Bag<(MutableChatListView, Pipe<(ChatListView, ViewUpdateType)>)>.Index, Signal<(ChatListView, ViewUpdateType), NoError>) { + let record = (view, Pipe<(ChatListView, ViewUpdateType)>()) + + let index = self.chatListViews.add(record) + return (index, record.1.signal()) + } + + func removeChatListView(index: Bag<(MutableChatListView, Pipe)>.Index) { + self.chatListViews.remove(index) + } + + func updateViews(currentOperationsByPeerId currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], peerIdsWithFilledHoles: Set, chatListOperations: [ChatListOperation], currentUpdatedPeers: [PeerId: Peer]) { + var updateTrackedHolesPeerIds: [PeerId] = [] + + for (peerId, bag) in self.messageHistoryViews { + var updateHoles = false + if let operations = currentOperationsByPeerId[peerId] { + updateHoles = true + for (mutableView, pipe) in bag.copyItems() { + let context = MutableMessageHistoryViewReplayContext() + var updated = false + if mutableView.replay(operations, context: context) { + mutableView.complete(context, fetchEarlier: { index, count in + return self.fetchEarlierHistoryEntries(peerId, index, count) + }, fetchLater: { index, count in + return self.fetchLaterHistoryEntries(peerId, index, count) + }) + updated = true + } + + if mutableView.updatePeers(currentUpdatedPeers) { + updated = true + } + + if updated { + mutableView.render(self.renderMessage) + pipe.putNext((MessageHistoryView(mutableView), peerIdsWithFilledHoles.contains(peerId) ? .FillHole : .Generic)) + } + } + } + + if updateHoles { + updateTrackedHolesPeerIds.append(peerId) + } + } + + if chatListOperations.count != 0 { + for (mutableView, pipe) in self.chatListViews.copyItems() { + let context = MutableChatListViewReplayContext() + if mutableView.replay(chatListOperations, context: context) { + mutableView.complete(context, fetchEarlier: self.fetchEarlierChatEntries, fetchLater: self.fetchLaterChatEntries) + mutableView.render(self.renderMessage) + pipe.putNext((ChatListView(mutableView), .Generic)) + } + } + } + + for peerId in updateTrackedHolesPeerIds { + self.updateTrackedHoles(peerId) + } + } + + func updateTrackedHoles(peerId: PeerId) { + if let bag = self.messageHistoryViews[peerId] { + var disposeHoles: [Disposable] = [] + + var firstHoles: [MessageHistoryHole] = [] + + for (view, _) in bag.copyItems() { + if let hole = view.firstHole() { + var exists = false + for existingHole in firstHoles { + if existingHole == hole { + exists = true + break + } + } + if !exists { + firstHoles.append(hole) + } + } + } + + if let holes = self.holeDisposablesByPeerId[peerId] { + var i = 0 + for (hole, disposable) in holes { + var exists = false + for firstHole in firstHoles { + if hole == firstHole { + exists = true + break + } + } + if !exists { + disposeHoles.append(disposable) + self.holeDisposablesByPeerId[peerId]!.removeAtIndex(i) + } else { + i++ + } + } + } + + for disposable in disposeHoles { + disposable.dispose() + } + + for hole in firstHoles { + var exists = false + if let existingHoles = self.holeDisposablesByPeerId[hole.id.peerId] { + for (existingHole, _) in existingHoles { + if existingHole == hole { + exists = true + break + } + } + } + + if !exists { + if self.holeDisposablesByPeerId[hole.id.peerId] == nil { + self.holeDisposablesByPeerId[hole.id.peerId] = [] + } + + self.holeDisposablesByPeerId[hole.id.peerId]!.append((hole, self.fetchMessageHistoryHole(hole))) + } + } + } else if let holes = self.holeDisposablesByPeerId[peerId] { + self.holeDisposablesByPeerId[peerId]?.removeAll() + + for (_, disposable) in holes { + disposable.dispose() + } + } + } +} diff --git a/PostboxTests/ChatListTableTests.swift b/PostboxTests/ChatListTableTests.swift new file mode 100644 index 0000000000..f8a7e39f76 --- /dev/null +++ b/PostboxTests/ChatListTableTests.swift @@ -0,0 +1,185 @@ +import Foundation + +import UIKit +import XCTest + +import Postbox +@testable import Postbox + +private let namespace: Int32 = 1 + +private let authorPeerId = PeerId(namespace: 2, id: 3) + +private enum Entry: Equatable, CustomStringConvertible { + case Message(Int32, Int32, Int32) + case Nothing(Int32, Int32, Int32) + + var description: String { + switch self { + case let .Message(peerId, id, timestamp): + return "Message(\(peerId), \(id), \(timestamp))" + case let .Nothing(peerId, id, timestamp): + return "Nothing(\(peerId), \(id), \(timestamp))" + } + } +} + +private func ==(lhs: Entry, rhs: Entry) -> Bool { + switch lhs { + case let .Message(lhsPeerId, lhsId, lhsTimestamp): + switch rhs { + case let .Message(rhsPeerId, rhsId, rhsTimestamp): + return lhsPeerId == rhsPeerId && lhsId == rhsId && lhsTimestamp == rhsTimestamp + case .Nothing: + return false + } + case let .Nothing(lhsPeerId, lhsId, lhsTimestamp): + switch rhs { + case .Message: + return false + case let .Nothing(rhsPeerId, rhsId, rhsTimestamp): + return lhsPeerId == rhsPeerId && lhsId == rhsId && lhsTimestamp == rhsTimestamp + } + } +} + +class ChatListTableTests: XCTestCase { + var valueBox: ValueBox? + var path: String? + + var globalMessageIdsTable: GlobalMessageIdsTable? + var indexTable: MessageHistoryIndexTable? + var mediaTable: MessageMediaTable? + var mediaCleanupTable: MediaCleanupTable? + var historyTable: MessageHistoryTable? + var chatListIndexTable: ChatListIndexTable? + var chatListTable: ChatListTable? + + override class func setUp() { + super.setUp() + } + + override func setUp() { + super.setUp() + + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + path = NSTemporaryDirectory().stringByAppendingString("\(randomId)") + self.valueBox = SqliteValueBox(basePath: path!) + + self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox!, tableId: 7, namespace: namespace) + self.indexTable = MessageHistoryIndexTable(valueBox: self.valueBox!, tableId: 1, globalMessageIdsTable: self.globalMessageIdsTable!) + self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox!, tableId: 3) + self.mediaTable = MessageMediaTable(valueBox: self.valueBox!, tableId: 2, mediaCleanupTable: self.mediaCleanupTable!) + self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!) + self.chatListIndexTable = ChatListIndexTable(valueBox: self.valueBox!, tableId: 5) + self.chatListTable = ChatListTable(valueBox: self.valueBox!, tableId: 6, indexTable: self.chatListIndexTable!) + } + + override func tearDown() { + super.tearDown() + + self.historyTable = nil + self.indexTable = nil + self.mediaTable = nil + self.mediaCleanupTable = nil + self.chatListIndexTable = nil + self.chatListTable = nil + + self.valueBox = nil + let _ = try? NSFileManager.defaultManager().removeItemAtPath(path!) + self.path = nil + } + + private func addMessage(peerId: Int32, _ id: Int32, _ timestamp: Int32, _ text: String = "", _ media: [Media] = []) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id), timestamp: timestamp, authorId: authorPeerId, text: text, attributes: [], media: media)], location: .Random, operationsByPeerId: &operationsByPeerId) + var operations: [ChatListOperation] = [] + self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) + } + + private func addHole(peerId: Int32, _ id: Int32) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.historyTable!.addHoles([MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id)], operationsByPeerId: &operationsByPeerId) + var operations: [ChatListOperation] = [] + self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) + } + + private func removeMessages(peerId: Int32, _ ids: [Int32]) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.historyTable!.removeMessages(ids.map({ MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: $0) }), operationsByPeerId: &operationsByPeerId) + 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])]) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.historyTable!.fillHole(MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id), fillType: fillType, messages: messages.map({ StoreMessage(id: MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: $0.0), timestamp: $0.1, authorId: authorPeerId, text: $0.2, attributes: [], media: $0.3) }), operationsByPeerId: &operationsByPeerId) + var operations: [ChatListOperation] = [] + self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) + } + + private func expectEntries(entries: [Entry]) { + let actualEntries = self.chatListTable!.debugList(self.historyTable!).map({ entry -> Entry in + switch entry { + case let .Message(message): + if message.authorId != authorPeerId { + XCTFail("Expected authorId \(authorPeerId), actual \(message.authorId)") + } + return .Message(message.id.peerId.id, message.id.id, message.timestamp) + case let .Nothing(index): + return .Nothing(index.id.peerId.id, index.id.id, index.timestamp) + } + }) + if entries != actualEntries { + XCTFail("Expected\n\(entries)\nActual\n\(actualEntries)") + } + } + + func testEmpty() { + expectEntries([]) + } + + func testAddSingleMessage() { + addMessage(1, 100, 100) + expectEntries([.Message(1, 100, 100)]) + } + + func testInsertLaterMessage() { + addMessage(1, 100, 100) + addMessage(1, 200, 200) + expectEntries([.Message(1, 200, 200)]) + } + + func testInsertEarlierMessage() { + addMessage(1, 100, 100) + addMessage(1, 10, 20) + expectEntries([.Message(1, 100, 100)]) + } + + func testInsertTwoChatMessages() { + addMessage(1, 100, 100) + addMessage(2, 10, 20) + expectEntries([.Message(2, 10, 20), .Message(1, 100, 100)]) + } + + func testMoveChatUpper() { + addMessage(1, 100, 100) + addMessage(2, 10, 20) + addMessage(2, 120, 120) + expectEntries([.Message(1, 100, 100), .Message(2, 120, 120)]) + } + + func testRemoveSingleMessage() { + addMessage(1, 100, 100) + removeMessages(1, [100]) + expectEntries([.Nothing(1, 100, 100)]) + } + + func testOverrideNothing() { + addMessage(1, 100, 100) + removeMessages(1, [100]) + addMessage(1, 100, 100) + expectEntries([.Message(1, 100, 100)]) + } +} \ No newline at end of file diff --git a/PostboxTests/CodingTests.swift b/PostboxTests/CodingTests.swift index 00c75ab465..51963433c8 100644 --- a/PostboxTests/CodingTests.swift +++ b/PostboxTests/CodingTests.swift @@ -97,7 +97,7 @@ func ==(lhs: TestKey, rhs: TestKey) -> Bool { return lhs.value == rhs.value } -class EmptyState: PostboxState { +class EmptyState: Coding { required init(decoder: Decoder) { } diff --git a/PostboxTests/MessageHistoryIndexTableTests.swift b/PostboxTests/MessageHistoryIndexTableTests.swift index 9c742da934..ca5b7bb9db 100644 --- a/PostboxTests/MessageHistoryIndexTableTests.swift +++ b/PostboxTests/MessageHistoryIndexTableTests.swift @@ -58,6 +58,7 @@ class MessageHistoryIndexTableTests: XCTestCase { var path: String? var indexTable: MessageHistoryIndexTable? + var globalMessageIdsTable: GlobalMessageIdsTable? override func setUp() { super.setUp() @@ -67,7 +68,8 @@ class MessageHistoryIndexTableTests: XCTestCase { path = NSTemporaryDirectory().stringByAppendingString("\(randomId)") self.valueBox = SqliteValueBox(basePath: path!) - self.indexTable = MessageHistoryIndexTable(valueBox: self.valueBox!, tableId: 1) + self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox!, tableId: 2, namespace: namespace) + self.indexTable = MessageHistoryIndexTable(valueBox: self.valueBox!, tableId: 1, globalMessageIdsTable: self.globalMessageIdsTable!) } override func tearDown() { @@ -81,23 +83,41 @@ class MessageHistoryIndexTableTests: XCTestCase { } func addHole(id: Int32) { - self.indexTable!.addHole(MessageId(peerId: peerId, namespace: namespace, id: id)) + var operations: [MessageHistoryIndexOperation] = [] + self.indexTable!.addHole(MessageId(peerId: peerId, namespace: namespace, id: id), operations: &operations) } func addMessage(id: Int32, _ timestamp: Int32) { - self.indexTable!.addMessage(MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp)) + var operations: [MessageHistoryIndexOperation] = [] + self.indexTable!.addMessages([StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, authorId: peerId, text: "", attributes: [], media: [])], location: .Random, operations: &operations) + } + + func addMessagesUpperBlock(messages: [(Int32, Int32)]) { + var operations: [MessageHistoryIndexOperation] = [] + self.indexTable!.addMessages(messages.map { (id, timestamp) in + return StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, authorId: peerId, text: "", attributes: [], media: []) + }, location: .UpperHistoryBlock, operations: &operations) } func fillHole(id: Int32, _ fillType: HoleFillType, _ messages: [(Int32, Int32)]) { - self.indexTable!.fillHole(MessageId(peerId: peerId, namespace: namespace, id: id), fillType: fillType, indices: messages.map({MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: $0.0), timestamp: $0.1)})) + 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, messages: messages.map({ + return StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: $0.0), timestamp: $0.1, authorId: peerId, text: "", attributes: [], media: []) + }), operations: &operations) } func removeMessage(id: Int32) { - self.indexTable!.removeMessage(MessageId(peerId: peerId, namespace: namespace, id: id)) + var operations: [MessageHistoryIndexOperation] = [] + self.indexTable!.removeMessage(MessageId(peerId: peerId, namespace: namespace, id: id), operations: &operations) } private func expect(items: [Item]) { - let actualItems = self.indexTable!.debugList(peerId, namespace: namespace).map { return Item($0) } + let actualList = self.indexTable!.debugList(peerId, namespace: namespace) + let actualItems = actualList.map { return Item($0) } if items != actualItems { XCTFail("Expected\n\(items)\nGot\n\(actualItems)") } @@ -118,6 +138,14 @@ class MessageHistoryIndexTableTests: XCTestCase { expect([.Message(90, 90), .Message(100, 100), .Message(110, 110)]) } + func testAddMessageIgnoreOverwrite() { + addMessage(100, 100) + expect([.Message(100, 100)]) + + addMessage(100, 110) + expect([.Message(100, 100)]) + } + func testAddHoleToEmpty() { addHole(100) expect([.Hole(1, Int32.max, Int32.max)]) @@ -424,4 +452,50 @@ class MessageHistoryIndexTableTests: XCTestCase { expect([.Message(100, 100), .Message(300, 300)]) } + + func testAddMessageCutInitialHole() { + addHole(1) + expect([.Hole(1, Int32.max, Int32.max)]) + addMessagesUpperBlock([(100, 100)]) + expect([.Hole(1, 99, 100), .Message(100, 100)]) + } + + func testAddMessageRemoveInitialHole() { + addHole(1) + expect([.Hole(1, Int32.max, Int32.max)]) + addMessagesUpperBlock([(1, 100)]) + expect([.Message(1, 100)]) + } + + func testAddMessageCutHoleAfterMessage1() { + addMessage(10, 10) + addHole(11) + expect([.Message(10, 10), .Hole(11, Int32.max, Int32.max)]) + addMessagesUpperBlock([(100, 100)]) + expect([.Message(10, 10), .Hole(11, 99, 100), .Message(100, 100)]) + } + + func testAddMessageCutHoleAfterMessage2() { + addMessage(10, 10) + addHole(11) + expect([.Message(10, 10), .Hole(11, Int32.max, Int32.max)]) + addMessagesUpperBlock([(12, 12)]) + expect([.Message(10, 10), .Hole(11, 11, 12), .Message(12, 12)]) + } + + func testAddMessageRemoveHoleAfterMessage() { + addMessage(10, 10) + addHole(11) + expect([.Message(10, 10), .Hole(11, Int32.max, Int32.max)]) + addMessagesUpperBlock([(11, 11)]) + expect([.Message(10, 10), .Message(11, 11)]) + } + + func testAddMessageRemoveHoleIgnoreOverwriteMessage() { + addMessage(10, 10) + addHole(11) + expect([.Message(10, 10), .Hole(11, Int32.max, Int32.max)]) + addMessagesUpperBlock([(10, 11)]) + expect([.Message(10, 10)]) + } } diff --git a/PostboxTests/MessageHistoryTableTests.swift b/PostboxTests/MessageHistoryTableTests.swift index 925f1acb8c..a9f7e3d6f9 100644 --- a/PostboxTests/MessageHistoryTableTests.swift +++ b/PostboxTests/MessageHistoryTableTests.swift @@ -8,21 +8,58 @@ import Postbox private let peerId = PeerId(namespace: 1, id: 1) private let namespace: Int32 = 1 +private let authorPeerId = PeerId(namespace: 1, id: 6) +private let peer = TestPeer(id: 6, data: "abc") -private func ==(lhs: (Int32, Int32, String, [Media]), rhs: (Int32, Int32, String, [Media])) -> Bool { - if lhs.3.count != rhs.3.count { +private func ==(lhs: [Media], rhs: [Media]) -> Bool { + if lhs.count != rhs.count { return false } - for i in 0 ..< lhs.3.count { - if !lhs.3[i].isEqual(rhs.3[i]) { + + for i in 0 ..< lhs.count { + if !lhs[i].isEqual(rhs[i]) { return false } } - return lhs.0 == rhs.0 && lhs.1 == rhs.1 && lhs.2 == rhs.2 + return true +} + +private enum Entry: Equatable, CustomStringConvertible { + case Message(Int32, Int32, String, [Media]) + case Hole(Int32, Int32, Int32) + + var description: String { + switch self { + case let .Message(id, timestamp, text, media): + return "Message(\(id), \(timestamp), \(text), \(media))" + 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): + switch rhs { + case let .Message(rhsId, rhsTimestamp, rhsText, rhsMedia): + return lhsId == rhsId && lhsTimestamp == rhsTimestamp && lhsText == rhsText && lhsMedia == rhsMedia + 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 class TestEmbeddedMedia: Media, CustomStringConvertible { var id: MediaId? { return nil } + var peerIds: [PeerId] = [] let data: String init(data: String) { @@ -51,6 +88,7 @@ private class TestEmbeddedMedia: Media, CustomStringConvertible { private class TestExternalMedia: Media { let id: MediaId? + var peerIds: [PeerId] = [] let data: String init(id: Int64, data: String) { @@ -81,6 +119,38 @@ private class TestExternalMedia: Media { } } +private class TestPeer: Peer { + let id: PeerId + let data: String + + init(id: Int32, data: String) { + self.id = PeerId(namespace: namespace, id: id) + self.data = data + } + + required init(decoder: Decoder) { + self.id = PeerId(namespace: decoder.decodeInt32ForKey("i.n"), id: decoder.decodeInt32ForKey("i.i")) + self.data = decoder.decodeStringForKey("s") + } + + func encode(encoder: Encoder) { + encoder.encodeInt32(self.id.namespace, forKey: "i.n") + encoder.encodeInt32(self.id.id, forKey: "i.i") + encoder.encodeString(self.data, forKey: "s") + } + + func isEqual(other: Peer) -> Bool { + if let other = other as? TestPeer { + return self.id == other.id && self.data == other.data + } + return false + } + + var description: String { + return "TestPeer(\(self.id.id), \(self.data))" + } +} + private enum MediaEntry: Equatable { case Direct(Media, Int) case MessageReference(Int32) @@ -118,6 +188,8 @@ class MessageHistoryTableTests: XCTestCase { var valueBox: ValueBox? var path: String? + var peerTable: PeerTable? + var globalMessageIdsTable: GlobalMessageIdsTable? var indexTable: MessageHistoryIndexTable? var mediaTable: MessageMediaTable? var mediaCleanupTable: MediaCleanupTable? @@ -128,6 +200,7 @@ class MessageHistoryTableTests: XCTestCase { declareEncodable(TestEmbeddedMedia.self, f: {TestEmbeddedMedia(decoder: $0)}) declareEncodable(TestExternalMedia.self, f: {TestExternalMedia(decoder: $0)}) + declareEncodable(TestPeer.self, f: {TestPeer(decoder: $0)}) } override func setUp() { @@ -138,10 +211,13 @@ class MessageHistoryTableTests: XCTestCase { path = NSTemporaryDirectory().stringByAppendingString("\(randomId)") self.valueBox = SqliteValueBox(basePath: path!) - self.indexTable = MessageHistoryIndexTable(valueBox: self.valueBox!, tableId: 1) + self.globalMessageIdsTable = GlobalMessageIdsTable(valueBox: self.valueBox!, tableId: 5, namespace: namespace) + self.indexTable = MessageHistoryIndexTable(valueBox: self.valueBox!, tableId: 1, globalMessageIdsTable: self.globalMessageIdsTable!) self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox!, tableId: 3) self.mediaTable = MessageMediaTable(valueBox: self.valueBox!, tableId: 2, mediaCleanupTable: self.mediaCleanupTable!) self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!) + self.peerTable = PeerTable(valueBox: self.valueBox!, tableId: 6) + self.peerTable!.set(peer) } override func tearDown() { @@ -151,36 +227,53 @@ class MessageHistoryTableTests: XCTestCase { self.indexTable = nil self.mediaTable = nil self.mediaCleanupTable = nil + self.peerTable = nil self.valueBox = nil let _ = try? NSFileManager.defaultManager().removeItemAtPath(path!) self.path = nil } - private func addMessage(id: Int32, _ timestamp: Int32, _ text: String, _ media: [Media] = []) { - self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, text: text, attributes: [], media: media)]) + private func addMessage(id: Int32, _ timestamp: Int32, _ text: String = "", _ media: [Media] = []) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: id), timestamp: timestamp, authorId: authorPeerId, text: text, attributes: [], media: media)], location: .Random, operationsByPeerId: &operationsByPeerId) + //print("\(operationsByPeerId[peerId]!)") + } + + private func addHole(id: Int32) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.historyTable!.addHoles([MessageId(peerId: peerId, namespace: namespace, id: id)], operationsByPeerId: &operationsByPeerId) } private func removeMessages(ids: [Int32]) { - self.historyTable!.removeMessages(ids.map({ MessageId(peerId: peerId, namespace: namespace, id: $0) })) + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.historyTable!.removeMessages(ids.map({ MessageId(peerId: peerId, namespace: namespace, id: $0) }), operationsByPeerId: &operationsByPeerId) + //print("\(operationsByPeerId[peerId]!)") } - func expectMessages(messages: [(Int32, Int32, String, [Media])]) { - let actualMessages = self.historyTable!.debugList(peerId).map({ ($0.id.id, $0.timestamp, $0.text, $0.media) }) - var equal = true - if messages.count != actualMessages.count { - equal = false - } else { - for i in 0 ..< messages.count { - if !(messages[i] == actualMessages[i]) { - equal = false - break - } + private func fillHole(id: Int32, _ fillType: HoleFillType, _ messages: [(Int32, Int32, String, [Media])]) { + var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] + self.historyTable!.fillHole(MessageId(peerId: peerId, namespace: namespace, id: id), fillType: fillType, messages: messages.map({ StoreMessage(id: MessageId(peerId: peerId, namespace: namespace, id: $0.0), timestamp: $0.1, authorId: authorPeerId, text: $0.2, attributes: [], media: $0.3) }), operationsByPeerId: &operationsByPeerId) + } + + private func expectEntries(entries: [Entry]) { + let actualEntries = self.historyTable!.debugList(peerId, peerTable: self.peerTable!).map({ entry -> Entry in + switch entry { + case let .RenderedMessage(message): + if let messagePeer = message.author { + if !peer.isEqual(messagePeer) { + XCTFail("Expected peer \(peer), actual: \(messagePeer)") + } + } else { + XCTFail("Expected peer \(peer), actual: nil") + } + return .Message(message.id.id, message.timestamp, message.text, message.media) + case let .Hole(hole): + return .Hole(hole.min, hole.maxIndex.id.id, hole.maxIndex.timestamp) } - } - - if !equal { - XCTFail("Expected\n\(messages)\nActual\n\(actualMessages)") + }) + if actualEntries != entries { + XCTFail("Expected\n\(entries)\nActual\n\(actualEntries)") } } @@ -214,20 +307,20 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "t100") addMessage(200, 200, "t200") - expectMessages([(100, 100, "t100", []), (200, 200, "t200", [])]) + expectEntries([.Message(100, 100, "t100", []), .Message(200, 200, "t200", [])]) } func testInsertMessageIgnoreOverwrite() { addMessage(100, 100, "t100") addMessage(100, 200, "t200") - expectMessages([(100, 100, "t100", [])]) + expectEntries([.Message(100, 100, "t100", [])]) } func testInsertMessageWithEmbeddedMedia() { addMessage(100, 100, "t100", [TestEmbeddedMedia(data: "abc1")]) - expectMessages([(100, 100, "t100", [TestEmbeddedMedia(data: "abc1")])]) + expectEntries([.Message(100, 100, "t100", [TestEmbeddedMedia(data: "abc1")])]) expectMedia([]) } @@ -235,7 +328,7 @@ class MessageHistoryTableTests: XCTestCase { let media = TestExternalMedia(id: 10, data: "abc1") addMessage(100, 100, "t100", [media]) - expectMessages([(100, 100, "t100", [media])]) + expectEntries([.Message(100, 100, "t100", [media])]) expectMedia([.MessageReference(100)]) } @@ -244,7 +337,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "t100", [media]) addMessage(200, 200, "t200", [media]) - expectMessages([(100, 100, "t100", [media]), (200, 200, "t200", [media])]) + expectEntries([.Message(100, 100, "t100", [media]), .Message(200, 200, "t200", [media])]) expectMedia([.Direct(media, 2)]) } @@ -254,7 +347,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "t100", [media]) addMessage(200, 200, "t200", [media1]) - expectMessages([(100, 100, "t100", [media]), (200, 200, "t200", [media])]) + expectEntries([.Message(100, 100, "t100", [media]), .Message(200, 200, "t200", [media])]) expectMedia([.Direct(media, 2)]) } @@ -263,7 +356,7 @@ class MessageHistoryTableTests: XCTestCase { removeMessages([100]) - expectMessages([]) + expectEntries([]) expectMedia([]) } @@ -272,7 +365,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "t100", [media]) self.removeMessages([100]) - expectMessages([]) + expectEntries([]) expectMedia([]) expectCleanupMedia([media]) } @@ -282,7 +375,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "t100", [media]) removeMessages([100]) - expectMessages([]) + expectEntries([]) expectMedia([]) expectCleanupMedia([media]) } @@ -293,14 +386,321 @@ class MessageHistoryTableTests: XCTestCase { addMessage(200, 200, "t200", [media]) removeMessages([100]) - expectMessages([(200, 200, "t200", [media])]) + expectEntries([.Message(200, 200, "t200", [media])]) expectMedia([.Direct(media, 1)]) expectCleanupMedia([]) removeMessages([200]) - expectMessages([]) + expectEntries([]) expectMedia([]) expectCleanupMedia([media]) } + + func testAddHoleToEmpty() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + } + + func testAddHoleToFullHole() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + addHole(110) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + } + + func testAddMessageToFullHole() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + addMessage(90, 90, "m90") + expectEntries([.Hole(1, 89, 90), .Message(90, 90, "m90", []), .Hole(91, Int32.max, Int32.max)]) + } + + func testAddMessageDividingUpperHole() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + addMessage(90, 90, "m90") + expectEntries([.Hole(1, 89, 90), .Message(90, 90, "m90", []), .Hole(91, Int32.max, Int32.max)]) + addMessage(100, 100, "m100") + expectEntries([.Hole(1, 89, 90), .Message(90, 90, "m90", []), .Hole(91, 99, 100), .Message(100, 100, "m100", []), .Hole(101, Int32.max, Int32.max)]) + } + + func testAddMessageDividingLowerHole() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + addMessage(90, 90, "m90") + expectEntries([.Hole(1, 89, 90), .Message(90, 90, "m90", []), .Hole(91, Int32.max, Int32.max)]) + addMessage(80, 80, "m80") + expectEntries([.Hole(1, 79, 80), .Message(80, 80, "m80", []), .Hole(81, 89, 90), .Message(90, 90, "m90", []), .Hole(91, Int32.max, Int32.max)]) + } + + func testAddMessageOffsettingUpperHole() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + + addMessage(90, 90, "m90") + expectEntries([.Hole(1, 89, 90), .Message(90, 90, "m90", []), .Hole(91, Int32.max, Int32.max)]) + addMessage(91, 91, "m91") + expectEntries([.Hole(1, 89, 90), .Message(90, 90, "m90", []), .Message(91, 91, "m91", []), .Hole(92, Int32.max, Int32.max)]) + } + + func testAddMessageOffsettingLowerHole() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + + addMessage(90, 90, "m90") + expectEntries([.Hole(1, 89, 90), .Message(90, 90, "m90", []), .Hole(91, Int32.max, Int32.max)]) + addMessage(89, 89, "m89") + expectEntries([.Hole(1, 88, 89), .Message(89, 89, "m89", []), .Message(90, 90, "m90", []), .Hole(91, Int32.max, Int32.max)]) + } + + func testAddMessageOffsettingLeftmostHole() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + + addMessage(1, 1, "m1") + + expectEntries([.Message(1, 1, "m1", []), .Hole(2, Int32.max, Int32.max)]) + } + + func testAddMessageRemovingLefmostHole() { + addHole(100) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + + addMessage(2, 2, "m2") + expectEntries([.Hole(1, 1, 2), .Message(2, 2, "m2", []), .Hole(3, Int32.max, Int32.max)]) + + addMessage(1, 1, "m1") + expectEntries([.Message(1, 1, "m1", []), .Message(2, 2, "m2", []), .Hole(3, Int32.max, Int32.max)]) + } + + func testAddHoleLowerThanMessage() { + addMessage(100, 100, "m100") + addHole(1) + + expectEntries([.Hole(1, 99, 100), .Message(100, 100, "m100", [])]) + } + + func testAddHoleHigherThanMessage() { + addMessage(100, 100, "m100") + addHole(200) + + expectEntries([.Message(100, 100, "m100", []), .Hole(101, Int32.max, Int32.max)]) + } + + func testIgnoreHigherHole() { + addHole(200) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + addHole(400) + expectEntries([.Hole(1, Int32.max, Int32.max)]) + } + + func testIgnoreHigherHoleAfterMessage() { + addMessage(100, 100, "m100") + addHole(200) + expectEntries([.Message(100, 100, "m100", []), .Hole(101, Int32.max, Int32.max)]) + addHole(400) + expectEntries([.Message(100, 100, "m100", []), .Hole(101, Int32.max, Int32.max)]) + } + + func testAddHoleBetweenMessages() { + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + addHole(150) + + expectEntries([.Message(100, 100, "m100", []), .Hole(101, 199, 200), .Message(200, 200, "m200", [])]) + } + + func testFillHoleEmpty() { + fillHole(1, .Complete, []) + expectEntries([]) + } + + func testFillHoleComplete() { + addHole(100) + + fillHole(1, .Complete, [(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", [])]) + 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", [])]) + expectEntries([.Message(1, 1, "m1", []), .Message(200, 200, "m200", [])]) + } + + func testFillHoleLowerToUpperToBounds() { + addHole(100) + + fillHole(1, .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", [])]) + expectEntries([.Message(100, 100, "m100", []), .Message(200, 200, "m200", []), .Hole(201, Int32.max, Int32.max)]) + } + + func testFillHoleBetweenMessagesUpperToLower() { + addHole(1) + + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + + fillHole(199, .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)]) + } + + func testFillHoleBetweenMessagesLowerToUpper() { + addHole(1) + + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + + fillHole(199, .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)]) + } + + func testFillHoleBetweenMessagesComplete() { + addHole(1) + + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + + fillHole(199, .Complete, [(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)]) + } + + func testFillHoleBetweenMessagesWithMessage() { + addMessage(200, 200, "m200") + addMessage(202, 202, "m202") + addHole(201) + addMessage(201, 201, "m201") + + expectEntries([.Message(200, 200, "m200", []), .Message(201, 201, "m201", []), .Message(202, 202, "m202", [])]) + } + + func testFillHoleWithNoMessagesComplete() { + addMessage(100, 100, "m100") + addHole(1) + + fillHole(99, .Complete, []) + + expectEntries([.Message(100, 100, "m100", [])]) + } + + func testFillHoleIgnoreOverMessage() { + addMessage(100, 100, "m100") + addMessage(101, 101, "m101") + + fillHole(100, .Complete, [(90, 90, "m90", [])]) + + expectEntries([.Message(90, 90, "m90", []), .Message(100, 100, "m100", []), .Message(101, 101, "m101", [])]) + } + + func testFillHoleWithOverflow() { + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + addHole(150) + + fillHole(199, .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", [])]) + } + + func testIgnoreHoleOverMessageBetweenMessages() { + addMessage(199, 199, "m199") + addMessage(200, 200, "m200") + addHole(200) + + expectEntries([.Message(199, 199, "m199", []), .Message(200, 200, "m200", [])]) + } + + func testMergeHoleAfterDeletingMessage() { + addMessage(100, 100, "m100") + addHole(1) + addHole(200) + + expectEntries([.Hole(1, 99, 100), .Message(100, 100, "m100", []), .Hole(101, Int32.max, Int32.max)]) + + removeMessages([100]) + + expectEntries([.Hole(1, Int32.max, Int32.max)]) + } + + func testMergeHoleLowerAfterDeletingMessage() { + addMessage(100, 100, "m100") + addHole(1) + addMessage(200, 200, "m200") + + removeMessages([100]) + + expectEntries([.Hole(1, 199, 200), .Message(200, 200, "m200", [])]) + } + + func testMergeHoleUpperAfterDeletingMessage() { + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + addHole(300) + + removeMessages([200]) + + expectEntries([.Message(100, 100, "m100", []), .Hole(101, Int32.max, Int32.max)]) + } + + func testExtendLowerHoleAfterDeletingMessage() { + addMessage(100, 100, "m100") + addHole(100) + + removeMessages([100]) + + expectEntries([.Hole(1, Int32.max, Int32.max)]) + } + + func testExtendUpperHoleAfterDeletingMessage() { + addMessage(100, 100, "m100") + addHole(101) + + removeMessages([100]) + + expectEntries([.Hole(1, Int32.max, Int32.max)]) + } + + func testDeleteMessageBelowMessage() { + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + removeMessages([100]) + + expectEntries([.Message(200, 200, "m200", [])]) + } + + func testDeleteMessageAboveMessage() { + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + removeMessages([200]) + + expectEntries([.Message(100, 100, "m100", [])]) + } + + func testDeleteMessageBetweenMessages() { + addMessage(100, 100, "m100") + addMessage(200, 200, "m200") + addMessage(300, 300, "m300") + removeMessages([200]) + + expectEntries([.Message(100, 100, "m100", []), .Message(300, 300, "m300", [])]) + } } \ No newline at end of file diff --git a/PostboxTests/MessageHistoryViewTests.swift b/PostboxTests/MessageHistoryViewTests.swift new file mode 100644 index 0000000000..9a30829567 --- /dev/null +++ b/PostboxTests/MessageHistoryViewTests.swift @@ -0,0 +1,3 @@ +import Foundation + +