mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Add 'submodules/Postbox/' from commit '534443c710'
git-subtree-dir: submodules/Postbox git-subtree-mainline:373769682egit-subtree-split:534443c710
This commit is contained in:
commit
4459dc5b47
218 changed files with 281352 additions and 0 deletions
25
submodules/Postbox/.gitignore
vendored
Normal file
25
submodules/Postbox/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
fastlane/README.md
|
||||
fastlane/report.xml
|
||||
fastlane/test_output/*
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
.DS_Store
|
||||
*.dSYM
|
||||
*.dSYM.zip
|
||||
*.ipa
|
||||
*/xcuserdata/*
|
||||
Postbox.xcodeproj/*
|
||||
3
submodules/Postbox/.gitmodules
vendored
Normal file
3
submodules/Postbox/.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "submodules/lmdb"]
|
||||
path = submodules/lmdb
|
||||
url = https://github.com/LMDB/lmdb.git
|
||||
44
submodules/Postbox/BUCK
Normal file
44
submodules/Postbox/BUCK
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
load('//tools:buck_utils.bzl', 'config_with_updated_linker_flags', 'configs_with_config', 'combined_config')
|
||||
load('//tools:buck_defs.bzl', 'SHARED_CONFIGS', 'EXTENSION_LIB_SPECIFIC_CONFIG')
|
||||
|
||||
apple_library(
|
||||
name = 'sqlcipher',
|
||||
srcs = glob([
|
||||
'Postbox/**/*.m',
|
||||
'Postbox/**/*.c',
|
||||
]),
|
||||
headers = glob([
|
||||
'Postbox/**/*.h',
|
||||
]),
|
||||
header_namespace = 'sqlcipher',
|
||||
exported_headers = glob([
|
||||
'Postbox/**/*.h',
|
||||
], exclude = ['Postbox/Postbox.h']),
|
||||
compiler_flags = [
|
||||
'-DNDEBUG',
|
||||
'-DSQLCIPHER_CRYPTO_CC=1',
|
||||
'-DSQLITE_ENABLE_FTS5',
|
||||
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
||||
'-DSQLITE_MAX_MMAP_SIZE=0',
|
||||
'-DSQLITE_HAS_CODEC',
|
||||
],
|
||||
modular = True,
|
||||
visibility = ['//submodules/Postbox:Postbox'],
|
||||
)
|
||||
|
||||
apple_library(
|
||||
name = 'Postbox',
|
||||
srcs = glob([
|
||||
'Postbox/**/*.swift'
|
||||
]),
|
||||
configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])),
|
||||
swift_compiler_flags = [
|
||||
'-suppress-warnings',
|
||||
'-application-extension',
|
||||
],
|
||||
visibility = ['PUBLIC'],
|
||||
deps = [
|
||||
':sqlcipher',
|
||||
'//submodules/SSignalKit:SwiftSignalKit'
|
||||
],
|
||||
)
|
||||
30
submodules/Postbox/Postbox/AccessChallengeDataView.swift
Normal file
30
submodules/Postbox/Postbox/AccessChallengeDataView.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableAccessChallengeDataView {
|
||||
var data: PostboxAccessChallengeData
|
||||
|
||||
init(data: PostboxAccessChallengeData) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
func replay(updatedData: PostboxAccessChallengeData?) -> Bool {
|
||||
var updated = false
|
||||
|
||||
if let data = updatedData {
|
||||
if self.data != data {
|
||||
self.data = data
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
public final class AccessChallengeDataView: PostboxView {
|
||||
public let data: PostboxAccessChallengeData
|
||||
|
||||
init(_ view: MutableAccessChallengeDataView) {
|
||||
self.data = view.data
|
||||
}
|
||||
}
|
||||
553
submodules/Postbox/Postbox/AccountManager.swift
Normal file
553
submodules/Postbox/Postbox/AccountManager.swift
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
import Foundation
|
||||
#if os(macOS)
|
||||
import SwiftSignalKitMac
|
||||
#else
|
||||
import SwiftSignalKit
|
||||
#endif
|
||||
|
||||
public struct AccountManagerModifier {
|
||||
public let getRecords: () -> [AccountRecord]
|
||||
public let updateRecord: (AccountRecordId, (AccountRecord?) -> (AccountRecord?)) -> Void
|
||||
public let getCurrent: () -> (AccountRecordId, [AccountRecordAttribute])?
|
||||
public let setCurrentId: (AccountRecordId) -> Void
|
||||
public let getCurrentAuth: () -> AuthAccountRecord?
|
||||
public let createAuth: ([AccountRecordAttribute]) -> AuthAccountRecord?
|
||||
public let removeAuth: () -> Void
|
||||
public let createRecord: ([AccountRecordAttribute]) -> AccountRecordId
|
||||
public let getSharedData: (ValueBoxKey) -> PreferencesEntry?
|
||||
public let updateSharedData: (ValueBoxKey, (PreferencesEntry?) -> PreferencesEntry?) -> Void
|
||||
public let getAccessChallengeData: () -> PostboxAccessChallengeData
|
||||
public let setAccessChallengeData: (PostboxAccessChallengeData) -> Void
|
||||
public let getVersion: () -> Int32?
|
||||
public let setVersion: (Int32) -> Void
|
||||
public let getNotice: (NoticeEntryKey) -> NoticeEntry?
|
||||
public let setNotice: (NoticeEntryKey, NoticeEntry?) -> Void
|
||||
public let clearNotices: () -> Void
|
||||
}
|
||||
|
||||
final class AccountManagerImpl {
|
||||
private let queue: Queue
|
||||
private let basePath: String
|
||||
private let atomicStatePath: String
|
||||
private let temporarySessionId: Int64
|
||||
private let guardValueBox: ValueBox?
|
||||
private let valueBox: ValueBox
|
||||
|
||||
private var tables: [Table] = []
|
||||
|
||||
private var currentAtomicState: AccountManagerAtomicState
|
||||
private var currentAtomicStateUpdated = false
|
||||
|
||||
private let legacyMetadataTable: AccountManagerMetadataTable
|
||||
private let legacyRecordTable: AccountManagerRecordTable
|
||||
|
||||
let sharedDataTable: AccountManagerSharedDataTable
|
||||
let noticeTable: NoticeTable
|
||||
|
||||
private var currentRecordOperations: [AccountManagerRecordOperation] = []
|
||||
private var currentMetadataOperations: [AccountManagerMetadataOperation] = []
|
||||
|
||||
private var currentUpdatedSharedDataKeys = Set<ValueBoxKey>()
|
||||
private var currentUpdatedNoticeEntryKeys = Set<NoticeEntryKey>()
|
||||
private var currentUpdatedAccessChallengeData: PostboxAccessChallengeData?
|
||||
|
||||
private var recordsViews = Bag<(MutableAccountRecordsView, ValuePipe<AccountRecordsView>)>()
|
||||
|
||||
private var sharedDataViews = Bag<(MutableAccountSharedDataView, ValuePipe<AccountSharedDataView>)>()
|
||||
private var noticeEntryViews = Bag<(MutableNoticeEntryView, ValuePipe<NoticeEntryView>)>()
|
||||
private var accessChallengeDataViews = Bag<(MutableAccessChallengeDataView, ValuePipe<AccessChallengeDataView>)>()
|
||||
|
||||
fileprivate init(queue: Queue, basePath: String, temporarySessionId: Int64) {
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
self.queue = queue
|
||||
self.basePath = basePath
|
||||
self.atomicStatePath = "\(basePath)/atomic-state"
|
||||
self.temporarySessionId = temporarySessionId
|
||||
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
|
||||
self.guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, encryptionParameters: nil, upgradeProgress: { _ in })
|
||||
self.valueBox = SqliteValueBox(basePath: basePath + "/db", queue: queue, encryptionParameters: nil, upgradeProgress: { _ in })
|
||||
|
||||
self.legacyMetadataTable = AccountManagerMetadataTable(valueBox: self.valueBox, table: AccountManagerMetadataTable.tableSpec(0))
|
||||
self.legacyRecordTable = AccountManagerRecordTable(valueBox: self.valueBox, table: AccountManagerRecordTable.tableSpec(1))
|
||||
self.sharedDataTable = AccountManagerSharedDataTable(valueBox: self.valueBox, table: AccountManagerSharedDataTable.tableSpec(2))
|
||||
self.noticeTable = NoticeTable(valueBox: self.valueBox, table: NoticeTable.tableSpec(3))
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: self.atomicStatePath))
|
||||
do {
|
||||
|
||||
let atomicState = try JSONDecoder().decode(AccountManagerAtomicState.self, from: data)
|
||||
self.currentAtomicState = atomicState
|
||||
} catch let e {
|
||||
postboxLog("decode atomic state error: \(e)")
|
||||
let _ = try? FileManager.default.removeItem(atPath: self.atomicStatePath)
|
||||
preconditionFailure()
|
||||
}
|
||||
} catch let e {
|
||||
postboxLog("load atomic state error: \(e)")
|
||||
var legacyRecordDict: [AccountRecordId: AccountRecord] = [:]
|
||||
for record in self.legacyRecordTable.getRecords() {
|
||||
legacyRecordDict[record.id] = record
|
||||
}
|
||||
self.currentAtomicState = AccountManagerAtomicState(records: legacyRecordDict, currentRecordId: self.legacyMetadataTable.getCurrentAccountId(), currentAuthRecord: self.legacyMetadataTable.getCurrentAuthAccount())
|
||||
self.syncAtomicStateToFile()
|
||||
}
|
||||
|
||||
postboxLog("AccountManager: currentAccountId = \(String(describing: currentAtomicState.currentRecordId))")
|
||||
|
||||
self.tables.append(self.legacyMetadataTable)
|
||||
self.tables.append(self.legacyRecordTable)
|
||||
self.tables.append(self.sharedDataTable)
|
||||
self.tables.append(self.noticeTable)
|
||||
|
||||
print("AccountManager initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
fileprivate func transaction<T>(ignoreDisabled: Bool, _ f: @escaping (AccountManagerModifier) -> T) -> Signal<T, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.queue.justDispatch {
|
||||
self.valueBox.begin()
|
||||
|
||||
let transaction = AccountManagerModifier(getRecords: {
|
||||
return self.currentAtomicState.records.map { $0.1 }
|
||||
}, updateRecord: { id, update in
|
||||
let current = self.currentAtomicState.records[id]
|
||||
let updated = update(current)
|
||||
if updated != current {
|
||||
if let updated = updated {
|
||||
self.currentAtomicState.records[id] = updated
|
||||
} else {
|
||||
self.currentAtomicState.records.removeValue(forKey: id)
|
||||
}
|
||||
self.currentAtomicStateUpdated = true
|
||||
self.currentRecordOperations.append(.set(id: id, record: updated))
|
||||
}
|
||||
}, getCurrent: {
|
||||
if let id = self.currentAtomicState.currentRecordId, let record = self.currentAtomicState.records[id] {
|
||||
return (record.id, record.attributes)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, setCurrentId: { id in
|
||||
self.currentAtomicState.currentRecordId = id
|
||||
self.currentMetadataOperations.append(.updateCurrentAccountId(id))
|
||||
self.currentAtomicStateUpdated = true
|
||||
}, getCurrentAuth: {
|
||||
if let record = self.currentAtomicState.currentAuthRecord {
|
||||
return record
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, createAuth: { attributes in
|
||||
let record = AuthAccountRecord(id: generateAccountRecordId(), attributes: attributes)
|
||||
self.currentAtomicState.currentAuthRecord = record
|
||||
self.currentAtomicStateUpdated = true
|
||||
self.currentMetadataOperations.append(.updateCurrentAuthAccountRecord(record))
|
||||
return record
|
||||
}, removeAuth: {
|
||||
self.currentAtomicState.currentAuthRecord = nil
|
||||
self.currentMetadataOperations.append(.updateCurrentAuthAccountRecord(nil))
|
||||
self.currentAtomicStateUpdated = true
|
||||
}, createRecord: { attributes in
|
||||
let id = generateAccountRecordId()
|
||||
let record = AccountRecord(id: id, attributes: attributes, temporarySessionId: nil)
|
||||
self.currentAtomicState.records[id] = record
|
||||
self.currentRecordOperations.append(.set(id: id, record: record))
|
||||
self.currentAtomicStateUpdated = true
|
||||
return id
|
||||
}, getSharedData: { key in
|
||||
return self.sharedDataTable.get(key: key)
|
||||
}, updateSharedData: { key, f in
|
||||
let updated = f(self.sharedDataTable.get(key: key))
|
||||
self.sharedDataTable.set(key: key, value: updated, updatedKeys: &self.currentUpdatedSharedDataKeys)
|
||||
}, getAccessChallengeData: {
|
||||
return self.legacyMetadataTable.getAccessChallengeData()
|
||||
}, setAccessChallengeData: { data in
|
||||
self.currentUpdatedAccessChallengeData = data
|
||||
self.legacyMetadataTable.setAccessChallengeData(data)
|
||||
}, getVersion: {
|
||||
return self.legacyMetadataTable.getVersion()
|
||||
}, setVersion: { version in
|
||||
self.legacyMetadataTable.setVersion(version)
|
||||
}, getNotice: { key in
|
||||
self.noticeTable.get(key: key)
|
||||
}, setNotice: { key, value in
|
||||
self.noticeTable.set(key: key, value: value)
|
||||
self.currentUpdatedNoticeEntryKeys.insert(key)
|
||||
}, clearNotices: {
|
||||
self.noticeTable.clear()
|
||||
})
|
||||
|
||||
let result = f(transaction)
|
||||
|
||||
self.beforeCommit()
|
||||
|
||||
self.valueBox.commit()
|
||||
//self.valueBox.checkpoint()
|
||||
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private func syncAtomicStateToFile() {
|
||||
if let data = try? JSONEncoder().encode(self.currentAtomicState) {
|
||||
if let _ = try? data.write(to: URL(fileURLWithPath: self.atomicStatePath), options: [.atomic]) {
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private func beforeCommit() {
|
||||
if self.currentAtomicStateUpdated {
|
||||
self.syncAtomicStateToFile()
|
||||
}
|
||||
|
||||
if !self.currentRecordOperations.isEmpty || !self.currentMetadataOperations.isEmpty {
|
||||
for (view, pipe) in self.recordsViews.copyItems() {
|
||||
if view.replay(operations: self.currentRecordOperations, metadataOperations: self.currentMetadataOperations) {
|
||||
pipe.putNext(AccountRecordsView(view))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.currentUpdatedSharedDataKeys.isEmpty {
|
||||
for (view, pipe) in self.sharedDataViews.copyItems() {
|
||||
if view.replay(accountManagerImpl: self, updatedKeys: self.currentUpdatedSharedDataKeys) {
|
||||
pipe.putNext(AccountSharedDataView(view))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.currentUpdatedNoticeEntryKeys.isEmpty {
|
||||
for (view, pipe) in self.noticeEntryViews.copyItems() {
|
||||
if view.replay(accountManagerImpl: self, updatedKeys: self.currentUpdatedNoticeEntryKeys) {
|
||||
pipe.putNext(NoticeEntryView(view))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let data = self.currentUpdatedAccessChallengeData {
|
||||
for (view, pipe) in self.accessChallengeDataViews.copyItems() {
|
||||
if view.replay(updatedData: data) {
|
||||
pipe.putNext(AccessChallengeDataView(view))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.currentRecordOperations.removeAll()
|
||||
self.currentMetadataOperations.removeAll()
|
||||
self.currentUpdatedSharedDataKeys.removeAll()
|
||||
self.currentUpdatedNoticeEntryKeys.removeAll()
|
||||
self.currentUpdatedAccessChallengeData = nil
|
||||
self.currentAtomicStateUpdated = false
|
||||
|
||||
for table in self.tables {
|
||||
table.beforeCommit()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func accountRecords() -> Signal<AccountRecordsView, NoError> {
|
||||
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountRecordsView, NoError> in
|
||||
return self.accountRecordsInternal(transaction: transaction)
|
||||
})
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
fileprivate func sharedData(keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView, NoError> {
|
||||
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountSharedDataView, NoError> in
|
||||
return self.sharedDataInternal(transaction: transaction, keys: keys)
|
||||
})
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
fileprivate func noticeEntry(key: NoticeEntryKey) -> Signal<NoticeEntryView, NoError> {
|
||||
return self.transaction(ignoreDisabled: false, { transaction -> Signal<NoticeEntryView, NoError> in
|
||||
return self.noticeEntryInternal(transaction: transaction, key: key)
|
||||
})
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
fileprivate func accessChallengeData() -> Signal<AccessChallengeDataView, NoError> {
|
||||
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccessChallengeDataView, NoError> in
|
||||
return self.accessChallengeDataInternal(transaction: transaction)
|
||||
})
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
private func accountRecordsInternal(transaction: AccountManagerModifier) -> Signal<AccountRecordsView, NoError> {
|
||||
assert(self.queue.isCurrent())
|
||||
let mutableView = MutableAccountRecordsView(getRecords: {
|
||||
return self.currentAtomicState.records.map { $0.1 }
|
||||
}, currentId: self.currentAtomicState.currentRecordId, currentAuth: self.currentAtomicState.currentAuthRecord)
|
||||
let pipe = ValuePipe<AccountRecordsView>()
|
||||
let index = self.recordsViews.add((mutableView, pipe))
|
||||
|
||||
let queue = self.queue
|
||||
return (.single(AccountRecordsView(mutableView))
|
||||
|> then(pipe.signal()))
|
||||
|> `catch` { _ -> Signal<AccountRecordsView, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|> afterDisposed { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.recordsViews.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sharedDataInternal(transaction: AccountManagerModifier, keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView, NoError> {
|
||||
let mutableView = MutableAccountSharedDataView(accountManagerImpl: self, keys: keys)
|
||||
let pipe = ValuePipe<AccountSharedDataView>()
|
||||
let index = self.sharedDataViews.add((mutableView, pipe))
|
||||
|
||||
let queue = self.queue
|
||||
return (.single(AccountSharedDataView(mutableView))
|
||||
|> then(pipe.signal()))
|
||||
|> `catch` { _ -> Signal<AccountSharedDataView, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|> afterDisposed { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.sharedDataViews.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func noticeEntryInternal(transaction: AccountManagerModifier, key: NoticeEntryKey) -> Signal<NoticeEntryView, NoError> {
|
||||
let mutableView = MutableNoticeEntryView(accountManagerImpl: self, key: key)
|
||||
let pipe = ValuePipe<NoticeEntryView>()
|
||||
let index = self.noticeEntryViews.add((mutableView, pipe))
|
||||
|
||||
let queue = self.queue
|
||||
return (.single(NoticeEntryView(mutableView))
|
||||
|> then(pipe.signal()))
|
||||
|> `catch` { _ -> Signal<NoticeEntryView, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|> afterDisposed { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.noticeEntryViews.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func accessChallengeDataInternal(transaction: AccountManagerModifier) -> Signal<AccessChallengeDataView, NoError> {
|
||||
let mutableView = MutableAccessChallengeDataView(data: transaction.getAccessChallengeData())
|
||||
let pipe = ValuePipe<AccessChallengeDataView>()
|
||||
let index = self.accessChallengeDataViews.add((mutableView, pipe))
|
||||
|
||||
let queue = self.queue
|
||||
return (.single(AccessChallengeDataView(mutableView))
|
||||
|> then(pipe.signal()))
|
||||
|> `catch` { _ -> Signal<AccessChallengeDataView, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
|> afterDisposed { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.accessChallengeDataViews.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func currentAccountRecord(allocateIfNotExists: Bool) -> Signal<(AccountRecordId, [AccountRecordAttribute])?, NoError> {
|
||||
return self.transaction(ignoreDisabled: false, { transaction -> Signal<(AccountRecordId, [AccountRecordAttribute])?, NoError> in
|
||||
let current = transaction.getCurrent()
|
||||
if let _ = current {
|
||||
} else if allocateIfNotExists {
|
||||
let id = generateAccountRecordId()
|
||||
transaction.setCurrentId(id)
|
||||
transaction.updateRecord(id, { _ in
|
||||
return AccountRecord(id: id, attributes: [], temporarySessionId: nil)
|
||||
})
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
let signal = self.accountRecordsInternal(transaction: transaction)
|
||||
|> map { view -> (AccountRecordId, [AccountRecordAttribute])? in
|
||||
if let currentRecord = view.currentRecord {
|
||||
return (currentRecord.id, currentRecord.attributes)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return signal
|
||||
})
|
||||
|> switchToLatest
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||
if let lhs = lhs, let rhs = rhs {
|
||||
if lhs.0 != rhs.0 {
|
||||
return false
|
||||
}
|
||||
if lhs.1.count != rhs.1.count {
|
||||
return false
|
||||
}
|
||||
for i in 0 ..< lhs.1.count {
|
||||
if !lhs.1[i].isEqual(to: rhs.1[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else if (lhs != nil) != (rhs != nil) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func allocatedTemporaryAccountId() -> Signal<AccountRecordId, NoError> {
|
||||
let temporarySessionId = self.temporarySessionId
|
||||
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountRecordId, NoError> in
|
||||
|
||||
let id = generateAccountRecordId()
|
||||
transaction.updateRecord(id, { _ in
|
||||
return AccountRecord(id: id, attributes: [], temporarySessionId: temporarySessionId)
|
||||
})
|
||||
|
||||
return .single(id)
|
||||
})
|
||||
|> switchToLatest
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
||||
return lhs == rhs
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public final class AccountManager {
|
||||
public let basePath: String
|
||||
public let mediaBox: MediaBox
|
||||
private let queue = Queue()
|
||||
private let impl: QueueLocalObject<AccountManagerImpl>
|
||||
public let temporarySessionId: Int64
|
||||
|
||||
public init(basePath: String) {
|
||||
self.basePath = basePath
|
||||
var temporarySessionId: Int64 = 0
|
||||
arc4random_buf(&temporarySessionId, 8)
|
||||
self.temporarySessionId = temporarySessionId
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return AccountManagerImpl(queue: queue, basePath: basePath, temporarySessionId: temporarySessionId)
|
||||
})
|
||||
self.mediaBox = MediaBox(basePath: basePath + "/media")
|
||||
}
|
||||
|
||||
public func transaction<T>(ignoreDisabled: Bool = false, _ f: @escaping (AccountManagerModifier) -> T) -> Signal<T, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.transaction(ignoreDisabled: ignoreDisabled, f).start(next: { next in
|
||||
subscriber.putNext(next)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func accountRecords() -> Signal<AccountRecordsView, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.accountRecords().start(next: { next in
|
||||
subscriber.putNext(next)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func sharedData(keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.sharedData(keys: keys).start(next: { next in
|
||||
subscriber.putNext(next)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func noticeEntry(key: NoticeEntryKey) -> Signal<NoticeEntryView, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.noticeEntry(key: key).start(next: { next in
|
||||
subscriber.putNext(next)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func accessChallengeData() -> Signal<AccessChallengeDataView, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.accessChallengeData().start(next: { next in
|
||||
subscriber.putNext(next)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func currentAccountRecord(allocateIfNotExists: Bool) -> Signal<(AccountRecordId, [AccountRecordAttribute])?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.currentAccountRecord(allocateIfNotExists: allocateIfNotExists).start(next: { next in
|
||||
subscriber.putNext(next)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func allocatedTemporaryAccountId() -> Signal<AccountRecordId, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
disposable.set(impl.allocatedTemporaryAccountId().start(next: { next in
|
||||
subscriber.putNext(next)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
}
|
||||
47
submodules/Postbox/Postbox/AccountManagerAtomicState.swift
Normal file
47
submodules/Postbox/Postbox/AccountManagerAtomicState.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import Foundation
|
||||
|
||||
final class AccountManagerAtomicState: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case records
|
||||
case currentRecordId
|
||||
case currentAuthRecord
|
||||
}
|
||||
|
||||
var records: [AccountRecordId: AccountRecord]
|
||||
var currentRecordId: AccountRecordId?
|
||||
var currentAuthRecord: AuthAccountRecord?
|
||||
|
||||
init(records: [AccountRecordId: AccountRecord] = [:], currentRecordId: AccountRecordId? = nil, currentAuthRecord: AuthAccountRecord? = nil) {
|
||||
self.records = records
|
||||
self.currentRecordId = currentRecordId
|
||||
self.currentAuthRecord = currentAuthRecord
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
if let records = try? container.decode(Array<AccountRecord>.self, forKey: .records) {
|
||||
var recordDict: [AccountRecordId: AccountRecord] = [:]
|
||||
for record in records {
|
||||
recordDict[record.id] = record
|
||||
}
|
||||
self.records = recordDict
|
||||
} else {
|
||||
self.records = try container.decode(Dictionary<AccountRecordId, AccountRecord>.self, forKey: .records)
|
||||
}
|
||||
if let maybeIdString = try? container.decodeIfPresent(String.self, forKey: .currentRecordId), let idString = maybeIdString, let idValue = Int64(idString) {
|
||||
self.currentRecordId = AccountRecordId(rawValue: idValue)
|
||||
} else {
|
||||
self.currentRecordId = try container.decodeIfPresent(AccountRecordId.self, forKey: .currentRecordId)
|
||||
}
|
||||
self.currentAuthRecord = try container.decodeIfPresent(AuthAccountRecord.self, forKey: .currentAuthRecord)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
let recordsArray: Array<AccountRecord> = Array(self.records.values)
|
||||
try container.encode(recordsArray, forKey: .records)
|
||||
let currentRecordIdString: String? = self.currentRecordId.flatMap({ "\($0.rawValue)" })
|
||||
try container.encodeIfPresent(currentRecordIdString, forKey: .currentRecordId)
|
||||
try container.encodeIfPresent(self.currentAuthRecord, forKey: .currentAuthRecord)
|
||||
}
|
||||
}
|
||||
256
submodules/Postbox/Postbox/AccountManagerMetadataTable.swift
Normal file
256
submodules/Postbox/Postbox/AccountManagerMetadataTable.swift
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
import Foundation
|
||||
|
||||
public struct AccessChallengeAttempts: PostboxCoding, Equatable {
|
||||
public let count: Int32
|
||||
public let timestamp: Int32
|
||||
|
||||
public init(count: Int32, timestamp: Int32) {
|
||||
self.count = count
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.count = decoder.decodeInt32ForKey("c", orElse: 0)
|
||||
self.timestamp = decoder.decodeInt32ForKey("t", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.count, forKey: "c")
|
||||
encoder.encodeInt32(self.timestamp, forKey: "t")
|
||||
}
|
||||
}
|
||||
|
||||
public enum PostboxAccessChallengeData: PostboxCoding, Equatable {
|
||||
case none
|
||||
case numericalPassword(value: String, timeout: Int32?, attempts: AccessChallengeAttempts?)
|
||||
case plaintextPassword(value: String, timeout: Int32?, attempts: AccessChallengeAttempts?)
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
switch decoder.decodeInt32ForKey("r", orElse: 0) {
|
||||
case 0:
|
||||
self = .none
|
||||
case 1:
|
||||
self = .numericalPassword(value: decoder.decodeStringForKey("t", orElse: ""), timeout: decoder.decodeOptionalInt32ForKey("a"), attempts: decoder.decodeObjectForKey("att", decoder: { AccessChallengeAttempts(decoder: $0) }) as? AccessChallengeAttempts)
|
||||
case 2:
|
||||
self = .plaintextPassword(value: decoder.decodeStringForKey("t", orElse: ""), timeout: decoder.decodeOptionalInt32ForKey("a"), attempts: decoder.decodeObjectForKey("att", decoder: { AccessChallengeAttempts(decoder: $0) }) as? AccessChallengeAttempts)
|
||||
default:
|
||||
assertionFailure()
|
||||
self = .none
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
switch self {
|
||||
case .none:
|
||||
encoder.encodeInt32(0, forKey: "r")
|
||||
case let .numericalPassword(text, timeout, attempts):
|
||||
encoder.encodeInt32(1, forKey: "r")
|
||||
encoder.encodeString(text, forKey: "t")
|
||||
if let timeout = timeout {
|
||||
encoder.encodeInt32(timeout, forKey: "a")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "a")
|
||||
}
|
||||
if let attempts = attempts {
|
||||
encoder.encodeObject(attempts, forKey: "att")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "att")
|
||||
}
|
||||
case let .plaintextPassword(text, timeout, attempts):
|
||||
encoder.encodeInt32(2, forKey: "r")
|
||||
encoder.encodeString(text, forKey: "t")
|
||||
if let timeout = timeout {
|
||||
encoder.encodeInt32(timeout, forKey: "a")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "a")
|
||||
}
|
||||
if let attempts = attempts {
|
||||
encoder.encodeObject(attempts, forKey: "att")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "att")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isLockable: Bool {
|
||||
if case .none = self {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public var autolockDeadline: Int32? {
|
||||
switch self {
|
||||
case .none:
|
||||
return nil
|
||||
case let .numericalPassword(_, timeout, _):
|
||||
return timeout
|
||||
case let .plaintextPassword(_, timeout, _):
|
||||
return timeout
|
||||
}
|
||||
}
|
||||
|
||||
public var attempts: AccessChallengeAttempts? {
|
||||
switch self {
|
||||
case .none:
|
||||
return nil
|
||||
case let .numericalPassword(_, _, attempts):
|
||||
return attempts
|
||||
case let .plaintextPassword(_, _, attempts):
|
||||
return attempts
|
||||
}
|
||||
}
|
||||
|
||||
public func withUpdatedAutolockDeadline(_ autolockDeadline: Int32?) -> PostboxAccessChallengeData {
|
||||
switch self {
|
||||
case .none:
|
||||
return self
|
||||
case let .numericalPassword(value, _, attempts):
|
||||
return .numericalPassword(value: value, timeout: autolockDeadline, attempts: attempts)
|
||||
case let .plaintextPassword(value, _, attempts):
|
||||
return .plaintextPassword(value: value, timeout: autolockDeadline, attempts: attempts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct AuthAccountRecord: PostboxCoding, Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case attributes
|
||||
}
|
||||
|
||||
public let id: AccountRecordId
|
||||
public let attributes: [AccountRecordAttribute]
|
||||
|
||||
init(id: AccountRecordId, attributes: [AccountRecordAttribute]) {
|
||||
self.id = id
|
||||
self.attributes = attributes
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.id = try container.decode(AccountRecordId.self, forKey: .id)
|
||||
let attributesData = try container.decode(Array<Data>.self, forKey: .attributes)
|
||||
var attributes: [AccountRecordAttribute] = []
|
||||
for data in attributesData {
|
||||
if let object = PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? AccountRecordAttribute {
|
||||
attributes.append(object)
|
||||
}
|
||||
}
|
||||
self.attributes = attributes
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
let attributesData: [Data] = self.attributes.map { attribute in
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(attribute)
|
||||
return encoder.makeData()
|
||||
}
|
||||
try container.encode(attributesData, forKey: .attributes)
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.id = AccountRecordId(rawValue: decoder.decodeOptionalInt64ForKey("id")!)
|
||||
self.attributes = decoder.decodeObjectArrayForKey("attributes").compactMap({ $0 as? AccountRecordAttribute })
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.id.rawValue, forKey: "id")
|
||||
encoder.encodeGenericObjectArray(self.attributes.map { $0 as PostboxCoding }, forKey: "attributes")
|
||||
}
|
||||
}
|
||||
|
||||
enum AccountManagerMetadataOperation {
|
||||
case updateCurrentAccountId(AccountRecordId)
|
||||
case updateCurrentAuthAccountRecord(AuthAccountRecord?)
|
||||
}
|
||||
|
||||
private enum MetadataKey: Int64 {
|
||||
case currentAccountId = 0
|
||||
case currentAuthAccount = 1
|
||||
case accessChallenge = 2
|
||||
case version = 3
|
||||
}
|
||||
|
||||
final class AccountManagerMetadataTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private func key(_ key: MetadataKey) -> ValueBoxKey {
|
||||
let result = ValueBoxKey(length: 8)
|
||||
result.setInt64(0, value: key.rawValue)
|
||||
return result
|
||||
}
|
||||
|
||||
func getVersion() -> Int32? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.version)) {
|
||||
var id: Int32 = 0
|
||||
value.read(&id, offset: 0, length: 4)
|
||||
return id
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func setVersion(_ version: Int32) {
|
||||
var value: Int32 = version
|
||||
self.valueBox.set(self.table, key: self.key(.version), value: MemoryBuffer(memory: &value, capacity: 4, length: 4, freeWhenDone: false))
|
||||
}
|
||||
|
||||
func getCurrentAccountId() -> AccountRecordId? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.currentAccountId)) {
|
||||
var id: Int64 = 0
|
||||
value.read(&id, offset: 0, length: 8)
|
||||
return AccountRecordId(rawValue: id)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setCurrentAccountId(_ id: AccountRecordId, operations: inout [AccountManagerMetadataOperation]) {
|
||||
var rawValue = id.rawValue
|
||||
self.valueBox.set(self.table, key: self.key(.currentAccountId), value: MemoryBuffer(memory: &rawValue, capacity: 8, length: 8, freeWhenDone: false))
|
||||
operations.append(.updateCurrentAccountId(id))
|
||||
}
|
||||
|
||||
func getCurrentAuthAccount() -> AuthAccountRecord? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.currentAuthAccount)), let object = PostboxDecoder(buffer: value).decodeRootObject() as? AuthAccountRecord {
|
||||
return object
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setCurrentAuthAccount(_ record: AuthAccountRecord?, operations: inout [AccountManagerMetadataOperation]) {
|
||||
if let record = record {
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(record)
|
||||
withExtendedLifetime(encoder, {
|
||||
self.valueBox.set(self.table, key: self.key(.currentAuthAccount), value: encoder.readBufferNoCopy())
|
||||
})
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: self.key(.currentAuthAccount), secure: false)
|
||||
}
|
||||
operations.append(.updateCurrentAuthAccountRecord(record))
|
||||
}
|
||||
|
||||
func getAccessChallengeData() -> PostboxAccessChallengeData {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.accessChallenge)) {
|
||||
return PostboxAccessChallengeData(decoder: PostboxDecoder(buffer: value))
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func setAccessChallengeData(_ data: PostboxAccessChallengeData) {
|
||||
let encoder = PostboxEncoder()
|
||||
data.encode(encoder)
|
||||
withExtendedLifetime(encoder, {
|
||||
self.valueBox.set(self.table, key: self.key(.accessChallenge), value: encoder.readBufferNoCopy())
|
||||
})
|
||||
}
|
||||
}
|
||||
48
submodules/Postbox/Postbox/AccountManagerRecordTable.swift
Normal file
48
submodules/Postbox/Postbox/AccountManagerRecordTable.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import Foundation
|
||||
|
||||
enum AccountManagerRecordOperation {
|
||||
case set(id: AccountRecordId, record: AccountRecord?)
|
||||
}
|
||||
|
||||
final class AccountManagerRecordTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private func key(_ key: AccountRecordId) -> ValueBoxKey {
|
||||
let result = ValueBoxKey(length: 8)
|
||||
result.setInt64(0, value: key.rawValue)
|
||||
return result
|
||||
}
|
||||
|
||||
func getRecords() -> [AccountRecord] {
|
||||
var records: [AccountRecord] = []
|
||||
self.valueBox.scan(self.table, values: { _, value in
|
||||
let record = AccountRecord(decoder: PostboxDecoder(buffer: value))
|
||||
records.append(record)
|
||||
return true
|
||||
})
|
||||
return records
|
||||
}
|
||||
|
||||
func getRecord(id: AccountRecordId) -> AccountRecord? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(id)) {
|
||||
return AccountRecord(decoder: PostboxDecoder(buffer: value))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setRecord(id: AccountRecordId, record: AccountRecord?, operations: inout [AccountManagerRecordOperation]) {
|
||||
if let record = record {
|
||||
let encoder = PostboxEncoder()
|
||||
record.encode(encoder)
|
||||
withExtendedLifetime(encoder, {
|
||||
self.valueBox.set(self.table, key: self.key(id), value: encoder.readBufferNoCopy())
|
||||
})
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: self.key(id), secure: false)
|
||||
}
|
||||
operations.append(.set(id: id, record: record))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import Foundation
|
||||
|
||||
final class AccountManagerSharedDataTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
func get(key: ValueBoxKey) -> PreferencesEntry? {
|
||||
if let value = self.valueBox.get(self.table, key: key), let object = PostboxDecoder(buffer: value).decodeRootObject() as? PreferencesEntry {
|
||||
return object
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func set(key: ValueBoxKey, value: PreferencesEntry?, updatedKeys: inout Set<ValueBoxKey>) {
|
||||
if let value = value {
|
||||
if let current = self.get(key: key), current.isEqual(to: value) {
|
||||
return
|
||||
}
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(value)
|
||||
self.valueBox.set(self.table, key: key, value: encoder.makeReadBufferAndReset())
|
||||
updatedKeys.insert(key)
|
||||
} else if self.get(key: key) != nil {
|
||||
self.valueBox.remove(self.table, key: key, secure: false)
|
||||
updatedKeys.insert(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
125
submodules/Postbox/Postbox/AccountRecord.swift
Normal file
125
submodules/Postbox/Postbox/AccountRecord.swift
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import Foundation
|
||||
|
||||
public protocol AccountRecordAttribute: class, PostboxCoding {
|
||||
func isEqual(to: AccountRecordAttribute) -> Bool
|
||||
}
|
||||
|
||||
public struct AccountRecordId: Comparable, Hashable, Codable {
|
||||
let rawValue: Int64
|
||||
|
||||
public init(rawValue: Int64) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public var int64: Int64 {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.rawValue.hashValue
|
||||
}
|
||||
|
||||
public static func ==(lhs: AccountRecordId, rhs: AccountRecordId) -> Bool {
|
||||
return lhs.rawValue == rhs.rawValue
|
||||
}
|
||||
|
||||
public static func <(lhs: AccountRecordId, rhs: AccountRecordId) -> Bool {
|
||||
return lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
public func generateAccountRecordId() -> AccountRecordId {
|
||||
var id: Int64 = 0
|
||||
arc4random_buf(&id, 8)
|
||||
return AccountRecordId(rawValue: id)
|
||||
}
|
||||
|
||||
public final class AccountRecord: PostboxCoding, Equatable, Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case attributes
|
||||
case temporarySessionId
|
||||
}
|
||||
|
||||
public let id: AccountRecordId
|
||||
public let attributes: [AccountRecordAttribute]
|
||||
public let temporarySessionId: Int64?
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
if let idString = try? container.decode(String.self, forKey: .id), let idValue = Int64(idString) {
|
||||
self.id = AccountRecordId(rawValue: idValue)
|
||||
} else {
|
||||
self.id = try container.decode(AccountRecordId.self, forKey: .id)
|
||||
}
|
||||
|
||||
let attributesData = try container.decode(Array<Data>.self, forKey: .attributes)
|
||||
var attributes: [AccountRecordAttribute] = []
|
||||
for data in attributesData {
|
||||
if let object = PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? AccountRecordAttribute {
|
||||
attributes.append(object)
|
||||
}
|
||||
}
|
||||
self.attributes = attributes
|
||||
|
||||
if let temporarySessionIdString = try container.decodeIfPresent(String.self, forKey: .temporarySessionId), let temporarySessionIdValue = Int64(temporarySessionIdString) {
|
||||
self.temporarySessionId = temporarySessionIdValue
|
||||
} else {
|
||||
self.temporarySessionId = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(String("\(self.id.rawValue)"), forKey: .id)
|
||||
let attributesData: [Data] = self.attributes.map { attribute in
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(attribute)
|
||||
return encoder.makeData()
|
||||
}
|
||||
try container.encode(attributesData, forKey: .attributes)
|
||||
let temporarySessionIdString: String? = self.temporarySessionId.flatMap({ "\($0)" })
|
||||
try container.encodeIfPresent(temporarySessionIdString, forKey: .temporarySessionId)
|
||||
}
|
||||
|
||||
public init(id: AccountRecordId, attributes: [AccountRecordAttribute], temporarySessionId: Int64?) {
|
||||
self.id = id
|
||||
self.attributes = attributes
|
||||
self.temporarySessionId = temporarySessionId
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.id = AccountRecordId(rawValue: decoder.decodeInt64ForKey("id", orElse: 0))
|
||||
self.attributes = (decoder.decodeObjectArrayForKey("attributes") as [PostboxCoding]).map { $0 as! AccountRecordAttribute }
|
||||
self.temporarySessionId = decoder.decodeOptionalInt64ForKey("temporarySessionId")
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.id.int64, forKey: "id")
|
||||
let attributes: [PostboxCoding] = self.attributes.map { $0 }
|
||||
encoder.encodeGenericObjectArray(attributes, forKey: "attributes")
|
||||
if let temporarySessionId = self.temporarySessionId {
|
||||
encoder.encodeInt64(temporarySessionId, forKey: "temporarySessionId")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "temporarySessionId")
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: AccountRecord, rhs: AccountRecord) -> Bool {
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
if lhs.attributes.count != rhs.attributes.count {
|
||||
return false
|
||||
}
|
||||
for i in 0 ..< lhs.attributes.count {
|
||||
if !lhs.attributes[i].isEqual(to: rhs.attributes[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if lhs.temporarySessionId != rhs.temporarySessionId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
85
submodules/Postbox/Postbox/AccountRecordsView.swift
Normal file
85
submodules/Postbox/Postbox/AccountRecordsView.swift
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableAccountRecordsView {
|
||||
fileprivate var records: [AccountRecord]
|
||||
fileprivate var currentId: AccountRecordId?
|
||||
fileprivate var currentAuth: AuthAccountRecord?
|
||||
|
||||
init(getRecords: () -> [AccountRecord], currentId: AccountRecordId?, currentAuth: AuthAccountRecord?) {
|
||||
self.records = getRecords()
|
||||
self.currentId = currentId
|
||||
self.currentAuth = currentAuth
|
||||
}
|
||||
|
||||
func replay(operations: [AccountManagerRecordOperation], metadataOperations: [AccountManagerMetadataOperation]) -> Bool {
|
||||
var updated = false
|
||||
|
||||
for operation in operations {
|
||||
switch operation {
|
||||
case let .set(id, record):
|
||||
if let record = record {
|
||||
updated = true
|
||||
var found = false
|
||||
for i in 0 ..< self.records.count {
|
||||
if self.records[i].id == id {
|
||||
self.records[i] = record
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
self.records.append(record)
|
||||
self.records.sort(by: { lhs, rhs in
|
||||
return lhs.id < rhs.id
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for i in 0 ..< self.records.count {
|
||||
if self.records[i].id == id {
|
||||
self.records.remove(at: i)
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for operation in metadataOperations {
|
||||
switch operation {
|
||||
case let .updateCurrentAccountId(id):
|
||||
updated = true
|
||||
self.currentId = id
|
||||
case let .updateCurrentAuthAccountRecord(record):
|
||||
updated = true
|
||||
self.currentAuth = record
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
public final class AccountRecordsView {
|
||||
public let records: [AccountRecord]
|
||||
public let currentRecord: AccountRecord?
|
||||
public let currentAuthAccount: AuthAccountRecord?
|
||||
|
||||
init(_ view: MutableAccountRecordsView) {
|
||||
self.records = view.records
|
||||
if let currentId = view.currentId {
|
||||
var currentRecord: AccountRecord?
|
||||
for record in view.records {
|
||||
if record.id == currentId {
|
||||
currentRecord = record
|
||||
break
|
||||
}
|
||||
}
|
||||
self.currentRecord = currentRecord
|
||||
} else {
|
||||
self.currentRecord = nil
|
||||
}
|
||||
self.currentAuthAccount = view.currentAuth
|
||||
}
|
||||
}
|
||||
36
submodules/Postbox/Postbox/AccountSharedData.swift
Normal file
36
submodules/Postbox/Postbox/AccountSharedData.swift
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableAccountSharedDataView {
|
||||
private let keys: Set<ValueBoxKey>
|
||||
fileprivate var entries: [ValueBoxKey: PreferencesEntry] = [:]
|
||||
|
||||
init(accountManagerImpl: AccountManagerImpl, keys: Set<ValueBoxKey>) {
|
||||
self.keys = keys
|
||||
for key in keys {
|
||||
if let value = accountManagerImpl.sharedDataTable.get(key: key) {
|
||||
self.entries[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replay(accountManagerImpl: AccountManagerImpl, updatedKeys: Set<ValueBoxKey>) -> Bool {
|
||||
var updated = false
|
||||
for key in updatedKeys.intersection(self.keys) {
|
||||
if let value = accountManagerImpl.sharedDataTable.get(key: key) {
|
||||
self.entries[key] = value
|
||||
} else {
|
||||
self.entries.removeValue(forKey: key)
|
||||
}
|
||||
updated = true
|
||||
}
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
public final class AccountSharedDataView {
|
||||
public let entries: [ValueBoxKey: PreferencesEntry]
|
||||
|
||||
init(_ view: MutableAccountSharedDataView) {
|
||||
self.entries = view.entries
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import Foundation
|
||||
|
||||
final class AdditionalChatListItemsTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private var cachedItems: [PeerId]?
|
||||
private var updatedItems = false
|
||||
|
||||
private func key(_ index: Int32) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: index)
|
||||
return key
|
||||
}
|
||||
|
||||
private func lowerBound() -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 1)
|
||||
key.setInt8(0, value: 0)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound() -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: Int32.max)
|
||||
return key
|
||||
}
|
||||
|
||||
func set(_ items: [PeerId]) -> Bool {
|
||||
if self.get() == items {
|
||||
return false
|
||||
}
|
||||
self.cachedItems = items
|
||||
self.updatedItems = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func get() -> [PeerId] {
|
||||
if let cachedItems = self.cachedItems {
|
||||
return cachedItems
|
||||
}
|
||||
var items: [PeerId] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(), end: self.upperBound(), values: { key, value in
|
||||
assert(key.getInt32(0) == Int32(items.count))
|
||||
var peerIdValue: Int64 = 0
|
||||
value.read(&peerIdValue, offset: 0, length: 8)
|
||||
items.append(PeerId(peerIdValue))
|
||||
return true
|
||||
}, limit: 0)
|
||||
self.cachedItems = items
|
||||
return items
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedItems = nil
|
||||
assert(!self.updatedItems)
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if self.updatedItems {
|
||||
var keys: [ValueBoxKey] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(), end: self.upperBound(), keys: { key in
|
||||
keys.append(key)
|
||||
return true
|
||||
}, limit: 0)
|
||||
for key in keys {
|
||||
self.valueBox.remove(self.table, key: key, secure: false)
|
||||
}
|
||||
|
||||
if let items = self.cachedItems {
|
||||
var index: Int32 = 0
|
||||
for item in items {
|
||||
var peerIdValue = item.toInt64()
|
||||
self.valueBox.set(self.table, key: self.key(index), value: MemoryBuffer(memory: &peerIdValue, capacity: 8, length: 8, freeWhenDone: false))
|
||||
index += 1
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
self.updatedItems = false
|
||||
}
|
||||
}
|
||||
}
|
||||
29
submodules/Postbox/Postbox/AdditionalChatListItemsView.swift
Normal file
29
submodules/Postbox/Postbox/AdditionalChatListItemsView.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableAdditionalChatListItemsView: MutablePostboxView {
|
||||
fileprivate var items: Set<PeerId>
|
||||
|
||||
init(postbox: Postbox) {
|
||||
self.items = Set(postbox.additionalChatListItemsTable.get())
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
if let items = transaction.replacedAdditionalChatListItems {
|
||||
self.items = Set(items)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return AdditionalChatListItemsView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class AdditionalChatListItemsView: PostboxView {
|
||||
public let items: Set<PeerId>
|
||||
|
||||
init(_ view: MutableAdditionalChatListItemsView) {
|
||||
self.items = view.items
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import Foundation
|
||||
|
||||
public enum AdditionalMessageHistoryViewData {
|
||||
case cachedPeerData(PeerId)
|
||||
case cachedPeerDataMessages(PeerId)
|
||||
case peerChatState(PeerId)
|
||||
case totalUnreadState
|
||||
case peerNotificationSettings(PeerId)
|
||||
case cacheEntry(ItemCacheEntryId)
|
||||
case preferencesEntry(ValueBoxKey)
|
||||
case peer(PeerId)
|
||||
case peerIsContact(PeerId)
|
||||
}
|
||||
|
||||
public enum AdditionalMessageHistoryViewDataEntry {
|
||||
case cachedPeerData(PeerId, CachedPeerData?)
|
||||
case cachedPeerDataMessages(PeerId, [MessageId: Message]?)
|
||||
case peerChatState(PeerId, PeerChatState?)
|
||||
case totalUnreadState(ChatListTotalUnreadState)
|
||||
case peerNotificationSettings(PeerNotificationSettings?)
|
||||
case cacheEntry(ItemCacheEntryId, PostboxCoding?)
|
||||
case preferencesEntry(ValueBoxKey, PreferencesEntry?)
|
||||
case peerIsContact(PeerId, Bool)
|
||||
case peer(PeerId, Peer?)
|
||||
}
|
||||
100
submodules/Postbox/Postbox/BinarySearch.swift
Normal file
100
submodules/Postbox/Postbox/BinarySearch.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import Foundation
|
||||
|
||||
func binarySearch<A, T: Comparable>(_ inputArr: [A], extract: (A) -> T, searchItem: T) -> Int? {
|
||||
var lowerIndex = 0
|
||||
var upperIndex = inputArr.count - 1
|
||||
|
||||
if lowerIndex > upperIndex {
|
||||
return nil
|
||||
}
|
||||
|
||||
while true {
|
||||
let currentIndex = (lowerIndex + upperIndex) / 2
|
||||
let value = extract(inputArr[currentIndex])
|
||||
|
||||
if value == searchItem {
|
||||
return currentIndex
|
||||
} else if lowerIndex > upperIndex {
|
||||
return nil
|
||||
} else {
|
||||
if (value > searchItem) {
|
||||
upperIndex = currentIndex - 1
|
||||
} else {
|
||||
lowerIndex = currentIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func binarySearch<T: Comparable>(_ inputArr: [T], searchItem: T) -> Int? {
|
||||
var lowerIndex = 0;
|
||||
var upperIndex = inputArr.count - 1
|
||||
|
||||
if lowerIndex > upperIndex {
|
||||
return nil
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let currentIndex = (lowerIndex + upperIndex) / 2
|
||||
if (inputArr[currentIndex] == searchItem) {
|
||||
return currentIndex
|
||||
} else if (lowerIndex > upperIndex) {
|
||||
return nil
|
||||
} else {
|
||||
if (inputArr[currentIndex] > searchItem) {
|
||||
upperIndex = currentIndex - 1
|
||||
} else {
|
||||
lowerIndex = currentIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func binaryInsertionIndex<A, T: Comparable>(_ inputArr: [A], extract: (A) -> T, searchItem: T) -> Int {
|
||||
var lo = 0
|
||||
var hi = inputArr.count - 1
|
||||
while lo <= hi {
|
||||
let mid = (lo + hi) / 2
|
||||
let value = extract(inputArr[mid])
|
||||
if value < searchItem {
|
||||
lo = mid + 1
|
||||
} else if searchItem < value {
|
||||
hi = mid - 1
|
||||
} else {
|
||||
return mid
|
||||
}
|
||||
}
|
||||
return lo
|
||||
}
|
||||
|
||||
func binaryInsertionIndex<T: Comparable>(_ inputArr: [T], searchItem: T) -> Int {
|
||||
var lo = 0
|
||||
var hi = inputArr.count - 1
|
||||
while lo <= hi {
|
||||
let mid = (lo + hi) / 2
|
||||
if inputArr[mid] < searchItem {
|
||||
lo = mid + 1
|
||||
} else if searchItem < inputArr[mid] {
|
||||
hi = mid - 1
|
||||
} else {
|
||||
return mid
|
||||
}
|
||||
}
|
||||
return lo
|
||||
}
|
||||
|
||||
func binaryInsertionIndexReverse<T: Comparable>(_ inputArr: [T], searchItem: T) -> Int {
|
||||
var lo = 0
|
||||
var hi = inputArr.count - 1
|
||||
while lo <= hi {
|
||||
let mid = (lo + hi) / 2
|
||||
if inputArr[mid] > searchItem {
|
||||
lo = mid + 1
|
||||
} else if searchItem > inputArr[mid] {
|
||||
hi = mid - 1
|
||||
} else {
|
||||
return mid
|
||||
}
|
||||
}
|
||||
return lo
|
||||
}
|
||||
31
submodules/Postbox/Postbox/CachedItemView.swift
Normal file
31
submodules/Postbox/Postbox/CachedItemView.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableCachedItemView: MutablePostboxView {
|
||||
private let id: ItemCacheEntryId
|
||||
fileprivate var value: PostboxCoding?
|
||||
|
||||
init(postbox: Postbox, id: ItemCacheEntryId) {
|
||||
self.id = id
|
||||
self.value = postbox.itemCacheTable.retrieve(id: id, metaTable: postbox.itemCacheMetaTable)
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
if transaction.updatedCacheEntryKeys.contains(self.id) {
|
||||
self.value = postbox.itemCacheTable.retrieve(id: id, metaTable: postbox.itemCacheMetaTable)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return CachedItemView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class CachedItemView: PostboxView {
|
||||
public let value: PostboxCoding?
|
||||
|
||||
init(_ view: MutableCachedItemView) {
|
||||
self.value = view.value
|
||||
}
|
||||
}
|
||||
9
submodules/Postbox/Postbox/CachedPeerData.swift
Normal file
9
submodules/Postbox/Postbox/CachedPeerData.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
public protocol CachedPeerData: PostboxCoding {
|
||||
var peerIds: Set<PeerId> { get }
|
||||
var messageIds: Set<MessageId> { get }
|
||||
|
||||
var associatedHistoryMessageId: MessageId? { get }
|
||||
|
||||
func isEqual(to: CachedPeerData) -> Bool
|
||||
}
|
||||
59
submodules/Postbox/Postbox/CachedPeerDataTable.swift
Normal file
59
submodules/Postbox/Postbox/CachedPeerDataTable.swift
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import Foundation
|
||||
|
||||
final class CachedPeerDataTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let sharedEncoder = PostboxEncoder()
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
|
||||
private var cachedDatas: [PeerId: CachedPeerData] = [:]
|
||||
private var updatedPeerIds = Set<PeerId>()
|
||||
|
||||
override init(valueBox: ValueBox, table: ValueBoxTable) {
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(_ id: PeerId) -> ValueBoxKey {
|
||||
self.sharedKey.setInt64(0, value: id.toInt64())
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
func set(id: PeerId, data: CachedPeerData) {
|
||||
self.cachedDatas[id] = data
|
||||
self.updatedPeerIds.insert(id)
|
||||
}
|
||||
|
||||
func get(_ id: PeerId) -> CachedPeerData? {
|
||||
if let status = self.cachedDatas[id] {
|
||||
return status
|
||||
}
|
||||
if let value = self.valueBox.get(self.table, key: self.key(id)) {
|
||||
if let data = PostboxDecoder(buffer: value).decodeRootObject() as? CachedPeerData {
|
||||
self.cachedDatas[id] = data
|
||||
return data
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedDatas.removeAll()
|
||||
self.updatedPeerIds.removeAll()
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
for peerId in self.updatedPeerIds {
|
||||
if let data = self.cachedDatas[peerId] {
|
||||
self.sharedEncoder.reset()
|
||||
self.sharedEncoder.encodeRootObject(data)
|
||||
|
||||
self.valueBox.set(self.table, key: self.key(peerId), value: self.sharedEncoder.readBufferNoCopy())
|
||||
}
|
||||
}
|
||||
|
||||
self.updatedPeerIds.removeAll()
|
||||
self.cachedDatas.removeAll()
|
||||
}
|
||||
}
|
||||
34
submodules/Postbox/Postbox/CachedPeerDataView.swift
Normal file
34
submodules/Postbox/Postbox/CachedPeerDataView.swift
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableCachedPeerDataView: MutablePostboxView {
|
||||
let peerId: PeerId
|
||||
var cachedPeerData: CachedPeerData?
|
||||
|
||||
init(postbox: Postbox, peerId: PeerId) {
|
||||
self.peerId = peerId
|
||||
self.cachedPeerData = postbox.cachedPeerDataTable.get(peerId)
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
if let cachedPeerData = transaction.currentUpdatedCachedPeerData[self.peerId] {
|
||||
self.cachedPeerData = cachedPeerData
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return CachedPeerDataView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class CachedPeerDataView: PostboxView {
|
||||
public let peerId: PeerId
|
||||
public let cachedPeerData: CachedPeerData?
|
||||
|
||||
init(_ view: MutableCachedPeerDataView) {
|
||||
self.peerId = view.peerId
|
||||
self.cachedPeerData = view.cachedPeerData
|
||||
}
|
||||
}
|
||||
21
submodules/Postbox/Postbox/ChatListHole.swift
Normal file
21
submodules/Postbox/Postbox/ChatListHole.swift
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import Foundation
|
||||
|
||||
public struct ChatListHole: Hashable, CustomStringConvertible {
|
||||
public let index: MessageIndex
|
||||
|
||||
public init(index: MessageIndex) {
|
||||
self.index = index
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "ChatListHole(\(self.index.id), \(self.index.timestamp))"
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.index.hashValue
|
||||
}
|
||||
|
||||
public static func <(lhs: ChatListHole, rhs: ChatListHole) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
}
|
||||
27
submodules/Postbox/Postbox/ChatListHolesView.swift
Normal file
27
submodules/Postbox/Postbox/ChatListHolesView.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import Foundation
|
||||
|
||||
public struct ChatListHolesEntry: Hashable {
|
||||
public let groupId: PeerGroupId
|
||||
public let hole: ChatListHole
|
||||
}
|
||||
|
||||
final class MutableChatListHolesView {
|
||||
fileprivate var entries = Set<ChatListHolesEntry>()
|
||||
|
||||
func update(holes: Set<ChatListHolesEntry>) -> Bool {
|
||||
if self.entries != holes {
|
||||
self.entries = holes
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatListHolesView {
|
||||
public let entries: Set<ChatListHolesEntry>
|
||||
|
||||
init(_ mutableView: MutableChatListHolesView) {
|
||||
self.entries = mutableView.entries
|
||||
}
|
||||
}
|
||||
639
submodules/Postbox/Postbox/ChatListIndexTable.swift
Normal file
639
submodules/Postbox/Postbox/ChatListIndexTable.swift
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
import Foundation
|
||||
|
||||
func shouldPeerParticipateInUnreadCountStats(peer: Peer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
struct ChatListPeerInclusionIndex {
|
||||
let topMessageIndex: MessageIndex?
|
||||
let inclusion: PeerChatListInclusion
|
||||
|
||||
func includedIndex(peerId: PeerId) -> (PeerGroupId, ChatListIndex)? {
|
||||
switch inclusion {
|
||||
case .notIncluded:
|
||||
return nil
|
||||
case let .ifHasMessagesOrOneOf(groupId, pinningIndex, minTimestamp):
|
||||
if let minTimestamp = minTimestamp {
|
||||
if let topMessageIndex = self.topMessageIndex, topMessageIndex.timestamp >= minTimestamp {
|
||||
return (groupId, ChatListIndex(pinningIndex: pinningIndex, messageIndex: topMessageIndex))
|
||||
} else {
|
||||
return (groupId, ChatListIndex(pinningIndex: pinningIndex, messageIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: minTimestamp)))
|
||||
}
|
||||
} else if let topMessageIndex = self.topMessageIndex {
|
||||
return (groupId, ChatListIndex(pinningIndex: pinningIndex, messageIndex: topMessageIndex))
|
||||
} else if let pinningIndex = pinningIndex {
|
||||
return (groupId, ChatListIndex(pinningIndex: pinningIndex, messageIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: 0)))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ChatListIndexFlags: OptionSet {
|
||||
var rawValue: Int8
|
||||
|
||||
init(rawValue: Int8) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let hasIndex = ChatListIndexFlags(rawValue: 1 << 0)
|
||||
}
|
||||
|
||||
final class ChatListIndexTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let peerNameIndexTable: PeerNameIndexTable
|
||||
private let metadataTable: MessageHistoryMetadataTable
|
||||
private let readStateTable: MessageHistoryReadStateTable
|
||||
private let notificationSettingsTable: PeerNotificationSettingsTable
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
|
||||
private var cachedPeerIndices: [PeerId: ChatListPeerInclusionIndex] = [:]
|
||||
|
||||
private var updatedPreviousPeerCachedIndices: [PeerId: ChatListPeerInclusionIndex] = [:]
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, peerNameIndexTable: PeerNameIndexTable, metadataTable: MessageHistoryMetadataTable, readStateTable: MessageHistoryReadStateTable, notificationSettingsTable: PeerNotificationSettingsTable) {
|
||||
self.peerNameIndexTable = peerNameIndexTable
|
||||
self.metadataTable = metadataTable
|
||||
self.readStateTable = readStateTable
|
||||
self.notificationSettingsTable = notificationSettingsTable
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(_ peerId: PeerId) -> ValueBoxKey {
|
||||
self.sharedKey.setInt32(0, value: peerId.namespace)
|
||||
self.sharedKey.setInt32(4, value: peerId.id)
|
||||
assert(self.sharedKey.getInt64(0) == peerId.toInt64())
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private func key(_ groupId: PeerGroupId) -> ValueBoxKey {
|
||||
self.sharedKey.setInt32(0, value: Int32.max)
|
||||
self.sharedKey.setInt32(4, value: groupId.rawValue)
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
func setTopMessageIndex(peerId: PeerId, index: MessageIndex?) -> ChatListPeerInclusionIndex {
|
||||
let current = self.get(peerId: peerId)
|
||||
if self.updatedPreviousPeerCachedIndices[peerId] == nil {
|
||||
self.updatedPreviousPeerCachedIndices[peerId] = current
|
||||
}
|
||||
let updated = ChatListPeerInclusionIndex(topMessageIndex: index, inclusion: current.inclusion)
|
||||
self.cachedPeerIndices[peerId] = updated
|
||||
return updated
|
||||
}
|
||||
|
||||
func setInclusion(peerId: PeerId, inclusion: PeerChatListInclusion) -> ChatListPeerInclusionIndex {
|
||||
let current = self.get(peerId: peerId)
|
||||
if self.updatedPreviousPeerCachedIndices[peerId] == nil {
|
||||
self.updatedPreviousPeerCachedIndices[peerId] = current
|
||||
}
|
||||
let updated = ChatListPeerInclusionIndex(topMessageIndex: current.topMessageIndex, inclusion: inclusion)
|
||||
self.cachedPeerIndices[peerId] = updated
|
||||
return updated
|
||||
}
|
||||
|
||||
func get(peerId: PeerId) -> ChatListPeerInclusionIndex {
|
||||
if let cached = self.cachedPeerIndices[peerId] {
|
||||
return cached
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(peerId)) {
|
||||
let topMessageIndex: MessageIndex?
|
||||
|
||||
var flagsValue: Int8 = 0
|
||||
value.read(&flagsValue, offset: 0, length: 1)
|
||||
let flags = ChatListIndexFlags(rawValue: flagsValue)
|
||||
|
||||
if flags.contains(.hasIndex) {
|
||||
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)
|
||||
topMessageIndex = MessageIndex(id: MessageId(peerId: peerId, namespace: idNamespace, id: idId), timestamp: idTimestamp)
|
||||
} else {
|
||||
topMessageIndex = nil
|
||||
}
|
||||
|
||||
let inclusion: PeerChatListInclusion
|
||||
|
||||
var inclusionId: Int8 = 0
|
||||
value.read(&inclusionId, offset: 0, length: 1)
|
||||
if inclusionId == 0 {
|
||||
inclusion = .notIncluded
|
||||
} else if inclusionId == 1 {
|
||||
var pinningIndexValue: UInt16 = 0
|
||||
value.read(&pinningIndexValue, offset: 0, length: 2)
|
||||
|
||||
var hasMinTimestamp: Int8 = 0
|
||||
value.read(&hasMinTimestamp, offset: 0, length: 1)
|
||||
let minTimestamp: Int32?
|
||||
if hasMinTimestamp != 0 {
|
||||
var minTimestampValue: Int32 = 0
|
||||
value.read(&minTimestampValue, offset: 0, length: 4)
|
||||
minTimestamp = minTimestampValue
|
||||
} else {
|
||||
minTimestamp = nil
|
||||
}
|
||||
|
||||
var groupIdValue: Int32 = 0
|
||||
value.read(&groupIdValue, offset: 0, length: 4)
|
||||
|
||||
inclusion = .ifHasMessagesOrOneOf(groupId: PeerGroupId(rawValue: groupIdValue), pinningIndex: chatListPinningIndexFromKeyValue(pinningIndexValue), minTimestamp: minTimestamp)
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
let inclusionIndex = ChatListPeerInclusionIndex(topMessageIndex: topMessageIndex, inclusion: inclusion)
|
||||
self.cachedPeerIndices[peerId] = inclusionIndex
|
||||
return inclusionIndex
|
||||
} else {
|
||||
let inclusionIndex = ChatListPeerInclusionIndex(topMessageIndex: nil, inclusion: .notIncluded)
|
||||
self.cachedPeerIndices[peerId] = inclusionIndex
|
||||
return inclusionIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAllPeerIds() -> [PeerId] {
|
||||
var peerIds: [PeerId] = []
|
||||
self.valueBox.scanInt64(self.table, keys: { key in
|
||||
peerIds.append(PeerId(key))
|
||||
return true
|
||||
})
|
||||
return peerIds
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedPeerIndices.removeAll()
|
||||
assert(self.updatedPreviousPeerCachedIndices.isEmpty)
|
||||
}
|
||||
|
||||
func commitWithTransaction(postbox: Postbox, alteredInitialPeerCombinedReadStates: [PeerId: CombinedPeerReadState], updatedPeers: [(Peer?, Peer)], transactionParticipationInTotalUnreadCountUpdates: (added: Set<PeerId>, removed: Set<PeerId>), updatedRootUnreadState: inout ChatListTotalUnreadState?, updatedGroupTotalUnreadSummaries: inout [PeerGroupId: PeerGroupUnreadCountersCombinedSummary], currentUpdatedGroupSummarySynchronizeOperations: inout [PeerGroupAndNamespace: Bool]) {
|
||||
var updatedPeerTags: [PeerId: (previous: PeerSummaryCounterTags, updated: PeerSummaryCounterTags)] = [:]
|
||||
for (previous, updated) in updatedPeers {
|
||||
let previousTags: PeerSummaryCounterTags
|
||||
if let previous = previous {
|
||||
previousTags = postbox.seedConfiguration.peerSummaryCounterTags(previous)
|
||||
} else {
|
||||
previousTags = []
|
||||
}
|
||||
let updatedTags = postbox.seedConfiguration.peerSummaryCounterTags(updated)
|
||||
if previousTags != updatedTags {
|
||||
updatedPeerTags[updated.id] = (previousTags, updatedTags)
|
||||
}
|
||||
}
|
||||
|
||||
if !self.updatedPreviousPeerCachedIndices.isEmpty || !alteredInitialPeerCombinedReadStates.isEmpty || !updatedPeerTags.isEmpty || !transactionParticipationInTotalUnreadCountUpdates.added.isEmpty || !transactionParticipationInTotalUnreadCountUpdates.removed.isEmpty {
|
||||
var addedToGroupPeerIds: [PeerId: PeerGroupId] = [:]
|
||||
var removedFromGroupPeerIds: [PeerId: PeerGroupId] = [:]
|
||||
var addedToIndexPeerIds = Set<PeerId>()
|
||||
var removedFromIndexPeerIds = Set<PeerId>()
|
||||
|
||||
for (peerId, previousIndex) in self.updatedPreviousPeerCachedIndices {
|
||||
let index = self.cachedPeerIndices[peerId]!
|
||||
if let (currentGroupId, _) = index.includedIndex(peerId: peerId) {
|
||||
let previousGroupId = previousIndex.includedIndex(peerId: peerId)?.0
|
||||
if previousGroupId != currentGroupId {
|
||||
addedToGroupPeerIds[peerId] = currentGroupId
|
||||
if let previousGroupId = previousGroupId {
|
||||
removedFromGroupPeerIds[peerId] = previousGroupId
|
||||
} else {
|
||||
addedToIndexPeerIds.insert(peerId)
|
||||
}
|
||||
}
|
||||
} else if let (previousGroupId, _) = previousIndex.includedIndex(peerId: peerId) {
|
||||
removedFromGroupPeerIds[peerId] = previousGroupId
|
||||
removedFromIndexPeerIds.insert(peerId)
|
||||
}
|
||||
|
||||
let writeBuffer = WriteBuffer()
|
||||
|
||||
var flags: ChatListIndexFlags = []
|
||||
|
||||
if index.topMessageIndex != nil {
|
||||
flags.insert(.hasIndex)
|
||||
}
|
||||
|
||||
var flagsValue = flags.rawValue
|
||||
writeBuffer.write(&flagsValue, offset: 0, length: 1)
|
||||
|
||||
if let topMessageIndex = index.topMessageIndex {
|
||||
var idNamespace: Int32 = topMessageIndex.id.namespace
|
||||
var idId: Int32 = topMessageIndex.id.id
|
||||
var idTimestamp: Int32 = topMessageIndex.timestamp
|
||||
writeBuffer.write(&idNamespace, offset: 0, length: 4)
|
||||
writeBuffer.write(&idId, offset: 0, length: 4)
|
||||
writeBuffer.write(&idTimestamp, offset: 0, length: 4)
|
||||
}
|
||||
|
||||
switch index.inclusion {
|
||||
case .notIncluded:
|
||||
var key: Int8 = 0
|
||||
writeBuffer.write(&key, offset: 0, length: 1)
|
||||
case let .ifHasMessagesOrOneOf(groupId, pinningIndex, minTimestamp):
|
||||
var key: Int8 = 1
|
||||
writeBuffer.write(&key, offset: 0, length: 1)
|
||||
|
||||
var pinningIndexValue: UInt16 = keyValueForChatListPinningIndex(pinningIndex)
|
||||
writeBuffer.write(&pinningIndexValue, offset: 0, length: 2)
|
||||
|
||||
if let minTimestamp = minTimestamp {
|
||||
var hasMinTimestamp: Int8 = 1
|
||||
writeBuffer.write(&hasMinTimestamp, offset: 0, length: 1)
|
||||
|
||||
var minTimestampValue = minTimestamp
|
||||
writeBuffer.write(&minTimestampValue, offset: 0, length: 4)
|
||||
} else {
|
||||
var hasMinTimestamp: Int8 = 0
|
||||
writeBuffer.write(&hasMinTimestamp, offset: 0, length: 1)
|
||||
}
|
||||
|
||||
var groupIdValue = groupId.rawValue
|
||||
writeBuffer.write(&groupIdValue, offset: 0, length: 4)
|
||||
}
|
||||
|
||||
withExtendedLifetime(writeBuffer, {
|
||||
self.valueBox.set(self.table, key: self.key(peerId), value: writeBuffer.readBufferNoCopy())
|
||||
})
|
||||
}
|
||||
self.updatedPreviousPeerCachedIndices.removeAll()
|
||||
|
||||
for peerId in addedToIndexPeerIds {
|
||||
self.peerNameIndexTable.setPeerCategoryState(peerId: peerId, category: [.chats], includes: true)
|
||||
}
|
||||
|
||||
for peerId in removedFromIndexPeerIds {
|
||||
self.peerNameIndexTable.setPeerCategoryState(peerId: peerId, category: [.chats], includes: false)
|
||||
}
|
||||
|
||||
var alteredPeerIds = Set<PeerId>()
|
||||
for (peerId, _) in alteredInitialPeerCombinedReadStates {
|
||||
alteredPeerIds.insert(peerId)
|
||||
}
|
||||
alteredPeerIds.formUnion(addedToGroupPeerIds.keys)
|
||||
alteredPeerIds.formUnion(removedFromGroupPeerIds.keys)
|
||||
alteredPeerIds.formUnion(transactionParticipationInTotalUnreadCountUpdates.added)
|
||||
alteredPeerIds.formUnion(transactionParticipationInTotalUnreadCountUpdates.removed)
|
||||
|
||||
for peerId in updatedPeerTags.keys {
|
||||
alteredPeerIds.insert(peerId)
|
||||
}
|
||||
|
||||
func alterTags(_ totalUnreadState: inout ChatListTotalUnreadState, _ peerId: PeerId, _ tag: PeerSummaryCounterTags, _ f: (ChatListTotalUnreadCounters, ChatListTotalUnreadCounters) -> (ChatListTotalUnreadCounters, ChatListTotalUnreadCounters)) {
|
||||
if totalUnreadState.absoluteCounters[tag] == nil {
|
||||
totalUnreadState.absoluteCounters[tag] = ChatListTotalUnreadCounters(messageCount: 0, chatCount: 0)
|
||||
}
|
||||
if totalUnreadState.filteredCounters[tag] == nil {
|
||||
totalUnreadState.filteredCounters[tag] = ChatListTotalUnreadCounters(messageCount: 0, chatCount: 0)
|
||||
}
|
||||
var (updatedAbsoluteCounters, updatedFilteredCounters) = f(totalUnreadState.absoluteCounters[tag]!, totalUnreadState.filteredCounters[tag]!)
|
||||
if updatedAbsoluteCounters.messageCount < 0 {
|
||||
updatedAbsoluteCounters.messageCount = 0
|
||||
}
|
||||
if updatedAbsoluteCounters.chatCount < 0 {
|
||||
updatedAbsoluteCounters.chatCount = 0
|
||||
}
|
||||
if updatedFilteredCounters.messageCount < 0 {
|
||||
updatedFilteredCounters.messageCount = 0
|
||||
}
|
||||
if updatedFilteredCounters.chatCount < 0 {
|
||||
updatedFilteredCounters.chatCount = 0
|
||||
}
|
||||
totalUnreadState.absoluteCounters[tag] = updatedAbsoluteCounters
|
||||
totalUnreadState.filteredCounters[tag] = updatedFilteredCounters
|
||||
}
|
||||
|
||||
func alterNamespace(summary: inout PeerGroupUnreadCountersSummary, previousState: PeerReadState?, updatedState: PeerReadState?) {
|
||||
let previousCount = previousState?.count ?? 0
|
||||
let updatedCount = updatedState?.count ?? 0
|
||||
if previousCount != updatedCount {
|
||||
if (previousCount != 0) != (updatedCount != 0) {
|
||||
if updatedCount != 0 {
|
||||
summary.all.chatCount += 1
|
||||
} else {
|
||||
summary.all.chatCount -= 1
|
||||
summary.all.chatCount = max(0, summary.all.chatCount)
|
||||
}
|
||||
}
|
||||
summary.all.messageCount += updatedCount - previousCount
|
||||
summary.all.messageCount = max(0, summary.all.messageCount)
|
||||
}
|
||||
}
|
||||
|
||||
var updatedRootState: ChatListTotalUnreadState?
|
||||
var updatedTotalUnreadSummaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary] = [:]
|
||||
|
||||
for peerId in alteredPeerIds {
|
||||
guard let peer = postbox.peerTable.get(peerId) else {
|
||||
continue
|
||||
}
|
||||
let notificationPeerId: PeerId = peer.associatedPeerId ?? peerId
|
||||
let initialReadState = alteredInitialPeerCombinedReadStates[peerId] ?? postbox.readStateTable.getCombinedState(peerId)
|
||||
let currentReadState = postbox.readStateTable.getCombinedState(peerId)
|
||||
|
||||
var groupIds: [PeerGroupId] = []
|
||||
if let (groupId, _) = self.get(peerId: peerId).includedIndex(peerId: peerId) {
|
||||
groupIds.append(groupId)
|
||||
}
|
||||
if let groupId = addedToGroupPeerIds[peerId] {
|
||||
if !groupIds.contains(groupId) {
|
||||
groupIds.append(groupId)
|
||||
}
|
||||
}
|
||||
if let groupId = removedFromGroupPeerIds[peerId] {
|
||||
if !groupIds.contains(groupId) {
|
||||
groupIds.append(groupId)
|
||||
}
|
||||
}
|
||||
|
||||
for groupId in groupIds {
|
||||
var totalRootUnreadState: ChatListTotalUnreadState?
|
||||
var summary: PeerGroupUnreadCountersCombinedSummary
|
||||
if case .root = groupId {
|
||||
if let current = updatedRootState {
|
||||
totalRootUnreadState = current
|
||||
} else {
|
||||
totalRootUnreadState = postbox.messageHistoryMetadataTable.getChatListTotalUnreadState()
|
||||
}
|
||||
}
|
||||
if let current = updatedTotalUnreadSummaries[groupId] {
|
||||
summary = current
|
||||
} else {
|
||||
summary = postbox.groupMessageStatsTable.get(groupId: groupId)
|
||||
}
|
||||
|
||||
var initialValue: (Int32, Bool, Bool) = (0, false, false)
|
||||
var currentValue: (Int32, Bool, Bool) = (0, false, false)
|
||||
|
||||
var initialStates: CombinedPeerReadState = CombinedPeerReadState(states: [])
|
||||
var currentStates: CombinedPeerReadState = CombinedPeerReadState(states: [])
|
||||
|
||||
if addedToGroupPeerIds[peerId] == groupId {
|
||||
if let currentReadState = currentReadState {
|
||||
currentValue = (currentReadState.count, currentReadState.isUnread, currentReadState.markedUnread)
|
||||
currentStates = currentReadState
|
||||
}
|
||||
} else if removedFromGroupPeerIds[peerId] == groupId {
|
||||
if let initialReadState = initialReadState {
|
||||
initialValue = (initialReadState.count, initialReadState.isUnread, initialReadState.markedUnread)
|
||||
initialStates = initialReadState
|
||||
}
|
||||
} else {
|
||||
if self.get(peerId: peerId).includedIndex(peerId: peerId)?.0 == groupId {
|
||||
if let initialReadState = initialReadState {
|
||||
initialValue = (initialReadState.count, initialReadState.isUnread, initialReadState.markedUnread)
|
||||
initialStates = initialReadState
|
||||
}
|
||||
if let currentReadState = currentReadState {
|
||||
currentValue = (currentReadState.count, currentReadState.isUnread, currentReadState.markedUnread)
|
||||
currentStates = currentReadState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var initialFilteredValue: (Int32, Bool, Bool) = initialValue
|
||||
var currentFilteredValue: (Int32, Bool, Bool) = currentValue
|
||||
|
||||
var initialFilteredStates: CombinedPeerReadState = initialStates
|
||||
var currentFilteredStates: CombinedPeerReadState = currentStates
|
||||
|
||||
if transactionParticipationInTotalUnreadCountUpdates.added.contains(peerId) {
|
||||
initialFilteredValue = (0, false, false)
|
||||
initialFilteredStates = CombinedPeerReadState(states: [])
|
||||
} else if transactionParticipationInTotalUnreadCountUpdates.removed.contains(peerId) {
|
||||
currentFilteredValue = (0, false, false)
|
||||
currentFilteredStates = CombinedPeerReadState(states: [])
|
||||
} else {
|
||||
if let notificationSettings = postbox.peerNotificationSettingsTable.getEffective(notificationPeerId), !notificationSettings.isRemovedFromTotalUnreadCount {
|
||||
} else {
|
||||
initialFilteredValue = (0, false, false)
|
||||
currentFilteredValue = (0, false, false)
|
||||
initialFilteredStates = CombinedPeerReadState(states: [])
|
||||
currentFilteredStates = CombinedPeerReadState(states: [])
|
||||
}
|
||||
}
|
||||
|
||||
if var currentTotalRootUnreadState = totalRootUnreadState {
|
||||
var keptTags: PeerSummaryCounterTags = postbox.seedConfiguration.peerSummaryCounterTags(peer)
|
||||
if let (removedTags, addedTags) = updatedPeerTags[peerId] {
|
||||
keptTags.remove(removedTags)
|
||||
keptTags.remove(addedTags)
|
||||
|
||||
for tag in removedTags {
|
||||
alterTags(¤tTotalRootUnreadState, peerId, tag, { absolute, filtered in
|
||||
var absolute = absolute
|
||||
var filtered = filtered
|
||||
absolute.messageCount -= initialValue.0
|
||||
if initialValue.1 {
|
||||
absolute.chatCount -= 1
|
||||
}
|
||||
if initialValue.2 && initialValue.0 == 0 {
|
||||
absolute.messageCount -= 1
|
||||
}
|
||||
filtered.messageCount -= initialFilteredValue.0
|
||||
if initialFilteredValue.1 {
|
||||
filtered.chatCount -= 1
|
||||
}
|
||||
if initialFilteredValue.2 && initialFilteredValue.0 == 0 {
|
||||
filtered.messageCount -= 1
|
||||
}
|
||||
return (absolute, filtered)
|
||||
})
|
||||
}
|
||||
for tag in addedTags {
|
||||
alterTags(¤tTotalRootUnreadState, peerId, tag, { absolute, filtered in
|
||||
var absolute = absolute
|
||||
var filtered = filtered
|
||||
absolute.messageCount += currentValue.0
|
||||
if currentValue.2 && currentValue.0 == 0 {
|
||||
absolute.messageCount += 1
|
||||
}
|
||||
if currentValue.1 {
|
||||
absolute.chatCount += 1
|
||||
}
|
||||
filtered.messageCount += currentFilteredValue.0
|
||||
if currentFilteredValue.1 {
|
||||
filtered.chatCount += 1
|
||||
}
|
||||
if currentFilteredValue.2 && currentFilteredValue.0 == 0 {
|
||||
filtered.messageCount += 1
|
||||
}
|
||||
return (absolute, filtered)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for tag in keptTags {
|
||||
alterTags(¤tTotalRootUnreadState, peerId, tag, { absolute, filtered in
|
||||
var absolute = absolute
|
||||
var filtered = filtered
|
||||
|
||||
let chatDifference: Int32
|
||||
if initialValue.1 != currentValue.1 {
|
||||
chatDifference = initialValue.1 ? -1 : 1
|
||||
} else {
|
||||
chatDifference = 0
|
||||
}
|
||||
|
||||
let currentUnreadMark: Int32 = currentValue.2 ? 1 : 0
|
||||
let initialUnreadMark: Int32 = initialValue.2 ? 1 : 0
|
||||
let messageDifference = max(currentValue.0, currentUnreadMark) - max(initialValue.0, initialUnreadMark)
|
||||
|
||||
let chatFilteredDifference: Int32
|
||||
if initialFilteredValue.1 != currentFilteredValue.1 {
|
||||
chatFilteredDifference = initialFilteredValue.1 ? -1 : 1
|
||||
} else {
|
||||
chatFilteredDifference = 0
|
||||
}
|
||||
let currentFilteredUnreadMark: Int32 = currentFilteredValue.2 ? 1 : 0
|
||||
let initialFilteredUnreadMark: Int32 = initialFilteredValue.2 ? 1 : 0
|
||||
let messageFilteredDifference = max(currentFilteredValue.0, currentFilteredUnreadMark) - max(initialFilteredValue.0, initialFilteredUnreadMark)
|
||||
|
||||
absolute.messageCount += messageDifference
|
||||
absolute.chatCount += chatDifference
|
||||
filtered.messageCount += messageFilteredDifference
|
||||
filtered.chatCount += chatFilteredDifference
|
||||
|
||||
return (absolute, filtered)
|
||||
})
|
||||
}
|
||||
|
||||
updatedRootState = currentTotalRootUnreadState
|
||||
}
|
||||
|
||||
var namespaces: [MessageId.Namespace] = []
|
||||
for (namespace, _) in initialStates.states {
|
||||
namespaces.append(namespace)
|
||||
}
|
||||
for (namespace, _) in currentStates.states {
|
||||
if !namespaces.contains(namespace) {
|
||||
namespaces.append(namespace)
|
||||
}
|
||||
}
|
||||
|
||||
for namespace in namespaces {
|
||||
if postbox.seedConfiguration.messageNamespacesRequiringGroupStatsValidation.contains(namespace) && addedToGroupPeerIds[peerId] == groupId && removedFromGroupPeerIds[peerId] == nil {
|
||||
postbox.synchronizeGroupMessageStatsTable.set(groupId: groupId, namespace: namespace, needsValidation: true, operations: ¤tUpdatedGroupSummarySynchronizeOperations)
|
||||
} else {
|
||||
var namespaceSummary = summary.namespaces[namespace] ?? PeerGroupUnreadCountersSummary(all: PeerGroupUnreadCounters(messageCount: 0, chatCount: 0))
|
||||
let previousState = initialStates.states.first(where: { $0.0 == namespace })?.1
|
||||
let updatedState = currentStates.states.first(where: { $0.0 == namespace })?.1
|
||||
alterNamespace(summary: &namespaceSummary, previousState: previousState, updatedState: updatedState)
|
||||
summary.namespaces[namespace] = namespaceSummary
|
||||
}
|
||||
}
|
||||
|
||||
updatedTotalUnreadSummaries[groupId] = summary
|
||||
}
|
||||
}
|
||||
|
||||
if let updatedRootState = updatedRootState {
|
||||
if postbox.messageHistoryMetadataTable.getChatListTotalUnreadState() != updatedRootState {
|
||||
postbox.messageHistoryMetadataTable.setChatListTotalUnreadState(updatedRootState)
|
||||
updatedRootUnreadState = updatedRootState
|
||||
}
|
||||
}
|
||||
|
||||
for groupId in updatedTotalUnreadSummaries.keys {
|
||||
if postbox.groupMessageStatsTable.get(groupId: groupId) != updatedTotalUnreadSummaries[groupId]! {
|
||||
postbox.groupMessageStatsTable.set(groupId: groupId, summary: updatedTotalUnreadSummaries[groupId]!)
|
||||
updatedGroupTotalUnreadSummaries[groupId] = updatedTotalUnreadSummaries[groupId]!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
assert(self.updatedPreviousPeerCachedIndices.isEmpty)
|
||||
}
|
||||
|
||||
func debugReindexUnreadCounts(postbox: Postbox) -> (ChatListTotalUnreadState, [PeerGroupId: PeerGroupUnreadCountersCombinedSummary]) {
|
||||
var peerIds: [PeerId] = []
|
||||
self.valueBox.scanInt64(self.table, values: { key, _ in
|
||||
let peerId = PeerId(key)
|
||||
if peerId.namespace != Int32.max {
|
||||
peerIds.append(peerId)
|
||||
}
|
||||
return true
|
||||
})
|
||||
var rootState = ChatListTotalUnreadState(absoluteCounters: [:], filteredCounters: [:])
|
||||
var summaries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary] = [:]
|
||||
for peerId in peerIds {
|
||||
guard let peer = postbox.peerTable.get(peerId) else {
|
||||
continue
|
||||
}
|
||||
guard let combinedState = postbox.readStateTable.getCombinedState(peerId) else {
|
||||
continue
|
||||
}
|
||||
let notificationPeerId: PeerId = peer.associatedPeerId ?? peerId
|
||||
let notificationSettings = postbox.peerNotificationSettingsTable.getEffective(notificationPeerId)
|
||||
let inclusion = self.get(peerId: peerId)
|
||||
if let (groupId, _) = inclusion.includedIndex(peerId: peerId) {
|
||||
if case .root = groupId {
|
||||
let peerMessageCount = combinedState.count
|
||||
|
||||
let summaryTags = postbox.seedConfiguration.peerSummaryCounterTags(peer)
|
||||
for tag in summaryTags {
|
||||
if rootState.absoluteCounters[tag] == nil {
|
||||
rootState.absoluteCounters[tag] = ChatListTotalUnreadCounters(messageCount: 0, chatCount: 0)
|
||||
}
|
||||
var messageCount = rootState.absoluteCounters[tag]!.messageCount
|
||||
messageCount = messageCount &+ peerMessageCount
|
||||
if messageCount < 0 {
|
||||
messageCount = 0
|
||||
}
|
||||
if combinedState.isUnread {
|
||||
rootState.absoluteCounters[tag]!.chatCount += 1
|
||||
}
|
||||
if combinedState.markedUnread {
|
||||
messageCount = max(1, messageCount)
|
||||
}
|
||||
rootState.absoluteCounters[tag]!.messageCount = messageCount
|
||||
}
|
||||
|
||||
if let notificationSettings = notificationSettings, !notificationSettings.isRemovedFromTotalUnreadCount {
|
||||
for tag in summaryTags {
|
||||
if rootState.filteredCounters[tag] == nil {
|
||||
rootState.filteredCounters[tag] = ChatListTotalUnreadCounters(messageCount: 0, chatCount: 0)
|
||||
}
|
||||
var messageCount = rootState.filteredCounters[tag]!.messageCount
|
||||
messageCount = messageCount &+ peerMessageCount
|
||||
if messageCount < 0 {
|
||||
messageCount = 0
|
||||
}
|
||||
if combinedState.isUnread {
|
||||
rootState.filteredCounters[tag]!.chatCount += 1
|
||||
}
|
||||
if combinedState.markedUnread {
|
||||
messageCount = max(1, messageCount)
|
||||
}
|
||||
rootState.filteredCounters[tag]!.messageCount = messageCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (namespace, state) in combinedState.states {
|
||||
if summaries[groupId] == nil {
|
||||
summaries[groupId] = PeerGroupUnreadCountersCombinedSummary(namespaces: [:])
|
||||
}
|
||||
if summaries[groupId]!.namespaces[namespace] == nil {
|
||||
summaries[groupId]!.namespaces[namespace] = PeerGroupUnreadCountersSummary(all: PeerGroupUnreadCounters(messageCount: 0, chatCount: 0))
|
||||
}
|
||||
if state.count > 0 {
|
||||
summaries[groupId]!.namespaces[namespace]!.all.chatCount += 1
|
||||
summaries[groupId]!.namespaces[namespace]!.all.messageCount += state.count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (rootState, summaries)
|
||||
}
|
||||
}
|
||||
783
submodules/Postbox/Postbox/ChatListTable.swift
Normal file
783
submodules/Postbox/Postbox/ChatListTable.swift
Normal file
|
|
@ -0,0 +1,783 @@
|
|||
import Foundation
|
||||
|
||||
enum ChatListOperation {
|
||||
case InsertEntry(ChatListIndex, IntermediateMessage?, CombinedPeerReadState?, PeerChatListEmbeddedInterfaceState?)
|
||||
case InsertHole(ChatListHole)
|
||||
case RemoveEntry([ChatListIndex])
|
||||
case RemoveHoles([ChatListIndex])
|
||||
}
|
||||
|
||||
enum ChatListEntryInfo {
|
||||
case message(ChatListIndex, MessageIndex?)
|
||||
case hole(ChatListHole)
|
||||
|
||||
var index: ChatListIndex {
|
||||
switch self {
|
||||
case let .message(index, _):
|
||||
return index
|
||||
case let .hole(hole):
|
||||
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatListIntermediateEntry {
|
||||
case message(ChatListIndex, IntermediateMessage?, PeerChatListEmbeddedInterfaceState?)
|
||||
case hole(ChatListHole)
|
||||
|
||||
var index: ChatListIndex {
|
||||
switch self {
|
||||
case let .message(index, _, _):
|
||||
return index
|
||||
case let .hole(hole):
|
||||
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatListRelativePosition {
|
||||
case later(than: ChatListIndex?)
|
||||
case earlier(than: ChatListIndex?)
|
||||
}
|
||||
|
||||
private enum ChatListEntryType: Int8 {
|
||||
case message = 1
|
||||
case hole = 2
|
||||
}
|
||||
|
||||
func chatListPinningIndexFromKeyValue(_ value: UInt16) -> UInt16? {
|
||||
if value == 0 {
|
||||
return nil
|
||||
} else {
|
||||
return UInt16.max - 1 - value
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueForChatListPinningIndex(_ index: UInt16?) -> UInt16 {
|
||||
if let index = index {
|
||||
return UInt16.max - 1 - index
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private func extractKey(_ key: ValueBoxKey) -> (groupId: PeerGroupId, pinningIndex: UInt16?, index: MessageIndex, type: Int8) {
|
||||
let groupIdValue = key.getInt32(0)
|
||||
return (
|
||||
groupId: PeerGroupId(rawValue: groupIdValue),
|
||||
pinningIndex: chatListPinningIndexFromKeyValue(key.getUInt16(4)),
|
||||
index: MessageIndex(
|
||||
id: MessageId(
|
||||
peerId: PeerId(key.getInt64(4 + 2 + 4 + 1 + 4)),
|
||||
namespace: Int32(key.getInt8(4 + 2 + 4)),
|
||||
id: key.getInt32(4 + 2 + 4 + 1)
|
||||
),
|
||||
timestamp: key.getInt32(4 + 2)
|
||||
),
|
||||
type: key.getInt8(4 + 2 + 4 + 1 + 4 + 8)
|
||||
)
|
||||
}
|
||||
|
||||
private func readEntry(groupId: PeerGroupId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, key: ValueBoxKey, value: ReadBuffer) -> ChatListIntermediateEntry {
|
||||
let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key)
|
||||
assert(groupId == keyGroupId)
|
||||
let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex)
|
||||
if type == ChatListEntryType.message.rawValue {
|
||||
var message: IntermediateMessage?
|
||||
if value.length != 0 {
|
||||
var idNamespace: Int32 = 0
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
var idId: Int32 = 0
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
var indexTimestamp: Int32 = 0
|
||||
value.read(&indexTimestamp, offset: 0, length: 4)
|
||||
|
||||
message = messageHistoryTable.getMessage(MessageIndex(id: MessageId(peerId: index.messageIndex.id.peerId, namespace: idNamespace, id: idId), timestamp: indexTimestamp))
|
||||
}
|
||||
return .message(index, message, peerChatInterfaceStateTable.get(index.messageIndex.id.peerId)?.chatListEmbeddedState)
|
||||
} else if type == ChatListEntryType.hole.rawValue {
|
||||
return .hole(ChatListHole(index: index.messageIndex))
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private func addOperation(_ operation: ChatListOperation, groupId: PeerGroupId, to operations: inout [PeerGroupId: [ChatListOperation]]) {
|
||||
if operations[groupId] == nil {
|
||||
operations[groupId] = []
|
||||
}
|
||||
operations[groupId]!.append(operation)
|
||||
}
|
||||
|
||||
/*
|
||||
dialog.unread_mark ? 1 : 0,
|
||||
dialog.peer.channel_id || dialog.peer.chat_id || dialog.peer.user_id,
|
||||
dialog.top_message.id,
|
||||
top_message.edit_date || top_message.date,
|
||||
dialog.read_inbox_max_id,
|
||||
dialog.read_outbox_max_id,
|
||||
dialog.unread_count,
|
||||
dialog.unread_mentions_count,
|
||||
draft.draft.date || 0
|
||||
|
||||
*/
|
||||
|
||||
public enum ChatListNamespaceEntry {
|
||||
case peer(index: ChatListIndex, readState: PeerReadState?, topMessageAttributes: [MessageAttribute], tagSummary: MessageHistoryTagNamespaceSummary?, interfaceState: PeerChatInterfaceState?)
|
||||
case hole(MessageIndex)
|
||||
|
||||
public var index: ChatListIndex {
|
||||
switch self {
|
||||
case let .peer(peer):
|
||||
return peer.index
|
||||
case let .hole(index):
|
||||
return ChatListIndex(pinningIndex: nil, messageIndex: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatListTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
let indexTable: ChatListIndexTable
|
||||
let emptyMemoryBuffer = MemoryBuffer()
|
||||
let metadataTable: MessageHistoryMetadataTable
|
||||
let seedConfiguration: SeedConfiguration
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, indexTable: ChatListIndexTable, metadataTable: MessageHistoryMetadataTable, seedConfiguration: SeedConfiguration) {
|
||||
self.indexTable = indexTable
|
||||
self.metadataTable = metadataTable
|
||||
self.seedConfiguration = seedConfiguration
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(groupId: PeerGroupId, index: ChatListIndex, type: ChatListEntryType) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4 + 2 + 4 + 1 + 4 + 8 + 1)
|
||||
key.setInt32(0, value: groupId.rawValue)
|
||||
key.setUInt16(4, value: keyValueForChatListPinningIndex(index.pinningIndex))
|
||||
key.setInt32(4 + 2, value: index.messageIndex.timestamp)
|
||||
key.setInt8(4 + 2 + 4, value: Int8(index.messageIndex.id.namespace))
|
||||
key.setInt32(4 + 2 + 4 + 1, value: index.messageIndex.id.id)
|
||||
key.setInt64(4 + 2 + 4 + 1 + 4, value: index.messageIndex.id.peerId.toInt64())
|
||||
key.setInt8(4 + 2 + 4 + 1 + 4 + 8, value: type.rawValue)
|
||||
return key
|
||||
}
|
||||
|
||||
private func lowerBound(groupId: PeerGroupId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4 + 2 + 4)
|
||||
key.setInt32(0, value: groupId.rawValue)
|
||||
key.setUInt16(4, value: 0)
|
||||
key.setInt32(4 + 2, value: 0)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(groupId: PeerGroupId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4 + 2 + 4)
|
||||
key.setInt32(0, value: groupId.rawValue)
|
||||
key.setUInt16(4, value: UInt16.max)
|
||||
key.setInt32(4 + 2, value: Int32.max)
|
||||
return key
|
||||
}
|
||||
|
||||
private func ensureInitialized(groupId: PeerGroupId) {
|
||||
if !self.metadataTable.isInitializedChatList(groupId: groupId) {
|
||||
let hole: ChatListHole?
|
||||
switch groupId {
|
||||
case .root:
|
||||
hole = self.seedConfiguration.initializeChatListWithHole.topLevel
|
||||
case .group:
|
||||
hole = self.seedConfiguration.initializeChatListWithHole.groups
|
||||
}
|
||||
|
||||
if let hole = hole {
|
||||
self.justInsertHole(groupId: groupId, hole: hole)
|
||||
}
|
||||
self.metadataTable.setInitializedChatList(groupId: groupId)
|
||||
}
|
||||
}
|
||||
|
||||
func updateInclusion(peerId: PeerId, updatedChatListInclusions: inout [PeerId: PeerChatListInclusion], _ f: (PeerChatListInclusion) -> PeerChatListInclusion) {
|
||||
let currentInclusion: PeerChatListInclusion
|
||||
if let updated = updatedChatListInclusions[peerId] {
|
||||
currentInclusion = updated
|
||||
} else {
|
||||
currentInclusion = self.indexTable.get(peerId: peerId).inclusion
|
||||
}
|
||||
let updatedInclusion = f(currentInclusion)
|
||||
if currentInclusion != updatedInclusion {
|
||||
updatedChatListInclusions[peerId] = updatedInclusion
|
||||
}
|
||||
}
|
||||
|
||||
func getPinnedItemIds(groupId: PeerGroupId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable) -> [(id: PinnedItemId, rank: Int)] {
|
||||
var itemIds: [(id: PinnedItemId, rank: Int)] = []
|
||||
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: UInt16.max - 1, messageIndex: MessageIndex.absoluteUpperBound()), type: .message).successor, values: { key, value in
|
||||
let keyIndex = extractKey(key)
|
||||
let entry = readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value)
|
||||
switch entry {
|
||||
case let .message(index, _, _):
|
||||
itemIds.append((.peer(index.messageIndex.id.peerId), Int(keyIndex.pinningIndex ?? 0)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return itemIds
|
||||
}
|
||||
|
||||
func setPinnedItemIds(groupId: PeerGroupId, itemIds: [PinnedItemId], updatedChatListInclusions: inout [PeerId: PeerChatListInclusion], messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable) {
|
||||
let updatedIds = Set(itemIds)
|
||||
for (itemId, _) in self.getPinnedItemIds(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable) {
|
||||
if !updatedIds.contains(itemId) {
|
||||
switch itemId {
|
||||
case let .peer(peerId):
|
||||
self.updateInclusion(peerId: peerId, updatedChatListInclusions: &updatedChatListInclusions, { inclusion in
|
||||
return inclusion.withoutPinningIndex()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0 ..< itemIds.count {
|
||||
switch itemIds[i] {
|
||||
case let .peer(peerId):
|
||||
self.updateInclusion(peerId: peerId, updatedChatListInclusions: &updatedChatListInclusions, { inclusion in
|
||||
return inclusion.withPinningIndex(groupId: groupId, pinningIndex: UInt16(i))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPeerChatListIndex(peerId: PeerId) -> (PeerGroupId, ChatListIndex)? {
|
||||
if let (groupId, index) = self.indexTable.get(peerId: peerId).includedIndex(peerId: peerId) {
|
||||
return (groupId, index)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getUnreadChatListPeerIds(postbox: Postbox, groupId: PeerGroupId) -> [PeerId] {
|
||||
var result: [PeerId] = []
|
||||
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in
|
||||
let (_, _, messageIndex, _) = extractKey(key)
|
||||
if let state = postbox.readStateTable.getCombinedState(messageIndex.id.peerId), state.isUnread {
|
||||
result.append(messageIndex.id.peerId)
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
private func topGroupMessageIndex(groupId: PeerGroupId) -> MessageIndex? {
|
||||
var result: MessageIndex?
|
||||
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in
|
||||
result = extractKey(key).index
|
||||
return true
|
||||
}, limit: 1)
|
||||
return result
|
||||
}
|
||||
|
||||
func replay(historyOperationsByPeerId: [PeerId: [MessageHistoryOperation]], updatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?], updatedChatListInclusions: [PeerId: PeerChatListInclusion], messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, operations: inout [PeerGroupId: [ChatListOperation]]) {
|
||||
var changedPeerIds = Set<PeerId>()
|
||||
for peerId in historyOperationsByPeerId.keys {
|
||||
changedPeerIds.insert(peerId)
|
||||
}
|
||||
for peerId in updatedPeerChatListEmbeddedStates.keys {
|
||||
changedPeerIds.insert(peerId)
|
||||
}
|
||||
for peerId in updatedChatListInclusions.keys {
|
||||
changedPeerIds.insert(peerId)
|
||||
}
|
||||
|
||||
self.ensureInitialized(groupId: .root)
|
||||
|
||||
for peerId in changedPeerIds {
|
||||
let currentGroupAndIndex = self.indexTable.get(peerId: peerId).includedIndex(peerId: peerId)
|
||||
if let (groupId, _) = currentGroupAndIndex {
|
||||
self.ensureInitialized(groupId: groupId)
|
||||
}
|
||||
|
||||
let topMessage = messageHistoryTable.topMessage(peerId)
|
||||
let embeddedChatState = peerChatInterfaceStateTable.get(peerId)?.chatListEmbeddedState
|
||||
|
||||
let rawTopMessageIndex: MessageIndex?
|
||||
let topMessageIndex: MessageIndex?
|
||||
if let topMessage = topMessage {
|
||||
var updatedTimestamp = topMessage.timestamp
|
||||
rawTopMessageIndex = MessageIndex(id: topMessage.id, timestamp: topMessage.timestamp)
|
||||
if let embeddedChatState = embeddedChatState {
|
||||
updatedTimestamp = max(updatedTimestamp, embeddedChatState.timestamp)
|
||||
}
|
||||
topMessageIndex = MessageIndex(id: topMessage.id, timestamp: updatedTimestamp)
|
||||
} else if let embeddedChatState = embeddedChatState, embeddedChatState.timestamp != 0 {
|
||||
topMessageIndex = MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 1), timestamp: embeddedChatState.timestamp)
|
||||
rawTopMessageIndex = nil
|
||||
} else {
|
||||
topMessageIndex = nil
|
||||
rawTopMessageIndex = nil
|
||||
}
|
||||
|
||||
var updatedIndex = self.indexTable.setTopMessageIndex(peerId: peerId, index: topMessageIndex)
|
||||
if let updatedInclusion = updatedChatListInclusions[peerId] {
|
||||
updatedIndex = self.indexTable.setInclusion(peerId: peerId, inclusion: updatedInclusion)
|
||||
}
|
||||
|
||||
if let (updatedGroupId, updatedOrderingIndex) = updatedIndex.includedIndex(peerId: peerId) {
|
||||
if let (currentGroupId, currentOrderingIndex) = currentGroupAndIndex {
|
||||
if currentGroupId != updatedGroupId || currentOrderingIndex != updatedOrderingIndex {
|
||||
self.justRemoveMessageIndex(groupId: currentGroupId, index: currentOrderingIndex)
|
||||
}
|
||||
addOperation(.RemoveEntry([currentOrderingIndex]), groupId: currentGroupId, to: &operations)
|
||||
}
|
||||
self.justInsertIndex(groupId: updatedGroupId, index: updatedOrderingIndex, topMessageIndex: rawTopMessageIndex)
|
||||
addOperation(.InsertEntry(updatedOrderingIndex, topMessage, messageHistoryTable.readStateTable.getCombinedState(peerId), embeddedChatState), groupId: updatedGroupId, to: &operations)
|
||||
} else {
|
||||
if let (currentGroupId, currentOrderingIndex) = currentGroupAndIndex {
|
||||
self.justRemoveMessageIndex(groupId: currentGroupId, index: currentOrderingIndex)
|
||||
addOperation(.RemoveEntry([currentOrderingIndex]), groupId: currentGroupId, to: &operations)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addHole(groupId: PeerGroupId, hole: ChatListHole, operations: inout [PeerGroupId: [ChatListOperation]]) {
|
||||
self.ensureInitialized(groupId: groupId)
|
||||
|
||||
if self.valueBox.get(self.table, key: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: nil, messageIndex: hole.index), type: .hole)) == nil {
|
||||
self.justInsertHole(groupId: groupId, hole: hole)
|
||||
addOperation(.InsertHole(hole), groupId: groupId, to: &operations)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceHole(groupId: PeerGroupId, index: MessageIndex, hole: ChatListHole?, operations: inout [PeerGroupId: [ChatListOperation]]) {
|
||||
self.ensureInitialized(groupId: groupId)
|
||||
|
||||
if self.valueBox.get(self.table, key: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: nil, messageIndex: index), type: .hole)) != nil {
|
||||
if let hole = hole {
|
||||
if hole.index != index {
|
||||
self.justRemoveHole(groupId: groupId, index: index)
|
||||
self.justInsertHole(groupId: groupId, hole: hole)
|
||||
addOperation(.RemoveHoles([ChatListIndex(pinningIndex: nil, messageIndex: index)]), groupId: groupId, to: &operations)
|
||||
addOperation(.InsertHole(hole), groupId: groupId, to: &operations)
|
||||
}
|
||||
} else{
|
||||
self.justRemoveHole(groupId: groupId, index: index)
|
||||
addOperation(.RemoveHoles([ChatListIndex(pinningIndex: nil, messageIndex: index)]), groupId: groupId, to: &operations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func justInsertIndex(groupId: PeerGroupId, index: ChatListIndex, topMessageIndex: MessageIndex?) {
|
||||
let buffer = WriteBuffer()
|
||||
if let topMessageIndex = topMessageIndex {
|
||||
var idNamespace = topMessageIndex.id.namespace
|
||||
buffer.write(&idNamespace, offset: 0, length: 4)
|
||||
var idId = topMessageIndex.id.id
|
||||
buffer.write(&idId, offset: 0, length: 4)
|
||||
var indexTimestamp = topMessageIndex.timestamp
|
||||
buffer.write(&indexTimestamp, offset: 0, length: 4)
|
||||
}
|
||||
self.valueBox.set(self.table, key: self.key(groupId: groupId, index: index, type: .message), value: buffer)
|
||||
}
|
||||
|
||||
private func justRemoveMessageIndex(groupId: PeerGroupId, index: ChatListIndex) {
|
||||
self.valueBox.remove(self.table, key: self.key(groupId: groupId, index: index, type: .message), secure: false)
|
||||
}
|
||||
|
||||
private func justInsertHole(groupId: PeerGroupId, hole: ChatListHole) {
|
||||
self.valueBox.set(self.table, key: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: nil, messageIndex: hole.index), type: .hole), value: self.emptyMemoryBuffer)
|
||||
}
|
||||
|
||||
private func justRemoveHole(groupId: PeerGroupId, index: MessageIndex) {
|
||||
self.valueBox.remove(self.table, key: self.key(groupId: groupId, index: ChatListIndex(pinningIndex: nil, messageIndex: index), type: .hole), secure: false)
|
||||
}
|
||||
|
||||
func entriesAround(groupId: PeerGroupId, index: ChatListIndex, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> (entries: [ChatListIntermediateEntry], lower: ChatListIntermediateEntry?, upper: ChatListIntermediateEntry?) {
|
||||
self.ensureInitialized(groupId: groupId)
|
||||
|
||||
var lowerEntries: [ChatListIntermediateEntry] = []
|
||||
var upperEntries: [ChatListIntermediateEntry] = []
|
||||
var lower: ChatListIntermediateEntry?
|
||||
var upper: ChatListIntermediateEntry?
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(groupId: groupId, index: index, type: .message), end: self.lowerBound(groupId: groupId), values: { key, value in
|
||||
lowerEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
|
||||
return true
|
||||
}, limit: count / 2 + 1)
|
||||
if lowerEntries.count >= count / 2 + 1 {
|
||||
lower = lowerEntries.last
|
||||
lowerEntries.removeLast()
|
||||
}
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(groupId: groupId, index: index, type: .message).predecessor, end: self.upperBound(groupId: groupId), values: { key, value in
|
||||
upperEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
|
||||
return true
|
||||
}, limit: count - lowerEntries.count + 1)
|
||||
if upperEntries.count >= count - lowerEntries.count + 1 {
|
||||
upper = upperEntries.last
|
||||
upperEntries.removeLast()
|
||||
}
|
||||
|
||||
if lowerEntries.count != 0 && lowerEntries.count + upperEntries.count < count {
|
||||
var additionalLowerEntries: [ChatListIntermediateEntry] = []
|
||||
let startEntryType: ChatListEntryType
|
||||
switch lowerEntries.last! {
|
||||
case .message:
|
||||
startEntryType = .message
|
||||
case .hole:
|
||||
startEntryType = .hole
|
||||
/*case .groupReference:
|
||||
startEntryType = .groupReference*/
|
||||
}
|
||||
self.valueBox.range(self.table, start: self.key(groupId: groupId, index: lowerEntries.last!.index, type: startEntryType), end: self.lowerBound(groupId: groupId), values: { key, value in
|
||||
additionalLowerEntries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
|
||||
return true
|
||||
}, limit: count - lowerEntries.count - upperEntries.count + 1)
|
||||
if additionalLowerEntries.count >= count - lowerEntries.count + upperEntries.count + 1 {
|
||||
lower = additionalLowerEntries.last
|
||||
additionalLowerEntries.removeLast()
|
||||
}
|
||||
lowerEntries.append(contentsOf: additionalLowerEntries)
|
||||
}
|
||||
|
||||
var entries: [ChatListIntermediateEntry] = []
|
||||
entries.append(contentsOf: lowerEntries.reversed())
|
||||
entries.append(contentsOf: upperEntries)
|
||||
return (entries: entries, lower: lower, upper: upper)
|
||||
}
|
||||
|
||||
func topPeerIds(groupId: PeerGroupId, count: Int) -> [PeerId] {
|
||||
var peerIds: [PeerId] = []
|
||||
while true {
|
||||
var completed = true
|
||||
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in
|
||||
let (keyGroupId, _, messageIndex, type) = extractKey(key)
|
||||
assert(groupId == keyGroupId)
|
||||
|
||||
if type == ChatListEntryType.message.rawValue {
|
||||
peerIds.append(messageIndex.id.peerId)
|
||||
}
|
||||
completed = false
|
||||
return true
|
||||
}, limit: count)
|
||||
if completed || peerIds.count >= count {
|
||||
break
|
||||
}
|
||||
}
|
||||
return peerIds
|
||||
}
|
||||
|
||||
func topMessageIndices(groupId: PeerGroupId, count: Int) -> [ChatListIndex] {
|
||||
var indices: [ChatListIndex] = []
|
||||
var startKey = self.upperBound(groupId: groupId)
|
||||
while true {
|
||||
var completed = true
|
||||
self.valueBox.range(self.table, start: startKey, end: self.lowerBound(groupId: groupId), keys: { key in
|
||||
startKey = key
|
||||
|
||||
let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key)
|
||||
assert(groupId == keyGroupId)
|
||||
|
||||
let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex)
|
||||
|
||||
if type == ChatListEntryType.message.rawValue {
|
||||
indices.append(index)
|
||||
}
|
||||
completed = false
|
||||
return true
|
||||
}, limit: indices.isEmpty ? count : 1)
|
||||
if completed || indices.count >= count {
|
||||
break
|
||||
}
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
func existingGroups() -> [PeerGroupId] {
|
||||
var result: [PeerGroupId] = []
|
||||
var lowerBound = self.lowerBound(groupId: PeerGroupId(rawValue: 1))
|
||||
let upperBound = self.upperBound(groupId: PeerGroupId(rawValue: Int32.max - 1))
|
||||
while true {
|
||||
var groupId: PeerGroupId?
|
||||
self.valueBox.range(self.table, start: lowerBound, end: upperBound, keys: { key in
|
||||
groupId = extractKey(key).groupId
|
||||
return false
|
||||
}, limit: 1)
|
||||
if let groupId = groupId {
|
||||
result.append(groupId)
|
||||
lowerBound = self.lowerBound(groupId: PeerGroupId(rawValue: groupId.rawValue + 1))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func earlierEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> [ChatListIntermediateEntry] {
|
||||
self.ensureInitialized(groupId: groupId)
|
||||
|
||||
var entries: [ChatListIntermediateEntry] = []
|
||||
let key: ValueBoxKey
|
||||
if let (index, message) = index {
|
||||
key = self.key(groupId: groupId, index: index, type: message ? .message : .hole)
|
||||
} else {
|
||||
key = self.upperBound(groupId: groupId)
|
||||
}
|
||||
|
||||
self.valueBox.range(self.table, start: key, end: self.lowerBound(groupId: groupId), values: { key, value in
|
||||
entries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
|
||||
return true
|
||||
}, limit: count)
|
||||
return entries
|
||||
}
|
||||
|
||||
func earlierEntryInfos(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> [ChatListEntryInfo] {
|
||||
self.ensureInitialized(groupId: groupId)
|
||||
|
||||
var entries: [ChatListEntryInfo] = []
|
||||
let key: ValueBoxKey
|
||||
if let (index, message) = index {
|
||||
key = self.key(groupId: groupId, index: index, type: message ? .message : .hole)
|
||||
} else {
|
||||
key = self.upperBound(groupId: groupId)
|
||||
}
|
||||
|
||||
self.valueBox.range(self.table, start: key, end: self.lowerBound(groupId: groupId), values: { key, value in
|
||||
let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key)
|
||||
assert(groupId == keyGroupId)
|
||||
|
||||
let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex)
|
||||
if type == ChatListEntryType.message.rawValue {
|
||||
var messageIndex: MessageIndex?
|
||||
if value.length != 0 {
|
||||
var idNamespace: Int32 = 0
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
var idId: Int32 = 0
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
var indexTimestamp: Int32 = 0
|
||||
value.read(&indexTimestamp, offset: 0, length: 4)
|
||||
messageIndex = MessageIndex(id: MessageId(peerId: index.messageIndex.id.peerId, namespace: idNamespace, id: idId), timestamp: indexTimestamp)
|
||||
}
|
||||
entries.append(.message(index, messageIndex))
|
||||
} else if type == ChatListEntryType.hole.rawValue {
|
||||
entries.append(.hole(ChatListHole(index: index.messageIndex)))
|
||||
}
|
||||
return true
|
||||
}, limit: count)
|
||||
return entries
|
||||
}
|
||||
|
||||
func laterEntries(groupId: PeerGroupId, index: (ChatListIndex, Bool)?, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, count: Int) -> [ChatListIntermediateEntry] {
|
||||
self.ensureInitialized(groupId: groupId)
|
||||
|
||||
var entries: [ChatListIntermediateEntry] = []
|
||||
let key: ValueBoxKey
|
||||
if let (index, message) = index {
|
||||
key = self.key(groupId: groupId, index: index, type: message ? .message : .hole)
|
||||
} else {
|
||||
key = self.lowerBound(groupId: groupId)
|
||||
}
|
||||
|
||||
self.valueBox.range(self.table, start: key, end: self.upperBound(groupId: groupId), values: { key, value in
|
||||
entries.append(readEntry(groupId: groupId, messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, key: key, value: value))
|
||||
return true
|
||||
}, limit: count)
|
||||
return entries
|
||||
}
|
||||
|
||||
func getStandalone(peerId: PeerId, messageHistoryTable: MessageHistoryTable) -> ChatListIntermediateEntry? {
|
||||
let index = self.indexTable.get(peerId: peerId)
|
||||
switch index.inclusion {
|
||||
case .ifHasMessagesOrOneOf:
|
||||
return nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let topMessageIndex = index.topMessageIndex {
|
||||
if let message = messageHistoryTable.getMessage(topMessageIndex) {
|
||||
return ChatListIntermediateEntry.message(ChatListIndex(pinningIndex: nil, messageIndex: topMessageIndex), message, nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func allEntries(groupId: PeerGroupId) -> [ChatListEntryInfo] {
|
||||
var entries: [ChatListEntryInfo] = []
|
||||
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), values: { key, value in
|
||||
let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key)
|
||||
assert(groupId == keyGroupId)
|
||||
|
||||
let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex)
|
||||
if type == ChatListEntryType.message.rawValue {
|
||||
var messageIndex: MessageIndex?
|
||||
if value.length != 0 {
|
||||
var idNamespace: Int32 = 0
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
var idId: Int32 = 0
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
var indexTimestamp: Int32 = 0
|
||||
value.read(&indexTimestamp, offset: 0, length: 4)
|
||||
messageIndex = MessageIndex(id: MessageId(peerId: index.messageIndex.id.peerId, namespace: idNamespace, id: idId), timestamp: indexTimestamp)
|
||||
}
|
||||
entries.append(.message(index, messageIndex))
|
||||
} else if type == ChatListEntryType.hole.rawValue {
|
||||
entries.append(.hole(ChatListHole(index: index.messageIndex)))
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return entries
|
||||
}
|
||||
|
||||
func entriesInRange(groupId: PeerGroupId, upperBound: ChatListIndex, lowerBound: ChatListIndex) -> [ChatListEntryInfo] {
|
||||
var entries: [ChatListEntryInfo] = []
|
||||
let upperBoundKey: ValueBoxKey
|
||||
if upperBound.messageIndex.timestamp == Int32.max {
|
||||
upperBoundKey = self.upperBound(groupId: groupId)
|
||||
} else {
|
||||
upperBoundKey = self.key(groupId: groupId, index: upperBound, type: .message).successor
|
||||
}
|
||||
self.valueBox.range(self.table, start: upperBoundKey, end: self.key(groupId: groupId, index: lowerBound, type: .message).predecessor, values: { key, value in
|
||||
let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key)
|
||||
assert(groupId == keyGroupId)
|
||||
|
||||
let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex)
|
||||
if type == ChatListEntryType.message.rawValue {
|
||||
var messageIndex: MessageIndex?
|
||||
if value.length != 0 {
|
||||
var idNamespace: Int32 = 0
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
var idId: Int32 = 0
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
var indexTimestamp: Int32 = 0
|
||||
value.read(&indexTimestamp, offset: 0, length: 4)
|
||||
messageIndex = MessageIndex(id: MessageId(peerId: index.messageIndex.id.peerId, namespace: idNamespace, id: idId), timestamp: indexTimestamp)
|
||||
}
|
||||
entries.append(.message(index, messageIndex))
|
||||
} else if type == ChatListEntryType.hole.rawValue {
|
||||
entries.append(.hole(ChatListHole(index: index.messageIndex)))
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return entries
|
||||
}
|
||||
|
||||
func getRelativeUnreadChatListIndex(postbox: Postbox, filtered: Bool, position: ChatListRelativePosition, groupId: PeerGroupId) -> ChatListIndex? {
|
||||
var result: ChatListIndex?
|
||||
|
||||
let lower: ValueBoxKey
|
||||
let upper: ValueBoxKey
|
||||
|
||||
switch position {
|
||||
case let .earlier(index):
|
||||
upper = self.upperBound(groupId: groupId)
|
||||
if let index = index {
|
||||
lower = self.key(groupId: groupId, index: index, type: .message)
|
||||
} else {
|
||||
lower = self.lowerBound(groupId: groupId)
|
||||
}
|
||||
case let .later(index):
|
||||
upper = self.lowerBound(groupId: groupId)
|
||||
if let index = index {
|
||||
lower = self.key(groupId: groupId, index: index, type: .message)
|
||||
} else {
|
||||
lower = self.upperBound(groupId: groupId)
|
||||
}
|
||||
}
|
||||
|
||||
self.valueBox.range(self.table, start: lower, end: upper, values: { key, value in
|
||||
let (keyGroupId, pinningIndex, messageIndex, type) = extractKey(key)
|
||||
assert(groupId == keyGroupId)
|
||||
|
||||
let index = ChatListIndex(pinningIndex: pinningIndex, messageIndex: messageIndex)
|
||||
if type == ChatListEntryType.message.rawValue {
|
||||
let peerId = index.messageIndex.id.peerId
|
||||
if let readState = postbox.readStateTable.getCombinedState(peerId), readState.isUnread {
|
||||
if filtered {
|
||||
var notificationSettings: PeerNotificationSettings?
|
||||
if let peer = postbox.peerTable.get(peerId) {
|
||||
if let notificationSettingsPeerId = peer.notificationSettingsPeerId {
|
||||
notificationSettings = postbox.peerNotificationSettingsTable.getEffective(notificationSettingsPeerId)
|
||||
} else {
|
||||
notificationSettings = postbox.peerNotificationSettingsTable.getEffective(peerId)
|
||||
}
|
||||
}
|
||||
if let notificationSettings = notificationSettings, !notificationSettings.isRemovedFromTotalUnreadCount {
|
||||
result = index
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
result = index
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
func debugList(groupId: PeerGroupId, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable) -> [ChatListIntermediateEntry] {
|
||||
return self.laterEntries(groupId: groupId, index: (ChatListIndex.absoluteLowerBound, true), messageHistoryTable: messageHistoryTable, peerChatInterfaceStateTable: peerChatInterfaceStateTable, count: 1000)
|
||||
}
|
||||
|
||||
func getNamespaceEntries(groupId: PeerGroupId, namespace: MessageId.Namespace, summaryTag: MessageTags?, messageIndexTable: MessageHistoryIndexTable, messageHistoryTable: MessageHistoryTable, peerChatInterfaceStateTable: PeerChatInterfaceStateTable, readStateTable: MessageHistoryReadStateTable, summaryTable: MessageHistoryTagsSummaryTable) -> [ChatListNamespaceEntry] {
|
||||
var result: [ChatListNamespaceEntry] = []
|
||||
self.valueBox.range(self.table, start: self.upperBound(groupId: groupId), end: self.lowerBound(groupId: groupId), keys: { key in
|
||||
let keyComponents = extractKey(key)
|
||||
if keyComponents.type == ChatListEntryType.hole.rawValue {
|
||||
if keyComponents.index.id.namespace == namespace {
|
||||
result.append(.hole(keyComponents.index))
|
||||
}
|
||||
} else {
|
||||
var topMessage: IntermediateMessage?
|
||||
var peerIndex: ChatListIndex?
|
||||
if let pinningIndex = keyComponents.pinningIndex {
|
||||
if keyComponents.index.id.namespace == namespace {
|
||||
peerIndex = ChatListIndex(pinningIndex: pinningIndex, messageIndex: keyComponents.index)
|
||||
}
|
||||
} else if keyComponents.index.id.namespace == namespace {
|
||||
peerIndex = ChatListIndex(pinningIndex: nil, messageIndex: keyComponents.index)
|
||||
} else {
|
||||
if let index = messageIndexTable.top(keyComponents.index.id.peerId, namespace: namespace) {
|
||||
peerIndex = ChatListIndex(pinningIndex: nil, messageIndex: index)
|
||||
topMessage = messageHistoryTable.getMessage(index)
|
||||
}
|
||||
}
|
||||
if topMessage == nil {
|
||||
if let index = messageIndexTable.top(keyComponents.index.id.peerId, namespace: namespace) {
|
||||
topMessage = messageHistoryTable.getMessage(index)
|
||||
}
|
||||
}
|
||||
if let peerIndex = peerIndex {
|
||||
var readState: PeerReadState?
|
||||
if let combinedState = readStateTable.getCombinedState(peerIndex.messageIndex.id.peerId) {
|
||||
for item in combinedState.states {
|
||||
if item.0 == namespace {
|
||||
readState = item.1
|
||||
}
|
||||
}
|
||||
}
|
||||
var tagSummary: MessageHistoryTagNamespaceSummary?
|
||||
if let summaryTag = summaryTag {
|
||||
tagSummary = summaryTable.get(MessageHistoryTagsSummaryKey(tag: summaryTag, peerId: peerIndex.messageIndex.id.peerId, namespace: namespace))
|
||||
}
|
||||
var topMessageAttributes: [MessageAttribute] = []
|
||||
if let topMessage = topMessage {
|
||||
topMessageAttributes = MessageHistoryTable.renderMessageAttributes(topMessage)
|
||||
}
|
||||
result.append(.peer(index: peerIndex, readState: readState, topMessageAttributes: topMessageAttributes, tagSummary: tagSummary, interfaceState: peerChatInterfaceStateTable.get(peerIndex.messageIndex.id.peerId)))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return result.sorted(by: { lhs, rhs in
|
||||
return lhs.index > rhs.index
|
||||
})
|
||||
}
|
||||
}
|
||||
930
submodules/Postbox/Postbox/ChatListView.swift
Normal file
930
submodules/Postbox/Postbox/ChatListView.swift
Normal file
|
|
@ -0,0 +1,930 @@
|
|||
import Foundation
|
||||
|
||||
public struct ChatListEntryMessageTagSummaryComponent {
|
||||
public let tag: MessageTags
|
||||
public let namespace: MessageId.Namespace
|
||||
|
||||
public init(tag: MessageTags, namespace: MessageId.Namespace) {
|
||||
self.tag = tag
|
||||
self.namespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListEntryPendingMessageActionsSummaryComponent {
|
||||
public let type: PendingMessageActionType
|
||||
public let namespace: MessageId.Namespace
|
||||
|
||||
public init(type: PendingMessageActionType, namespace: MessageId.Namespace) {
|
||||
self.type = type
|
||||
self.namespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListEntrySummaryComponents {
|
||||
public let tagSummary: ChatListEntryMessageTagSummaryComponent?
|
||||
public let actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent?
|
||||
|
||||
public init(tagSummary: ChatListEntryMessageTagSummaryComponent? = nil, actionsSummary: ChatListEntryPendingMessageActionsSummaryComponent? = nil) {
|
||||
self.tagSummary = tagSummary
|
||||
self.actionsSummary = actionsSummary
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListMessageTagSummaryInfo: Equatable {
|
||||
public let tagSummaryCount: Int32?
|
||||
public let actionsSummaryCount: Int32?
|
||||
|
||||
public init(tagSummaryCount: Int32? = nil, actionsSummaryCount: Int32? = nil) {
|
||||
self.tagSummaryCount = tagSummaryCount
|
||||
self.actionsSummaryCount = actionsSummaryCount
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatListMessageTagSummaryInfo, rhs: ChatListMessageTagSummaryInfo) -> Bool {
|
||||
return lhs.tagSummaryCount == rhs.tagSummaryCount && lhs.actionsSummaryCount == rhs.actionsSummaryCount
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatListGroupReferencePeer: Equatable {
|
||||
public let peer: RenderedPeer
|
||||
public let isUnread: Bool
|
||||
|
||||
init(peer: RenderedPeer, isUnread: Bool) {
|
||||
self.peer = peer
|
||||
self.isUnread = isUnread
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatListGroupReferencePeer, rhs: ChatListGroupReferencePeer) -> Bool {
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.isUnread != rhs.isUnread {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListGroupReferenceEntry: Equatable {
|
||||
public let groupId: PeerGroupId
|
||||
public let message: Message?
|
||||
public let renderedPeers: [ChatListGroupReferencePeer]
|
||||
public let unreadState: PeerGroupUnreadCountersCombinedSummary
|
||||
|
||||
public static func ==(lhs: ChatListGroupReferenceEntry, rhs: ChatListGroupReferenceEntry) -> Bool {
|
||||
if lhs.groupId != rhs.groupId {
|
||||
return false
|
||||
}
|
||||
if lhs.unreadState != rhs.unreadState {
|
||||
return false
|
||||
}
|
||||
if lhs.message?.stableVersion != rhs.message?.stableVersion {
|
||||
return false
|
||||
}
|
||||
if lhs.renderedPeers != rhs.renderedPeers {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatListEntry: Comparable {
|
||||
case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo)
|
||||
case HoleEntry(ChatListHole)
|
||||
|
||||
public var index: ChatListIndex {
|
||||
switch self {
|
||||
case let .MessageEntry(index, _, _, _, _, _, _, _):
|
||||
return index
|
||||
case let .HoleEntry(hole):
|
||||
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .MessageEntry(lhsIndex, lhsMessage, lhsReadState, lhsSettings, lhsEmbeddedState, lhsPeer, lhsPresence, lhsInfo):
|
||||
switch rhs {
|
||||
case let .MessageEntry(rhsIndex, rhsMessage, rhsReadState, rhsSettings, rhsEmbeddedState, rhsPeer, rhsPresence, rhsInfo):
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsReadState != rhsReadState {
|
||||
return false
|
||||
}
|
||||
if lhsMessage?.stableVersion != rhsMessage?.stableVersion {
|
||||
return false
|
||||
}
|
||||
if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings {
|
||||
if !lhsSettings.isEqual(to: rhsSettings) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsSettings != nil) != (rhsSettings != nil) {
|
||||
return false
|
||||
}
|
||||
if let lhsEmbeddedState = lhsEmbeddedState, let rhsEmbeddedState = rhsEmbeddedState {
|
||||
if !lhsEmbeddedState.isEqual(to: rhsEmbeddedState) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsEmbeddedState != nil) != (rhsEmbeddedState != nil) {
|
||||
return false
|
||||
}
|
||||
if lhsPeer != rhsPeer {
|
||||
return false
|
||||
}
|
||||
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
||||
if !lhsPresence.isEqual(to: rhsPresence) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPresence != nil) != (rhsPresence != nil) {
|
||||
return false
|
||||
}
|
||||
if lhsInfo != rhsInfo {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .HoleEntry(hole):
|
||||
if case .HoleEntry(hole) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func <(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
}
|
||||
|
||||
private func processedChatListEntry(_ entry: MutableChatListEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) -> MutableChatListEntry {
|
||||
switch entry {
|
||||
case let .IntermediateMessageEntry(index, message, readState, embeddedState):
|
||||
var updatedMessage = message
|
||||
if let message = message, let cachedData = cachedDataTable.get(message.id.peerId), let associatedHistoryMessageId = cachedData.associatedHistoryMessageId, message.id.id == 1 {
|
||||
if let messageIndex = messageHistoryTable.messageHistoryIndexTable.earlierEntries(id: associatedHistoryMessageId, count: 1).first {
|
||||
if let associatedMessage = messageHistoryTable.getMessage(messageIndex) {
|
||||
updatedMessage = associatedMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
return .IntermediateMessageEntry(index, updatedMessage, readState, embeddedState)
|
||||
default:
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
enum MutableChatListEntry: Equatable {
|
||||
case IntermediateMessageEntry(ChatListIndex, IntermediateMessage?, CombinedPeerReadState?, PeerChatListEmbeddedInterfaceState?)
|
||||
case MessageEntry(ChatListIndex, Message?, CombinedPeerReadState?, PeerNotificationSettings?, PeerChatListEmbeddedInterfaceState?, RenderedPeer, PeerPresence?, ChatListMessageTagSummaryInfo)
|
||||
case HoleEntry(ChatListHole)
|
||||
|
||||
init(_ intermediateEntry: ChatListIntermediateEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) {
|
||||
switch intermediateEntry {
|
||||
case let .message(index, message, embeddedState):
|
||||
self = processedChatListEntry(.IntermediateMessageEntry(index, message, readStateTable.getCombinedState(index.messageIndex.id.peerId), embeddedState), cachedDataTable: cachedDataTable, readStateTable: readStateTable, messageHistoryTable: messageHistoryTable)
|
||||
case let .hole(hole):
|
||||
self = .HoleEntry(hole)
|
||||
}
|
||||
}
|
||||
|
||||
var index: ChatListIndex {
|
||||
switch self {
|
||||
case let .IntermediateMessageEntry(index, _, _, _):
|
||||
return index
|
||||
case let .MessageEntry(index, _, _, _, _, _, _, _):
|
||||
return index
|
||||
case let .HoleEntry(hole):
|
||||
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: MutableChatListEntry, rhs: MutableChatListEntry) -> Bool {
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
|
||||
switch lhs {
|
||||
case .IntermediateMessageEntry:
|
||||
switch rhs {
|
||||
case .IntermediateMessageEntry:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .MessageEntry:
|
||||
switch rhs {
|
||||
case .MessageEntry:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .HoleEntry:
|
||||
switch rhs {
|
||||
case .HoleEntry:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class MutableChatListViewReplayContext {
|
||||
var invalidEarlier: Bool = false
|
||||
var invalidLater: Bool = false
|
||||
var removedEntries: Bool = false
|
||||
|
||||
func empty() -> Bool {
|
||||
return !self.removedEntries && !invalidEarlier && !invalidLater
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChatListEntryType {
|
||||
case message
|
||||
case hole
|
||||
case groupReference
|
||||
}
|
||||
|
||||
private func updateMessagePeers(_ message: Message, updatedPeers: [PeerId: Peer]) -> Message? {
|
||||
var updated = false
|
||||
for (peerId, currentPeer) in message.peers {
|
||||
if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) {
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if updated {
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
for (peerId, currentPeer) in message.peers {
|
||||
if let updatedPeer = updatedPeers[peerId] {
|
||||
peers[peerId] = updatedPeer
|
||||
} else {
|
||||
peers[peerId] = currentPeer
|
||||
}
|
||||
}
|
||||
return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func updatedRenderedPeer(_ renderedPeer: RenderedPeer, updatedPeers: [PeerId: Peer]) -> RenderedPeer? {
|
||||
var updated = false
|
||||
for (peerId, currentPeer) in renderedPeer.peers {
|
||||
if let updatedPeer = updatedPeers[peerId], !arePeersEqual(currentPeer, updatedPeer) {
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if updated {
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
for (peerId, currentPeer) in renderedPeer.peers {
|
||||
if let updatedPeer = updatedPeers[peerId] {
|
||||
peers[peerId] = updatedPeer
|
||||
} else {
|
||||
peers[peerId] = currentPeer
|
||||
}
|
||||
}
|
||||
return RenderedPeer(peerId: renderedPeer.peerId, peers: peers)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
final class MutableChatListView {
|
||||
let groupId: PeerGroupId
|
||||
private let summaryComponents: ChatListEntrySummaryComponents
|
||||
fileprivate var additionalItemIds: Set<PeerId>
|
||||
fileprivate var additionalItemEntries: [MutableChatListEntry]
|
||||
fileprivate var earlier: MutableChatListEntry?
|
||||
fileprivate var later: MutableChatListEntry?
|
||||
fileprivate var entries: [MutableChatListEntry]
|
||||
fileprivate var groupEntries: [ChatListGroupReferenceEntry]
|
||||
private var count: Int
|
||||
|
||||
init(postbox: Postbox, groupId: PeerGroupId, earlier: MutableChatListEntry?, entries: [MutableChatListEntry], later: MutableChatListEntry?, count: Int, summaryComponents: ChatListEntrySummaryComponents) {
|
||||
self.groupId = groupId
|
||||
self.earlier = earlier
|
||||
self.entries = entries
|
||||
self.later = later
|
||||
self.count = count
|
||||
self.summaryComponents = summaryComponents
|
||||
self.additionalItemEntries = []
|
||||
if case .root = groupId {
|
||||
let itemIds = postbox.additionalChatListItemsTable.get()
|
||||
self.additionalItemIds = Set(itemIds)
|
||||
for peerId in itemIds {
|
||||
if let entry = postbox.chatListTable.getStandalone(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable) {
|
||||
self.additionalItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
|
||||
}
|
||||
}
|
||||
self.groupEntries = []
|
||||
self.reloadGroups(postbox: postbox)
|
||||
} else {
|
||||
self.additionalItemIds = Set()
|
||||
self.groupEntries = []
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadGroups(postbox: Postbox) {
|
||||
self.groupEntries.removeAll()
|
||||
if case .root = self.groupId {
|
||||
for groupId in postbox.chatListTable.existingGroups() {
|
||||
var foundIndices: [(ChatListIndex, MessageIndex)] = []
|
||||
var unpinnedCount = 0
|
||||
let maxCount = 8
|
||||
|
||||
var upperBound: (ChatListIndex, Bool)?
|
||||
inner: while true {
|
||||
if let entry = postbox.chatListTable.earlierEntryInfos(groupId: groupId, index: upperBound, messageHistoryTable: postbox.messageHistoryTable, peerChatInterfaceStateTable: postbox.peerChatInterfaceStateTable, count: 1).first {
|
||||
switch entry {
|
||||
case let .message(index, messageIndex):
|
||||
if let messageIndex = messageIndex {
|
||||
foundIndices.append((index, messageIndex))
|
||||
if index.pinningIndex == nil {
|
||||
unpinnedCount += 1
|
||||
}
|
||||
|
||||
if unpinnedCount >= maxCount {
|
||||
break inner
|
||||
}
|
||||
|
||||
upperBound = (entry.index, true)
|
||||
} else {
|
||||
upperBound = (entry.index.predecessor, true)
|
||||
}
|
||||
case .hole:
|
||||
upperBound = (entry.index, false)
|
||||
}
|
||||
} else {
|
||||
break inner
|
||||
}
|
||||
}
|
||||
|
||||
foundIndices.sort(by: { $0.1 > $1.1 })
|
||||
if foundIndices.count > maxCount {
|
||||
foundIndices.removeSubrange(maxCount...)
|
||||
}
|
||||
|
||||
if !foundIndices.isEmpty {
|
||||
var message: Message?
|
||||
var renderedPeers: [ChatListGroupReferencePeer] = []
|
||||
for (index, messageIndex) in foundIndices {
|
||||
if let peer = postbox.peerTable.get(index.messageIndex.id.peerId) {
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
peers[peer.id] = peer
|
||||
if let associatedPeerId = peer.associatedPeerId {
|
||||
if let associatedPeer = postbox.peerTable.get(associatedPeerId) {
|
||||
peers[associatedPeer.id] = associatedPeer
|
||||
}
|
||||
}
|
||||
|
||||
let renderedPeer = RenderedPeer(peerId: peer.id, peers: peers)
|
||||
let isUnread = postbox.readStateTable.getCombinedState(peer.id)?.isUnread ?? false
|
||||
renderedPeers.append(ChatListGroupReferencePeer(peer: renderedPeer, isUnread: isUnread))
|
||||
|
||||
if foundIndices.count == 1 && message == nil {
|
||||
message = postbox.messageHistoryTable.getMessage(messageIndex).flatMap({ postbox.messageHistoryTable.renderMessage($0, peerTable: postbox.peerTable) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.groupEntries.append(ChatListGroupReferenceEntry(groupId: groupId, message: message, renderedPeers: renderedPeers, unreadState: postbox.groupMessageStatsTable.get(groupId: groupId)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshDueToExternalTransaction(postbox: Postbox) -> Bool {
|
||||
var index = ChatListIndex.absoluteUpperBound
|
||||
if !self.entries.isEmpty && self.later != nil {
|
||||
index = self.entries[self.entries.count / 2].index
|
||||
}
|
||||
|
||||
let (entries, earlier, later) = postbox.fetchAroundChatEntries(groupId: self.groupId, index: index, count: self.entries.count)
|
||||
let currentGroupEntries = self.groupEntries
|
||||
|
||||
self.reloadGroups(postbox: postbox)
|
||||
|
||||
var updated = false
|
||||
|
||||
if self.groupEntries != currentGroupEntries {
|
||||
updated = true
|
||||
}
|
||||
|
||||
if entries != self.entries || earlier != self.earlier || later != self.later {
|
||||
self.entries = entries
|
||||
self.earlier = earlier
|
||||
self.later = later
|
||||
updated = true
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, operations: [PeerGroupId: [ChatListOperation]], updatedPeerNotificationSettings: [PeerId: PeerNotificationSettings], updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence], transaction: PostboxTransaction, context: MutableChatListViewReplayContext) -> Bool {
|
||||
var hasChanges = false
|
||||
|
||||
if let groupOperations = operations[self.groupId] {
|
||||
for operation in groupOperations {
|
||||
switch operation {
|
||||
case let .InsertEntry(index, message, combinedReadState, embeddedState):
|
||||
if self.add(.IntermediateMessageEntry(index, message, combinedReadState, embeddedState), postbox: postbox) {
|
||||
hasChanges = true
|
||||
}
|
||||
case let .InsertHole(index):
|
||||
if self.add(.HoleEntry(index), postbox: postbox) {
|
||||
hasChanges = true
|
||||
}
|
||||
case let .RemoveEntry(indices):
|
||||
if self.remove(Set(indices), type: .message, context: context) {
|
||||
hasChanges = true
|
||||
}
|
||||
case let .RemoveHoles(indices):
|
||||
if self.remove(Set(indices), type: .hole, context: context) {
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .root = self.groupId {
|
||||
var invalidatedGroups = false
|
||||
for (groupId, groupOperations) in operations {
|
||||
if case .group = groupId, !groupOperations.isEmpty {
|
||||
invalidatedGroups = true
|
||||
}
|
||||
}
|
||||
|
||||
if invalidatedGroups {
|
||||
self.reloadGroups(postbox: postbox)
|
||||
hasChanges = true
|
||||
} else {
|
||||
for i in 0 ..< self.groupEntries.count {
|
||||
if let updatedState = transaction.currentUpdatedTotalUnreadSummaries[self.groupEntries[i].groupId] {
|
||||
self.groupEntries[i] = ChatListGroupReferenceEntry(groupId: self.groupEntries[i].groupId, message: self.groupEntries[i].message, renderedPeers: self.groupEntries[i].renderedPeers, unreadState: updatedState)
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
if !transaction.alteredInitialPeerCombinedReadStates.isEmpty {
|
||||
for i in 0 ..< self.groupEntries.count {
|
||||
for j in 0 ..< groupEntries[i].renderedPeers.count {
|
||||
if transaction.alteredInitialPeerCombinedReadStates[groupEntries[i].renderedPeers[j].peer.peerId] != nil {
|
||||
let isUnread = postbox.readStateTable.getCombinedState(groupEntries[i].renderedPeers[j].peer.peerId)?.isUnread ?? false
|
||||
if isUnread != groupEntries[i].renderedPeers[j].isUnread {
|
||||
var renderedPeers = self.groupEntries[i].renderedPeers
|
||||
renderedPeers[j] = ChatListGroupReferencePeer(peer: groupEntries[i].renderedPeers[j].peer, isUnread: isUnread)
|
||||
self.groupEntries[i] = ChatListGroupReferenceEntry(groupId: self.groupEntries[i].groupId, message: self.groupEntries[i].message, renderedPeers: renderedPeers, unreadState: self.groupEntries[i].unreadState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !updatedPeerNotificationSettings.isEmpty {
|
||||
for i in 0 ..< self.entries.count {
|
||||
switch self.entries[i] {
|
||||
case let .MessageEntry(index, message, readState, _, embeddedState, peer, peerPresence, summaryInfo):
|
||||
var notificationSettingsPeerId = peer.peerId
|
||||
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
|
||||
notificationSettingsPeerId = associatedPeerId
|
||||
}
|
||||
if let settings = updatedPeerNotificationSettings[notificationSettingsPeerId] {
|
||||
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo)
|
||||
hasChanges = true
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if !updatedPeers.isEmpty {
|
||||
for i in 0 ..< self.entries.count {
|
||||
switch self.entries[i] {
|
||||
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo):
|
||||
var updatedMessage: Message?
|
||||
if let message = message {
|
||||
updatedMessage = updateMessagePeers(message, updatedPeers: updatedPeers)
|
||||
}
|
||||
let updatedPeer = updatedRenderedPeer(peer, updatedPeers: updatedPeers)
|
||||
if updatedMessage != nil || updatedPeer != nil {
|
||||
self.entries[i] = .MessageEntry(index, updatedMessage ?? message, readState, settings, embeddedState, updatedPeer ?? peer, peerPresence, summaryInfo)
|
||||
hasChanges = true
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if !updatedPeerPresences.isEmpty {
|
||||
for i in 0 ..< self.entries.count {
|
||||
switch self.entries[i] {
|
||||
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, _, summaryInfo):
|
||||
var presencePeerId = peer.peerId
|
||||
if let peer = peer.peers[peer.peerId], let associatedPeerId = peer.associatedPeerId {
|
||||
presencePeerId = associatedPeerId
|
||||
}
|
||||
if let presence = updatedPeerPresences[presencePeerId] {
|
||||
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, presence, summaryInfo)
|
||||
hasChanges = true
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty {
|
||||
for i in 0 ..< self.entries.count {
|
||||
switch self.entries[i] {
|
||||
case let .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, currentSummary):
|
||||
var updatedTagSummaryCount: Int32?
|
||||
var updatedActionsSummaryCount: Int32?
|
||||
|
||||
if let tagSummary = self.summaryComponents.tagSummary {
|
||||
let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: index.messageIndex.id.peerId, namespace: tagSummary.namespace)
|
||||
if let summary = transaction.currentUpdatedMessageTagSummaries[key] {
|
||||
updatedTagSummaryCount = summary.count
|
||||
}
|
||||
}
|
||||
|
||||
if let actionsSummary = self.summaryComponents.actionsSummary {
|
||||
let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: index.messageIndex.id.peerId, namespace: actionsSummary.namespace)
|
||||
if let count = transaction.currentUpdatedMessageActionsSummaries[key] {
|
||||
updatedActionsSummaryCount = count
|
||||
}
|
||||
}
|
||||
|
||||
if updatedTagSummaryCount != nil || updatedActionsSummaryCount != nil {
|
||||
let summaryInfo = ChatListMessageTagSummaryInfo(tagSummaryCount: updatedTagSummaryCount ?? currentSummary.tagSummaryCount, actionsSummaryCount: updatedActionsSummaryCount ?? currentSummary.actionsSummaryCount)
|
||||
|
||||
self.entries[i] = .MessageEntry(index, message, readState, settings, embeddedState, peer, peerPresence, summaryInfo)
|
||||
hasChanges = true
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
var updateAdditionalItems = false
|
||||
if let itemIds = transaction.replacedAdditionalChatListItems {
|
||||
self.additionalItemIds = Set(itemIds)
|
||||
updateAdditionalItems = true
|
||||
}
|
||||
for peerId in self.additionalItemIds {
|
||||
if transaction.currentOperationsByPeerId[peerId] != nil {
|
||||
updateAdditionalItems = true
|
||||
}
|
||||
if transaction.currentUpdatedPeers[peerId] != nil {
|
||||
updateAdditionalItems = true
|
||||
}
|
||||
if transaction.currentUpdatedChatListInclusions[peerId] != nil {
|
||||
updateAdditionalItems = true
|
||||
}
|
||||
}
|
||||
if updateAdditionalItems {
|
||||
self.additionalItemEntries.removeAll()
|
||||
for peerId in postbox.additionalChatListItemsTable.get() {
|
||||
if let entry = postbox.chatListTable.getStandalone(peerId: peerId, messageHistoryTable: postbox.messageHistoryTable) {
|
||||
self.additionalItemEntries.append(MutableChatListEntry(entry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable))
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
}
|
||||
return hasChanges
|
||||
}
|
||||
|
||||
func add(_ initialEntry: MutableChatListEntry, postbox: Postbox) -> Bool {
|
||||
let entry = processedChatListEntry(initialEntry, cachedDataTable: postbox.cachedPeerDataTable, readStateTable: postbox.readStateTable, messageHistoryTable: postbox.messageHistoryTable)
|
||||
if self.entries.count == 0 {
|
||||
self.entries.append(entry)
|
||||
return true
|
||||
} else {
|
||||
let first = self.entries[self.entries.count - 1]
|
||||
let last = self.entries[0]
|
||||
|
||||
let next = self.later
|
||||
|
||||
if entry.index < last.index {
|
||||
if self.earlier == nil || self.earlier!.index < entry.index {
|
||||
if self.entries.count < self.count {
|
||||
self.entries.insert(entry, at: 0)
|
||||
} else {
|
||||
self.earlier = entry
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if entry.index > first.index {
|
||||
if next != nil && entry.index > next!.index {
|
||||
if self.later == nil || self.later!.index > entry.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.remove(at: 0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
} else if entry != last && entry != first {
|
||||
var i = self.entries.count
|
||||
while i >= 1 {
|
||||
if self.entries[i - 1].index < entry.index {
|
||||
break
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
self.entries.insert(entry, at: i)
|
||||
if self.entries.count > self.count {
|
||||
self.earlier = self.entries[0]
|
||||
self.entries.remove(at: 0)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func remove(_ indices: Set<ChatListIndex>, type: ChatListEntryType, context: MutableChatListViewReplayContext) -> Bool {
|
||||
var hasChanges = false
|
||||
if let earlier = self.earlier, indices.contains(earlier.index) {
|
||||
var match = false
|
||||
switch earlier {
|
||||
case .HoleEntry:
|
||||
match = type == .hole
|
||||
case .IntermediateMessageEntry, .MessageEntry:
|
||||
match = type == .message
|
||||
/*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry:
|
||||
match = type == .groupReference*/
|
||||
}
|
||||
if match {
|
||||
context.invalidEarlier = true
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
if let later = self.later, indices.contains(later.index) {
|
||||
var match = false
|
||||
switch later {
|
||||
case .HoleEntry:
|
||||
match = type == .hole
|
||||
case .IntermediateMessageEntry, .MessageEntry:
|
||||
match = type == .message
|
||||
/*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry:
|
||||
match = type == .groupReference*/
|
||||
}
|
||||
if match {
|
||||
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) {
|
||||
var match = false
|
||||
switch self.entries[i] {
|
||||
case .HoleEntry:
|
||||
match = type == .hole
|
||||
case .IntermediateMessageEntry, .MessageEntry:
|
||||
match = type == .message
|
||||
/*case .IntermediateGroupReferenceEntry, .GroupReferenceEntry:
|
||||
match = type == .groupReference*/
|
||||
}
|
||||
if match {
|
||||
self.entries.remove(at: i)
|
||||
context.removedEntries = true
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges
|
||||
}
|
||||
|
||||
func complete(postbox: Postbox, context: MutableChatListViewReplayContext) {
|
||||
if context.removedEntries {
|
||||
var addedEntries: [MutableChatListEntry] = []
|
||||
|
||||
var latestAnchor: ChatListIndex?
|
||||
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 += postbox.fetchLaterChatEntries(groupId: self.groupId, index: later.index.predecessor, count: self.count)
|
||||
}
|
||||
if let earlier = self.earlier {
|
||||
addedEntries += postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlier.index.successor, count: self.count)
|
||||
}
|
||||
|
||||
addedEntries += self.entries
|
||||
addedEntries.sort(by: { $0.index < $1.index })
|
||||
var i = addedEntries.count - 1
|
||||
while i >= 1 {
|
||||
if addedEntries[i].index.messageIndex.id == addedEntries[i - 1].index.messageIndex.id {
|
||||
addedEntries.remove(at: i)
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
self.entries = []
|
||||
|
||||
var anchorIndex = addedEntries.count - 1
|
||||
if let latestAnchor = latestAnchor {
|
||||
var i = addedEntries.count - 1
|
||||
while i >= 0 {
|
||||
if addedEntries[i].index <= latestAnchor {
|
||||
anchorIndex = i
|
||||
break
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
|
||||
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], at: 0)
|
||||
i -= 1
|
||||
}
|
||||
|
||||
self.earlier = nil
|
||||
if anchorIndex - self.count >= 0 {
|
||||
self.earlier = addedEntries[anchorIndex - self.count]
|
||||
}
|
||||
} else {
|
||||
if context.invalidEarlier {
|
||||
var earlyId: ChatListIndex?
|
||||
let i = 0
|
||||
if i < self.entries.count {
|
||||
earlyId = self.entries[i].index
|
||||
}
|
||||
|
||||
let earlierEntries = postbox.fetchEarlierChatEntries(groupId: self.groupId, index: earlyId, count: 1)
|
||||
self.earlier = earlierEntries.first
|
||||
}
|
||||
|
||||
if context.invalidLater {
|
||||
var laterId: ChatListIndex?
|
||||
let i = self.entries.count - 1
|
||||
if i >= 0 {
|
||||
laterId = self.entries[i].index
|
||||
}
|
||||
|
||||
let laterEntries = postbox.fetchLaterChatEntries(groupId: self.groupId, index: laterId, count: 1)
|
||||
self.later = laterEntries.first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func firstHole() -> ChatListHole? {
|
||||
for entry in self.entries {
|
||||
if case let .HoleEntry(hole) = entry {
|
||||
return hole
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func renderEntry(_ entry: MutableChatListEntry, postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) -> MutableChatListEntry? {
|
||||
switch entry {
|
||||
case let .IntermediateMessageEntry(index, message, combinedReadState, embeddedState):
|
||||
let renderedMessage: Message?
|
||||
if let message = message {
|
||||
renderedMessage = renderMessage(message)
|
||||
} else {
|
||||
renderedMessage = nil
|
||||
}
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
var notificationSettings: PeerNotificationSettings?
|
||||
var presence: PeerPresence?
|
||||
if let peer = getPeer(index.messageIndex.id.peerId) {
|
||||
peers[peer.id] = peer
|
||||
if let associatedPeerId = peer.associatedPeerId {
|
||||
if let associatedPeer = getPeer(associatedPeerId) {
|
||||
peers[associatedPeer.id] = associatedPeer
|
||||
}
|
||||
notificationSettings = getPeerNotificationSettings(associatedPeerId)
|
||||
presence = getPeerPresence(associatedPeerId)
|
||||
} else {
|
||||
notificationSettings = getPeerNotificationSettings(index.messageIndex.id.peerId)
|
||||
presence = getPeerPresence(index.messageIndex.id.peerId)
|
||||
}
|
||||
}
|
||||
|
||||
var tagSummaryCount: Int32?
|
||||
var actionsSummaryCount: Int32?
|
||||
|
||||
if let tagSummary = self.summaryComponents.tagSummary {
|
||||
let key = MessageHistoryTagsSummaryKey(tag: tagSummary.tag, peerId: index.messageIndex.id.peerId, namespace: tagSummary.namespace)
|
||||
if let summary = postbox.messageHistoryTagsSummaryTable.get(key) {
|
||||
tagSummaryCount = summary.count
|
||||
}
|
||||
}
|
||||
|
||||
if let actionsSummary = self.summaryComponents.actionsSummary {
|
||||
let key = PendingMessageActionsSummaryKey(type: actionsSummary.type, peerId: index.messageIndex.id.peerId, namespace: actionsSummary.namespace)
|
||||
actionsSummaryCount = postbox.pendingMessageActionsMetadataTable.getCount(.peerNamespaceAction(key.peerId, key.namespace, key.type))
|
||||
}
|
||||
|
||||
return .MessageEntry(index, renderedMessage, combinedReadState, notificationSettings, embeddedState, RenderedPeer(peerId: index.messageIndex.id.peerId, peers: peers), presence, ChatListMessageTagSummaryInfo(tagSummaryCount: tagSummaryCount, actionsSummaryCount: actionsSummaryCount))
|
||||
/*case let .IntermediateGroupReferenceEntry(groupId, index, counters):
|
||||
let message = postbox.messageHistoryTable.getMessage(index.messageIndex).flatMap(postbox.renderIntermediateMessage)
|
||||
return .GroupReferenceEntry(groupId, index, message, ChatListGroupReferenceTopPeers(postbox: postbox, groupId: groupId), counters ?? ChatListGroupReferenceUnreadCounters(postbox: postbox, groupId: groupId))*/
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func render(postbox: Postbox, renderMessage: (IntermediateMessage) -> Message, getPeer: (PeerId) -> Peer?, getPeerNotificationSettings: (PeerId) -> PeerNotificationSettings?, getPeerPresence: (PeerId) -> PeerPresence?) {
|
||||
for i in 0 ..< self.entries.count {
|
||||
if let updatedEntry = self.renderEntry(self.entries[i], postbox: postbox, renderMessage: renderMessage, getPeer: getPeer, getPeerNotificationSettings: getPeerNotificationSettings, getPeerPresence: getPeerPresence) {
|
||||
self.entries[i] = updatedEntry
|
||||
}
|
||||
}
|
||||
for i in 0 ..< self.additionalItemEntries.count {
|
||||
if let updatedEntry = self.renderEntry(self.additionalItemEntries[i], postbox: postbox, renderMessage: renderMessage, getPeer: getPeer, getPeerNotificationSettings: getPeerNotificationSettings, getPeerPresence: getPeerPresence) {
|
||||
self.additionalItemEntries[i] = updatedEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatListView {
|
||||
public let groupId: PeerGroupId
|
||||
public let additionalItemEntries: [ChatListEntry]
|
||||
public let entries: [ChatListEntry]
|
||||
public let groupEntries: [ChatListGroupReferenceEntry]
|
||||
public let earlierIndex: ChatListIndex?
|
||||
public let laterIndex: ChatListIndex?
|
||||
|
||||
init(_ mutableView: MutableChatListView) {
|
||||
self.groupId = mutableView.groupId
|
||||
|
||||
var entries: [ChatListEntry] = []
|
||||
for entry in mutableView.entries {
|
||||
switch entry {
|
||||
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo):
|
||||
entries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo))
|
||||
case let .HoleEntry(hole):
|
||||
entries.append(.HoleEntry(hole))
|
||||
/*case let .GroupReferenceEntry(groupId, index, message, topPeers, counters):
|
||||
entries.append(.GroupReferenceEntry(groupId, index, message, topPeers.getPeers(), GroupReferenceUnreadCounters(counters)))*/
|
||||
case .IntermediateMessageEntry:
|
||||
assertionFailure()
|
||||
/*case .IntermediateGroupReferenceEntry:
|
||||
assertionFailure()*/
|
||||
}
|
||||
}
|
||||
self.groupEntries = mutableView.groupEntries
|
||||
self.entries = entries
|
||||
self.earlierIndex = mutableView.earlier?.index
|
||||
self.laterIndex = mutableView.later?.index
|
||||
|
||||
var additionalItemEntries: [ChatListEntry] = []
|
||||
for entry in mutableView.additionalItemEntries {
|
||||
switch entry {
|
||||
case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo):
|
||||
additionalItemEntries.append(.MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, peerPresence, summaryInfo))
|
||||
case .HoleEntry:
|
||||
assertionFailure()
|
||||
/*case .GroupReferenceEntry:
|
||||
assertionFailure()*/
|
||||
case .IntermediateMessageEntry:
|
||||
assertionFailure()
|
||||
/*case .IntermediateGroupReferenceEntry:
|
||||
assertionFailure()*/
|
||||
}
|
||||
}
|
||||
|
||||
self.additionalItemEntries = additionalItemEntries
|
||||
}
|
||||
}
|
||||
6
submodules/Postbox/Postbox/ChatLocation.swift
Normal file
6
submodules/Postbox/Postbox/ChatLocation.swift
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
public enum ChatLocation: Equatable {
|
||||
case peer(PeerId)
|
||||
//case group(PeerGroupId)
|
||||
}
|
||||
1468
submodules/Postbox/Postbox/Coding.swift
Normal file
1468
submodules/Postbox/Postbox/Coding.swift
Normal file
File diff suppressed because it is too large
Load diff
2
submodules/Postbox/Postbox/Config/Postbox.xcconfig
Normal file
2
submodules/Postbox/Postbox/Config/Postbox.xcconfig
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SWIFT_INCLUDE_PATHS = $(SRCROOT)/Postbox
|
||||
MODULEMAP_PRIVATE_FILE = $(SRCROOT)/Postbox/module.private.modulemap
|
||||
34
submodules/Postbox/Postbox/ContactPeerIdsView.swift
Normal file
34
submodules/Postbox/Postbox/ContactPeerIdsView.swift
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableContactPeerIdsView {
|
||||
fileprivate var remoteTotalCount: Int32
|
||||
fileprivate var peerIds: Set<PeerId>
|
||||
|
||||
init(remoteTotalCount: Int32, peerIds: Set<PeerId>) {
|
||||
self.remoteTotalCount = remoteTotalCount
|
||||
self.peerIds = peerIds
|
||||
}
|
||||
|
||||
func replay(updateRemoteTotalCount: Int32?, replace replacePeerIds: Set<PeerId>) -> Bool {
|
||||
var updated = false
|
||||
if let updateRemoteTotalCount = updateRemoteTotalCount, self.remoteTotalCount != updateRemoteTotalCount {
|
||||
self.remoteTotalCount = updateRemoteTotalCount
|
||||
updated = true
|
||||
}
|
||||
if self.peerIds != replacePeerIds {
|
||||
self.peerIds = replacePeerIds
|
||||
updated = true
|
||||
}
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
public final class ContactPeerIdsView {
|
||||
public let remoteTotalCount: Int32
|
||||
public let peerIds: Set<PeerId>
|
||||
|
||||
init(_ mutableView: MutableContactPeerIdsView) {
|
||||
self.remoteTotalCount = mutableView.remoteTotalCount
|
||||
self.peerIds = mutableView.peerIds
|
||||
}
|
||||
}
|
||||
83
submodules/Postbox/Postbox/ContactPeersView.swift
Normal file
83
submodules/Postbox/Postbox/ContactPeersView.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableContactPeersView {
|
||||
fileprivate var peers: [PeerId: Peer]
|
||||
fileprivate var peerPresences: [PeerId: PeerPresence]
|
||||
fileprivate var peerIds: Set<PeerId>
|
||||
fileprivate var accountPeer: Peer?
|
||||
private let includePresences: Bool
|
||||
|
||||
init(peers: [PeerId: Peer], peerPresences: [PeerId: PeerPresence], accountPeer: Peer?, includePresences: Bool) {
|
||||
self.peers = peers
|
||||
self.peerIds = Set<PeerId>(peers.map { $0.0 })
|
||||
self.peerPresences = peerPresences
|
||||
self.accountPeer = accountPeer
|
||||
self.includePresences = includePresences
|
||||
}
|
||||
|
||||
func replay(replacePeerIds: Set<PeerId>?, updatedPeerPresences: [PeerId: PeerPresence], getPeer: (PeerId) -> Peer?, getPeerPresence: (PeerId) -> PeerPresence?) -> Bool {
|
||||
var updated = false
|
||||
if let replacePeerIds = replacePeerIds {
|
||||
let removedPeerIds = self.peerIds.subtracting(replacePeerIds)
|
||||
let addedPeerIds = replacePeerIds.subtracting(self.peerIds)
|
||||
|
||||
self.peerIds = replacePeerIds
|
||||
|
||||
for peerId in removedPeerIds {
|
||||
let _ = self.peers.removeValue(forKey: peerId)
|
||||
let _ = self.peerPresences.removeValue(forKey: peerId)
|
||||
}
|
||||
|
||||
for peerId in addedPeerIds {
|
||||
if let peer = getPeer(peerId) {
|
||||
self.peers[peerId] = peer
|
||||
}
|
||||
if self.includePresences {
|
||||
if let presence = getPeerPresence(peerId) {
|
||||
self.peerPresences[peerId] = presence
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !removedPeerIds.isEmpty || !addedPeerIds.isEmpty {
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
if self.includePresences, !updatedPeerPresences.isEmpty {
|
||||
for peerId in self.peerIds {
|
||||
if let presence = updatedPeerPresences[peerId] {
|
||||
updated = true
|
||||
self.peerPresences[peerId] = presence
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
public final class ContactPeersView {
|
||||
public let peers: [Peer]
|
||||
public let peerPresences: [PeerId: PeerPresence]
|
||||
public let accountPeer: Peer?
|
||||
|
||||
init(_ mutableView: MutableContactPeersView) {
|
||||
if let accountPeer = mutableView.accountPeer {
|
||||
var peers: [Peer] = []
|
||||
peers.reserveCapacity(mutableView.peers.count)
|
||||
let accountPeerId = accountPeer.id
|
||||
for peer in mutableView.peers.values {
|
||||
if peer.id != accountPeerId {
|
||||
peers.append(peer)
|
||||
}
|
||||
}
|
||||
self.peers = peers
|
||||
} else {
|
||||
self.peers = mutableView.peers.map({ $0.1 })
|
||||
}
|
||||
self.peerPresences = mutableView.peerPresences
|
||||
self.accountPeer = mutableView.accountPeer
|
||||
}
|
||||
}
|
||||
|
||||
87
submodules/Postbox/Postbox/ContactTable.swift
Normal file
87
submodules/Postbox/Postbox/ContactTable.swift
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import Foundation
|
||||
|
||||
final class ContactTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let peerNameIndexTable: PeerNameIndexTable
|
||||
|
||||
private var peerIdsBeforeModification: Set<PeerId>?
|
||||
private var peerIds: Set<PeerId>?
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, peerNameIndexTable: PeerNameIndexTable) {
|
||||
self.peerNameIndexTable = peerNameIndexTable
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(_ id: PeerId, sharedKey: ValueBoxKey = ValueBoxKey(length: 8)) -> ValueBoxKey {
|
||||
sharedKey.setInt64(0, value: id.toInt64())
|
||||
return sharedKey
|
||||
}
|
||||
|
||||
private func lowerBound() -> ValueBoxKey {
|
||||
return self.key(PeerId(namespace: 0, id: 0))
|
||||
}
|
||||
|
||||
private func upperBound() -> ValueBoxKey {
|
||||
return self.key(PeerId(namespace: Int32.max, id: Int32.max))
|
||||
}
|
||||
|
||||
func isContact(peerId: PeerId) -> Bool {
|
||||
return self.get().contains(peerId)
|
||||
}
|
||||
|
||||
func get() -> Set<PeerId> {
|
||||
if let peerIds = self.peerIds {
|
||||
return peerIds
|
||||
} else {
|
||||
var peerIds = Set<PeerId>()
|
||||
self.valueBox.range(self.table, start: self.lowerBound(), end: self.upperBound(), keys: { key in
|
||||
peerIds.insert(PeerId(key.getInt64(0)))
|
||||
return true
|
||||
}, limit: 0)
|
||||
self.peerIds = peerIds
|
||||
return peerIds
|
||||
}
|
||||
}
|
||||
|
||||
func replace(_ ids: Set<PeerId>) {
|
||||
if self.peerIdsBeforeModification == nil {
|
||||
self.peerIdsBeforeModification = self.get()
|
||||
}
|
||||
|
||||
self.peerIds = ids
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
assert(self.peerIdsBeforeModification == nil)
|
||||
self.peerIds = nil
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if let peerIdsBeforeModification = self.peerIdsBeforeModification {
|
||||
if let peerIds = self.peerIds {
|
||||
let removedPeerIds = peerIdsBeforeModification.subtracting(peerIds)
|
||||
let addedPeerIds = peerIds.subtracting(peerIdsBeforeModification)
|
||||
|
||||
let sharedKey = self.key(PeerId(namespace: 0, id: 0))
|
||||
|
||||
for peerId in removedPeerIds {
|
||||
self.valueBox.remove(self.table, key: self.key(peerId, sharedKey: sharedKey), secure: false)
|
||||
self.peerNameIndexTable.setPeerCategoryState(peerId: peerId, category: [.contacts], includes: false)
|
||||
}
|
||||
|
||||
for peerId in addedPeerIds {
|
||||
self.valueBox.set(self.table, key: self.key(peerId, sharedKey: sharedKey), value: MemoryBuffer())
|
||||
self.peerNameIndexTable.setPeerCategoryState(peerId: peerId, category: [.contacts], includes: true)
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
self.peerIdsBeforeModification = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
8
submodules/Postbox/Postbox/Crc32.h
Normal file
8
submodules/Postbox/Postbox/Crc32.h
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#ifndef Postbox_Crc32_h
|
||||
#define Postbox_Crc32_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
uint32_t Crc32(const void *bytes, int length);
|
||||
|
||||
#endif
|
||||
7
submodules/Postbox/Postbox/Crc32.m
Normal file
7
submodules/Postbox/Postbox/Crc32.m
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#import "Crc32.h"
|
||||
|
||||
#import <zlib.h>
|
||||
|
||||
uint32_t Crc32(const void *bytes, int length) {
|
||||
return (uint32_t)crc32(0, bytes, (uInt)length);
|
||||
}
|
||||
67
submodules/Postbox/Postbox/Database.swift
Normal file
67
submodules/Postbox/Postbox/Database.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// SQLite.swift
|
||||
// https://github.com/stephencelis/SQLite.swift
|
||||
// Copyright (c) 2014-2015 Stephen Celis.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import sqlcipher
|
||||
|
||||
public final class Database {
|
||||
internal var handle: OpaquePointer? = nil
|
||||
|
||||
public init?(_ location: String) {
|
||||
if location != ":memory:" {
|
||||
let _ = open(location + "-guard", O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR)
|
||||
}
|
||||
let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX
|
||||
let res = sqlite3_open_v2(location, &self.handle, flags, nil)
|
||||
if res != SQLITE_OK {
|
||||
postboxLog("sqlite3_open_v2: \(res)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
sqlite3_close(self.handle)
|
||||
} // sqlite3_close_v2 in Yosemite/iOS 8?
|
||||
|
||||
public func execute(_ SQL: String) -> Bool {
|
||||
let res = sqlite3_exec(self.handle, SQL, nil, nil, nil)
|
||||
if res == SQLITE_OK {
|
||||
return true
|
||||
} else {
|
||||
if let error = sqlite3_errmsg(self.handle), let str = NSString(utf8String: error) {
|
||||
print("SQL error \(res): \(str) on SQL")
|
||||
} else {
|
||||
print("SQL error \(res) on SQL")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func currentError() -> String? {
|
||||
if let error = sqlite3_errmsg(self.handle), let str = NSString(utf8String: error) {
|
||||
return "SQL error \(str)"
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import Foundation
|
||||
|
||||
final class DeviceContactImportInfoTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
func get(_ identifier: ValueBoxKey) -> PostboxCoding? {
|
||||
if let value = self.valueBox.get(self.table, key: identifier), let object = PostboxDecoder(buffer: value).decodeRootObject() {
|
||||
return object
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func set(_ identifier: ValueBoxKey, value: PostboxCoding?) {
|
||||
if let value = value {
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(value)
|
||||
withExtendedLifetime(encoder, {
|
||||
self.valueBox.set(self.table, key: identifier, value: encoder.readBufferNoCopy())
|
||||
})
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: identifier, secure: false)
|
||||
}
|
||||
}
|
||||
|
||||
func getIdentifiers() -> [ValueBoxKey] {
|
||||
var result: [ValueBoxKey] = []
|
||||
self.valueBox.scan(self.table, keys: { key in
|
||||
result.append(key)
|
||||
return true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func enumerateDeviceContactImportInfoItems(_ f: (ValueBoxKey, PostboxCoding) -> Bool) {
|
||||
self.valueBox.scan(self.table, values: { key, value in
|
||||
if let object = PostboxDecoder(buffer: value).decodeRootObject() {
|
||||
return f(key, object)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
21
submodules/Postbox/Postbox/FileSize.swift
Normal file
21
submodules/Postbox/Postbox/FileSize.swift
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import Foundation
|
||||
|
||||
public func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int? {
|
||||
if useTotalFileAllocatedSize {
|
||||
let url = URL(fileURLWithPath: path)
|
||||
if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) {
|
||||
if values.isRegularFile ?? false {
|
||||
if let fileSize = values.totalFileAllocatedSize {
|
||||
return fileSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var value = stat()
|
||||
if stat(path, &value) == 0 {
|
||||
return Int(value.st_size)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
211
submodules/Postbox/Postbox/GlobalMessageHistoryTagsTable.swift
Normal file
211
submodules/Postbox/Postbox/GlobalMessageHistoryTagsTable.swift
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import Foundation
|
||||
|
||||
enum IntermediateGlobalMessageTagsEntry {
|
||||
case message(IntermediateMessage)
|
||||
case hole(MessageIndex)
|
||||
|
||||
var index: MessageIndex {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
return message.index
|
||||
case let .hole(index):
|
||||
return index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GlobalMessageHistoryTagsTableEntry {
|
||||
case message(MessageIndex)
|
||||
case hole(MessageIndex)
|
||||
|
||||
var index: MessageIndex {
|
||||
switch self {
|
||||
case let .message(index):
|
||||
return index
|
||||
case let .hole(index):
|
||||
return index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GlobalMessageHistoryTagsOperation {
|
||||
case insertMessage(GlobalMessageTags, IntermediateMessage)
|
||||
case insertHole(GlobalMessageTags, MessageIndex)
|
||||
case remove([(GlobalMessageTags, MessageIndex)])
|
||||
case updateTimestamp(GlobalMessageTags, MessageIndex, Int32)
|
||||
}
|
||||
|
||||
private func parseEntry(key: ValueBoxKey, value: ReadBuffer) -> GlobalMessageHistoryTagsTableEntry {
|
||||
var type: Int8 = 0
|
||||
value.read(&type, offset: 0, length: 1)
|
||||
let index = MessageIndex(id: MessageId(peerId: PeerId(key.getInt64(4 + 4 + 4 + 4)), namespace: key.getInt32(4 + 4), id: key.getInt32(4 + 4 + 4)), timestamp: key.getInt32(4))
|
||||
if type == 0 {
|
||||
return .message(index)
|
||||
} else if type == 1 {
|
||||
return .hole(index)
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
class GlobalMessageHistoryTagsTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 4 + 4 + 4 + 4 + 8)
|
||||
|
||||
private var cachedInitializedTags = Set<GlobalMessageTags>()
|
||||
|
||||
private func key(_ tagMask: GlobalMessageTags, index: MessageIndex, key: ValueBoxKey = ValueBoxKey(length: 4 + 4 + 4 + 4 + 8)) -> ValueBoxKey {
|
||||
key.setUInt32(0, value: tagMask.rawValue)
|
||||
key.setInt32(4, value: index.timestamp)
|
||||
key.setInt32(4 + 4, value: index.id.namespace)
|
||||
key.setInt32(4 + 4 + 4, value: index.id.id)
|
||||
key.setInt64(4 + 4 + 4 + 4, value: index.id.peerId.toInt64())
|
||||
return key
|
||||
}
|
||||
|
||||
private func lowerBound(_ tagMask: GlobalMessageTags) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setUInt32(0, value: tagMask.rawValue)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(_ tagMask: GlobalMessageTags) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setUInt32(0, value: tagMask.rawValue)
|
||||
return key.successor
|
||||
}
|
||||
|
||||
func ensureInitialized(_ tagMask: GlobalMessageTags) {
|
||||
for tag in tagMask {
|
||||
if !self.cachedInitializedTags.contains(tag) {
|
||||
var isEmpty = true
|
||||
self.valueBox.range(self.table, start: self.lowerBound(tag), end: self.upperBound(tag), keys: { _ in
|
||||
isEmpty = false
|
||||
return false
|
||||
}, limit: 1)
|
||||
if isEmpty {
|
||||
self.addHole(tag, index: MessageIndex.absoluteUpperBound())
|
||||
}
|
||||
self.cachedInitializedTags.insert(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addMessage(_ tagMask: GlobalMessageTags, index: MessageIndex) -> Bool {
|
||||
self.ensureInitialized(tagMask)
|
||||
|
||||
assert(tagMask.isSingleTag)
|
||||
var upperIsHole = false
|
||||
self.valueBox.range(self.table, start: self.key(tagMask, index: index, key: self.sharedKey), end: self.upperBound(tagMask), values: { key, value in
|
||||
let entry = parseEntry(key: key, value: value)
|
||||
if case .hole = entry {
|
||||
upperIsHole = true
|
||||
}
|
||||
return false
|
||||
}, limit: 1)
|
||||
if !upperIsHole {
|
||||
var type: Int8 = 0
|
||||
self.valueBox.set(self.table, key: self.key(tagMask, index: index, key: self.sharedKey), value: MemoryBuffer(memory: &type, capacity: 1, length: 1, freeWhenDone: false))
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func addHole(_ tagMask: GlobalMessageTags, index: MessageIndex) {
|
||||
assert(tagMask.isSingleTag)
|
||||
var type: Int8 = 1
|
||||
self.valueBox.set(self.table, key: self.key(tagMask, index: index, key: self.sharedKey), value: MemoryBuffer(memory: &type, capacity: 1, length: 1, freeWhenDone: false))
|
||||
}
|
||||
|
||||
func remove(_ tagMask: GlobalMessageTags, index: MessageIndex) {
|
||||
assert(tagMask.isSingleTag)
|
||||
self.valueBox.remove(self.table, key: self.key(tagMask, index: index, key: self.sharedKey), secure: false)
|
||||
}
|
||||
|
||||
func get(_ tagMask: GlobalMessageTags, index: MessageIndex) -> GlobalMessageHistoryTagsTableEntry? {
|
||||
let key = self.key(tagMask, index: index)
|
||||
if let value = self.valueBox.get(self.table, key: key) {
|
||||
return parseEntry(key: key, value: value)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func entriesAround(_ tagMask: GlobalMessageTags, index: MessageIndex, count: Int) -> (entries: [GlobalMessageHistoryTagsTableEntry], lower: GlobalMessageHistoryTagsTableEntry?, upper: GlobalMessageHistoryTagsTableEntry?) {
|
||||
var lowerEntries: [GlobalMessageHistoryTagsTableEntry] = []
|
||||
var upperEntries: [GlobalMessageHistoryTagsTableEntry] = []
|
||||
var lower: GlobalMessageHistoryTagsTableEntry?
|
||||
var upper: GlobalMessageHistoryTagsTableEntry?
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(tagMask, index: index), end: self.lowerBound(tagMask), values: { key, value in
|
||||
lowerEntries.append(parseEntry(key: key, value: value))
|
||||
return true
|
||||
}, limit: count / 2 + 1)
|
||||
|
||||
if lowerEntries.count >= count / 2 + 1 {
|
||||
lower = lowerEntries.last
|
||||
lowerEntries.removeLast()
|
||||
}
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(tagMask, index: index).predecessor, end: self.upperBound(tagMask), values: { key, value in
|
||||
upperEntries.append(parseEntry(key: key, value: value))
|
||||
return true
|
||||
}, limit: count - lowerEntries.count + 1)
|
||||
if upperEntries.count >= count - lowerEntries.count + 1 {
|
||||
upper = upperEntries.last
|
||||
upperEntries.removeLast()
|
||||
}
|
||||
|
||||
if lowerEntries.count != 0 && lowerEntries.count + upperEntries.count < count {
|
||||
var additionalLowerEntries: [GlobalMessageHistoryTagsTableEntry] = []
|
||||
self.valueBox.range(self.table, start: self.key(tagMask, index: lowerEntries.last!.index), end: self.lowerBound(tagMask), values: { key, value in
|
||||
additionalLowerEntries.append(parseEntry(key: key, value: value))
|
||||
return true
|
||||
}, limit: count - lowerEntries.count - upperEntries.count + 1)
|
||||
if additionalLowerEntries.count >= count - lowerEntries.count + upperEntries.count + 1 {
|
||||
lower = additionalLowerEntries.last
|
||||
additionalLowerEntries.removeLast()
|
||||
}
|
||||
lowerEntries.append(contentsOf: additionalLowerEntries)
|
||||
}
|
||||
|
||||
var entries: [GlobalMessageHistoryTagsTableEntry] = []
|
||||
entries.append(contentsOf: lowerEntries.reversed())
|
||||
entries.append(contentsOf: upperEntries)
|
||||
return (entries: entries, lower: lower, upper: upper)
|
||||
}
|
||||
|
||||
func earlierEntries(_ tagMask: GlobalMessageTags, index: MessageIndex?, count: Int) -> [GlobalMessageHistoryTagsTableEntry] {
|
||||
var indices: [GlobalMessageHistoryTagsTableEntry] = []
|
||||
let key: ValueBoxKey
|
||||
if let index = index {
|
||||
key = self.key(tagMask, index: index)
|
||||
} else {
|
||||
key = self.upperBound(tagMask)
|
||||
}
|
||||
self.valueBox.range(self.table, start: key, end: self.lowerBound(tagMask), values: { key, value in
|
||||
indices.append(parseEntry(key: key, value: value))
|
||||
return true
|
||||
}, limit: count)
|
||||
return indices
|
||||
}
|
||||
|
||||
func laterEntries(_ tagMask: GlobalMessageTags, index: MessageIndex?, count: Int) -> [GlobalMessageHistoryTagsTableEntry] {
|
||||
var indices: [GlobalMessageHistoryTagsTableEntry] = []
|
||||
let key: ValueBoxKey
|
||||
if let index = index {
|
||||
key = self.key(tagMask, index: index)
|
||||
} else {
|
||||
key = self.lowerBound(tagMask)
|
||||
}
|
||||
self.valueBox.range(self.table, start: key, end: self.upperBound(tagMask), values: { key, value in
|
||||
indices.append(parseEntry(key: key, value: value))
|
||||
return true
|
||||
}, limit: count)
|
||||
return indices
|
||||
}
|
||||
}
|
||||
51
submodules/Postbox/Postbox/GlobalMessageIdsTable.swift
Normal file
51
submodules/Postbox/Postbox/GlobalMessageIdsTable.swift
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Foundation
|
||||
|
||||
final class GlobalMessageIdsTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let seedConfiguration: SeedConfiguration
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
private let sharedBuffer = WriteBuffer()
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, seedConfiguration: SeedConfiguration) {
|
||||
self.seedConfiguration = seedConfiguration
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(_ id: Int32) -> ValueBoxKey {
|
||||
self.sharedKey.setInt64(0, value: Int64(id))
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
func set(_ globalId: Int32, id: MessageId) {
|
||||
assert(id.namespace == 0)
|
||||
assert(id.peerId.namespace == 0 || id.peerId.namespace == 1)
|
||||
assert(self.seedConfiguration.globalMessageIdsPeerIdNamespaces.contains(GlobalMessageIdsNamespace(peerIdNamespace: id.peerId.namespace, messageIdNamespace: id.namespace)))
|
||||
|
||||
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.table, key: self.key(globalId), value: self.sharedBuffer)
|
||||
}
|
||||
|
||||
func get(_ globalId: Int32) -> MessageId? {
|
||||
if let value = self.valueBox.get(self.table, 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.table, key: self.key(globalId), secure: false)
|
||||
}
|
||||
}
|
||||
469
submodules/Postbox/Postbox/GlobalMessageTagsView.swift
Normal file
469
submodules/Postbox/Postbox/GlobalMessageTagsView.swift
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
import Foundation
|
||||
|
||||
private enum InternalGlobalMessageTagsEntry: Comparable {
|
||||
case intermediateMessage(IntermediateMessage)
|
||||
case message(Message)
|
||||
case hole(MessageIndex)
|
||||
|
||||
var index: MessageIndex {
|
||||
switch self {
|
||||
case let .intermediateMessage(message):
|
||||
return message.index
|
||||
case let .message(message):
|
||||
return message.index
|
||||
case let .hole(index):
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: InternalGlobalMessageTagsEntry, rhs: InternalGlobalMessageTagsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .intermediateMessage(lhsMessage):
|
||||
if case let .intermediateMessage(rhsMessage) = rhs {
|
||||
if lhsMessage.stableVersion != rhsMessage.stableVersion {
|
||||
return false
|
||||
}
|
||||
if lhsMessage.index != rhsMessage.index {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .message(lhsMessage):
|
||||
if case let .message(rhsMessage) = rhs {
|
||||
if lhsMessage.stableVersion != rhsMessage.stableVersion {
|
||||
return false
|
||||
}
|
||||
if lhsMessage.index != rhsMessage.index {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .hole(index):
|
||||
if case .hole(index) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: InternalGlobalMessageTagsEntry, rhs: InternalGlobalMessageTagsEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
}
|
||||
|
||||
public enum GlobalMessageTagsEntry {
|
||||
case message(Message)
|
||||
case hole(MessageIndex)
|
||||
|
||||
public var index: MessageIndex {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
return message.index
|
||||
case let .hole(index):
|
||||
return index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class MutableGlobalMessageTagsViewReplayContext {
|
||||
var invalidEarlier: Bool = false
|
||||
var invalidLater: Bool = false
|
||||
var removedEntries: Bool = false
|
||||
|
||||
func empty() -> Bool {
|
||||
return !self.removedEntries && !invalidEarlier && !invalidLater
|
||||
}
|
||||
}
|
||||
|
||||
final class MutableGlobalMessageTagsView: MutablePostboxView {
|
||||
private let globalTag: GlobalMessageTags
|
||||
private let count: Int
|
||||
private let groupingPredicate: ((Message, Message) -> Bool)?
|
||||
|
||||
fileprivate var entries: [InternalGlobalMessageTagsEntry]
|
||||
fileprivate var earlier: MessageIndex?
|
||||
fileprivate var later: MessageIndex?
|
||||
|
||||
init(postbox: Postbox, globalTag: GlobalMessageTags, position: MessageIndex, count: Int, groupingPredicate: ((Message, Message) -> Bool)?) {
|
||||
self.globalTag = globalTag
|
||||
self.count = count
|
||||
self.groupingPredicate = groupingPredicate
|
||||
|
||||
let (entries, lower, upper) = postbox.messageHistoryTable.entriesAround(globalTagMask: globalTag, index: position, count: count)
|
||||
|
||||
self.entries = entries.map { entry -> InternalGlobalMessageTagsEntry in
|
||||
switch entry {
|
||||
case let .message(message):
|
||||
return .intermediateMessage(message)
|
||||
case let .hole(index):
|
||||
return .hole(index)
|
||||
}
|
||||
}
|
||||
self.earlier = lower
|
||||
self.later = upper
|
||||
|
||||
self.render(postbox: postbox)
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var hasChanges = false
|
||||
|
||||
let context = MutableGlobalMessageTagsViewReplayContext()
|
||||
|
||||
for operation in transaction.currentGlobalTagsOperations {
|
||||
switch operation {
|
||||
case let .insertMessage(tags, message):
|
||||
if (self.globalTag.rawValue & tags.rawValue) != 0 {
|
||||
if self.add(.intermediateMessage(message)) {
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
case let .insertHole(tags, index):
|
||||
if (self.globalTag.rawValue & tags.rawValue) != 0 {
|
||||
if self.add(.hole(index)) {
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
case let .remove(tagsAndIndices):
|
||||
var indices = Set<MessageIndex>()
|
||||
for (tags, index) in tagsAndIndices {
|
||||
if (self.globalTag.rawValue & tags.rawValue) != 0 {
|
||||
indices.insert(index)
|
||||
}
|
||||
}
|
||||
if !indices.isEmpty {
|
||||
if self.remove(indices, context: context) {
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
case let .updateTimestamp(tags, previousIndex, updatedTimestamp):
|
||||
if (self.globalTag.rawValue & tags.rawValue) != 0 {
|
||||
inner: for i in 0 ..< self.entries.count {
|
||||
let entry = self.entries[i]
|
||||
if entry.index == previousIndex {
|
||||
let updatedIndex = MessageIndex(id: entry.index.id, timestamp: updatedTimestamp)
|
||||
if self.remove(Set([entry.index]), context: context) {
|
||||
hasChanges = true
|
||||
}
|
||||
switch entry {
|
||||
case .hole:
|
||||
if self.add(.hole(updatedIndex)) {
|
||||
hasChanges = true
|
||||
}
|
||||
case let .intermediateMessage(message):
|
||||
if self.add(.intermediateMessage(IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: updatedTimestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: message.embeddedMediaData, referencedMedia: message.referencedMedia))) {
|
||||
hasChanges = true
|
||||
}
|
||||
case let .message(message):
|
||||
if self.add(.message(Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, timestamp: updatedTimestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds))) {
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasChanges || !context.empty() {
|
||||
self.complete(postbox: postbox, context: context)
|
||||
self.render(postbox: postbox)
|
||||
|
||||
self.render(postbox: postbox)
|
||||
}
|
||||
|
||||
return hasChanges
|
||||
}
|
||||
|
||||
private func add(_ entry: InternalGlobalMessageTagsEntry) -> Bool {
|
||||
if self.entries.count == 0 {
|
||||
self.entries.append(entry)
|
||||
return true
|
||||
} else {
|
||||
let first = self.entries[self.entries.count - 1]
|
||||
let last = self.entries[0]
|
||||
|
||||
let next = self.later
|
||||
|
||||
if entry.index < last.index {
|
||||
if self.earlier == nil || self.earlier! < entry.index {
|
||||
if self.entries.count < self.count {
|
||||
self.entries.insert(entry, at: 0)
|
||||
} else {
|
||||
self.earlier = entry.index
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if entry.index > first.index {
|
||||
if next != nil && entry.index > next! {
|
||||
if self.later == nil || self.later! > entry.index {
|
||||
if self.entries.count < self.count {
|
||||
self.entries.append(entry)
|
||||
} else {
|
||||
self.later = entry.index
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
self.entries.append(entry)
|
||||
if self.entries.count > self.count {
|
||||
self.earlier = self.entries[0].index
|
||||
self.entries.remove(at: 0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
} else if entry != last && entry != first {
|
||||
var i = self.entries.count
|
||||
while i >= 1 {
|
||||
if self.entries[i - 1].index < entry.index {
|
||||
break
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
self.entries.insert(entry, at: i)
|
||||
if self.entries.count > self.count {
|
||||
self.earlier = self.entries[0].index
|
||||
self.entries.remove(at: 0)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func remove(_ indices: Set<MessageIndex>, context: MutableGlobalMessageTagsViewReplayContext) -> Bool {
|
||||
var hasChanges = false
|
||||
if let earlier = self.earlier, indices.contains(earlier) {
|
||||
context.invalidEarlier = true
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
if let later = self.later , indices.contains(later) {
|
||||
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.remove(at: i)
|
||||
context.removedEntries = true
|
||||
hasChanges = true
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges
|
||||
}
|
||||
|
||||
private func complete(postbox: Postbox, context: MutableGlobalMessageTagsViewReplayContext) {
|
||||
if context.removedEntries {
|
||||
self.completeWithReset(postbox: postbox)
|
||||
} else {
|
||||
if context.invalidEarlier {
|
||||
var earlyId: MessageIndex?
|
||||
let i = 0
|
||||
if i < self.entries.count {
|
||||
earlyId = self.entries[i].index
|
||||
}
|
||||
|
||||
let earlierEntries = postbox.messageHistoryTable.earlierEntries(globalTagMask: self.globalTag, index: earlyId, count: 1).map { entry -> InternalGlobalMessageTagsEntry in
|
||||
switch entry {
|
||||
case let .message(message):
|
||||
return .intermediateMessage(message)
|
||||
case let .hole(index):
|
||||
return .hole(index)
|
||||
}
|
||||
}
|
||||
self.earlier = earlierEntries.first?.index
|
||||
}
|
||||
|
||||
if context.invalidLater {
|
||||
var laterId: MessageIndex?
|
||||
let i = self.entries.count - 1
|
||||
if i >= 0 {
|
||||
laterId = self.entries[i].index
|
||||
}
|
||||
|
||||
let laterEntries = postbox.messageHistoryTable.laterEntries(globalTagMask: self.globalTag, index: laterId, count: 1).map { entry -> InternalGlobalMessageTagsEntry in
|
||||
switch entry {
|
||||
case let .message(message):
|
||||
return .intermediateMessage(message)
|
||||
case let .hole(index):
|
||||
return .hole(index)
|
||||
}
|
||||
}
|
||||
self.later = laterEntries.first?.index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func completeWithReset(postbox: Postbox) {
|
||||
var addedEntries: [InternalGlobalMessageTagsEntry] = []
|
||||
|
||||
var latestAnchor: MessageIndex?
|
||||
if let last = self.entries.last {
|
||||
latestAnchor = last.index
|
||||
}
|
||||
|
||||
if latestAnchor == nil {
|
||||
if let later = self.later {
|
||||
latestAnchor = later
|
||||
}
|
||||
}
|
||||
|
||||
if let later = self.later {
|
||||
addedEntries += postbox.messageHistoryTable.laterEntries(globalTagMask: self.globalTag, index: later.predecessor(), count: self.count).map { entry -> InternalGlobalMessageTagsEntry in
|
||||
switch entry {
|
||||
case let .message(message):
|
||||
return .intermediateMessage(message)
|
||||
case let .hole(index):
|
||||
return .hole(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let earlier = self.earlier {
|
||||
addedEntries += postbox.messageHistoryTable.earlierEntries(globalTagMask: self.globalTag, index: earlier.successor(), count: self.count).map { entry -> InternalGlobalMessageTagsEntry in
|
||||
switch entry {
|
||||
case let .message(message):
|
||||
return .intermediateMessage(message)
|
||||
case let .hole(index):
|
||||
return .hole(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addedEntries += self.entries
|
||||
addedEntries.sort(by: { $0.index < $1.index })
|
||||
var i = addedEntries.count - 1
|
||||
while i >= 1 {
|
||||
if addedEntries[i].index.id == addedEntries[i - 1].index.id {
|
||||
addedEntries.remove(at: i)
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
self.entries = []
|
||||
|
||||
var anchorIndex = addedEntries.count - 1
|
||||
if let latestAnchor = latestAnchor {
|
||||
var i = addedEntries.count - 1
|
||||
while i >= 0 {
|
||||
if addedEntries[i].index <= latestAnchor {
|
||||
anchorIndex = i
|
||||
break
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
|
||||
/*let indices = self.groupedIndices(addedEntries)
|
||||
|
||||
if indices.count > self.count {
|
||||
|
||||
} else {
|
||||
self.later = nil
|
||||
self.earlier = nil
|
||||
self.entries = addedEntries
|
||||
}*/
|
||||
|
||||
self.later = nil
|
||||
if anchorIndex + 1 < addedEntries.count {
|
||||
self.later = addedEntries[anchorIndex + 1].index
|
||||
}
|
||||
|
||||
i = anchorIndex
|
||||
while i >= 0 && i > anchorIndex - self.count {
|
||||
self.entries.insert(addedEntries[i], at: 0)
|
||||
i -= 1
|
||||
}
|
||||
|
||||
self.earlier = nil
|
||||
if anchorIndex - self.count >= 0 {
|
||||
self.earlier = addedEntries[anchorIndex - self.count].index
|
||||
}
|
||||
}
|
||||
|
||||
private func groupedIndices(_ entries: [InternalGlobalMessageTagsEntry]) -> [[Int]] {
|
||||
if entries.isEmpty {
|
||||
return []
|
||||
}
|
||||
if let groupingPredicate = self.groupingPredicate {
|
||||
var result: [[Int]] = [[0]]
|
||||
for i in 1 ..< entries.count {
|
||||
switch entries[i] {
|
||||
case .hole:
|
||||
result.append([i])
|
||||
case let .message(message):
|
||||
switch entries[i - 1] {
|
||||
case .hole:
|
||||
result.append([i])
|
||||
case let .message(previousMessage):
|
||||
if !groupingPredicate(message, previousMessage) {
|
||||
result.append([i])
|
||||
} else {
|
||||
result[result.count - 1].append(i)
|
||||
}
|
||||
case .intermediateMessage:
|
||||
assertionFailure()
|
||||
result.append([i])
|
||||
}
|
||||
case .intermediateMessage:
|
||||
assertionFailure()
|
||||
result.append([i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
return (0 ..< entries.count).map { [$0] }
|
||||
}
|
||||
}
|
||||
|
||||
private func render(postbox: Postbox) {
|
||||
for i in 0 ..< self.entries.count {
|
||||
if case let .intermediateMessage(message) = self.entries[i] {
|
||||
self.entries[i] = .message(postbox.renderIntermediateMessage(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return GlobalMessageTagsView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class GlobalMessageTagsView: PostboxView {
|
||||
public let entries: [GlobalMessageTagsEntry]
|
||||
public let earlier: MessageIndex?
|
||||
public let later: MessageIndex?
|
||||
|
||||
init(_ view: MutableGlobalMessageTagsView) {
|
||||
var entries: [GlobalMessageTagsEntry] = []
|
||||
for entry in view.entries {
|
||||
switch entry {
|
||||
case let .message(message):
|
||||
entries.append(.message(message))
|
||||
case let .hole(index):
|
||||
entries.append(.hole(index))
|
||||
case .intermediateMessage:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
}
|
||||
self.entries = entries
|
||||
self.earlier = view.earlier
|
||||
self.later = view.later
|
||||
}
|
||||
}
|
||||
212
submodules/Postbox/Postbox/GroupMessageStatsTable.swift
Normal file
212
submodules/Postbox/Postbox/GroupMessageStatsTable.swift
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import Foundation
|
||||
|
||||
public struct PeerGroupUnreadCounters: PostboxCoding, Equatable {
|
||||
public var messageCount: Int32
|
||||
public var chatCount: Int32
|
||||
|
||||
public init(messageCount: Int32, chatCount: Int32) {
|
||||
self.messageCount = messageCount
|
||||
self.chatCount = chatCount
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.messageCount = decoder.decodeInt32ForKey("m", orElse: 0)
|
||||
self.chatCount = decoder.decodeInt32ForKey("c", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.messageCount, forKey: "m")
|
||||
encoder.encodeInt32(self.chatCount, forKey: "c")
|
||||
}
|
||||
}
|
||||
|
||||
public struct PeerGroupUnreadCountersSummary: PostboxCoding, Equatable {
|
||||
public var all: PeerGroupUnreadCounters
|
||||
|
||||
public init(all: PeerGroupUnreadCounters) {
|
||||
self.all = all
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.all = decoder.decodeObjectForKey("a", decoder: { PeerGroupUnreadCounters(decoder: $0) }) as! PeerGroupUnreadCounters
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObject(self.all, forKey: "a")
|
||||
}
|
||||
}
|
||||
|
||||
public struct PeerGroupUnreadCountersCombinedSummary: PostboxCoding, Equatable {
|
||||
public enum CountingCategory {
|
||||
case chats
|
||||
case messages
|
||||
}
|
||||
|
||||
public enum MuteCategory {
|
||||
case all
|
||||
}
|
||||
|
||||
public var namespaces: [MessageId.Namespace: PeerGroupUnreadCountersSummary]
|
||||
|
||||
public init(namespaces: [MessageId.Namespace: PeerGroupUnreadCountersSummary]) {
|
||||
self.namespaces = namespaces
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.namespaces = decoder.decodeObjectDictionaryForKey("n", keyDecoder: { $0.decodeInt32ForKey("k", orElse: 0) }, valueDecoder: { PeerGroupUnreadCountersSummary(decoder: $0) })
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObjectDictionary(self.namespaces, forKey: "n", keyEncoder: { $1.encodeInt32($0, forKey: "k") })
|
||||
}
|
||||
|
||||
public func count(countingCategory: CountingCategory, mutedCategory: MuteCategory) -> Int32 {
|
||||
var result: Int32 = 0
|
||||
for (_, summary) in self.namespaces {
|
||||
switch mutedCategory {
|
||||
case .all:
|
||||
switch countingCategory {
|
||||
case .chats:
|
||||
result = result &+ summary.all.chatCount
|
||||
case .messages:
|
||||
result = result &+ summary.all.messageCount
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListTotalUnreadState: PostboxCoding, Equatable {
|
||||
public var absoluteCounters: [PeerSummaryCounterTags: ChatListTotalUnreadCounters]
|
||||
public var filteredCounters: [PeerSummaryCounterTags: ChatListTotalUnreadCounters]
|
||||
|
||||
public init(absoluteCounters: [PeerSummaryCounterTags: ChatListTotalUnreadCounters], filteredCounters: [PeerSummaryCounterTags: ChatListTotalUnreadCounters]) {
|
||||
self.absoluteCounters = absoluteCounters
|
||||
self.filteredCounters = filteredCounters
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.absoluteCounters = decoder.decodeObjectDictionaryForKey("ad", keyDecoder: { decoder in
|
||||
return PeerSummaryCounterTags(rawValue: decoder.decodeInt32ForKey("k", orElse: 0))
|
||||
}, valueDecoder: { decoder in
|
||||
return ChatListTotalUnreadCounters(decoder: decoder)
|
||||
})
|
||||
self.filteredCounters = decoder.decodeObjectDictionaryForKey("fd", keyDecoder: { decoder in
|
||||
return PeerSummaryCounterTags(rawValue: decoder.decodeInt32ForKey("k", orElse: 0))
|
||||
}, valueDecoder: { decoder in
|
||||
return ChatListTotalUnreadCounters(decoder: decoder)
|
||||
})
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeObjectDictionary(self.absoluteCounters, forKey: "ad", keyEncoder: { key, encoder in
|
||||
encoder.encodeInt32(key.rawValue, forKey: "k")
|
||||
})
|
||||
encoder.encodeObjectDictionary(self.filteredCounters, forKey: "fd", keyEncoder: { key, encoder in
|
||||
encoder.encodeInt32(key.rawValue, forKey: "k")
|
||||
})
|
||||
}
|
||||
|
||||
public func count(for category: ChatListTotalUnreadStateCategory, in statsType: ChatListTotalUnreadStateStats, with tags: PeerSummaryCounterTags) -> Int32 {
|
||||
let counters: [PeerSummaryCounterTags: ChatListTotalUnreadCounters]
|
||||
switch category {
|
||||
case .raw:
|
||||
counters = self.absoluteCounters
|
||||
case .filtered:
|
||||
counters = self.filteredCounters
|
||||
}
|
||||
var result: Int32 = 0
|
||||
for tag in tags {
|
||||
if let category = counters[tag] {
|
||||
switch statsType {
|
||||
case .messages:
|
||||
result = result &+ category.messageCount
|
||||
case .chats:
|
||||
result = result &+ category.chatCount
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
final class GroupMessageStatsTable: Table {
|
||||
private var cachedEntries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary]?
|
||||
private var updatedGroupIds = Set<PeerGroupId>()
|
||||
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private func preloadCache() {
|
||||
if self.cachedEntries == nil {
|
||||
var entries: [PeerGroupId: PeerGroupUnreadCountersCombinedSummary] = [:]
|
||||
self.valueBox.scanInt64(self.table, values: { key, value in
|
||||
let groupIdValue: Int32 = Int32(clamping: key)
|
||||
let groupId = PeerGroupId(rawValue: groupIdValue)
|
||||
let state = PeerGroupUnreadCountersCombinedSummary(decoder: PostboxDecoder(buffer: value))
|
||||
entries[groupId] = state
|
||||
return true
|
||||
})
|
||||
self.cachedEntries = entries
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
self.preloadCache()
|
||||
|
||||
for groupId in self.cachedEntries!.keys {
|
||||
self.set(groupId: groupId, summary: PeerGroupUnreadCountersCombinedSummary(namespaces: [:]))
|
||||
}
|
||||
}
|
||||
|
||||
func get(groupId: PeerGroupId) -> PeerGroupUnreadCountersCombinedSummary {
|
||||
self.preloadCache()
|
||||
|
||||
if let state = self.cachedEntries?[groupId] {
|
||||
return state
|
||||
} else {
|
||||
return PeerGroupUnreadCountersCombinedSummary(namespaces: [:])
|
||||
}
|
||||
}
|
||||
|
||||
func set(groupId: PeerGroupId, summary: PeerGroupUnreadCountersCombinedSummary) {
|
||||
self.preloadCache()
|
||||
|
||||
let previousSummary = self.get(groupId: groupId)
|
||||
if previousSummary != summary {
|
||||
self.cachedEntries![groupId] = summary
|
||||
self.updatedGroupIds.insert(groupId)
|
||||
}
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedEntries = nil
|
||||
assert(self.updatedGroupIds.isEmpty)
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if !self.updatedGroupIds.isEmpty {
|
||||
if let cachedEntries = self.cachedEntries {
|
||||
let sharedKey = ValueBoxKey(length: 8)
|
||||
let sharedEncoder = PostboxEncoder()
|
||||
for groupId in self.updatedGroupIds {
|
||||
sharedKey.setInt64(0, value: Int64(groupId.rawValue))
|
||||
sharedEncoder.reset()
|
||||
|
||||
if let state = cachedEntries[groupId] {
|
||||
state.encode(sharedEncoder)
|
||||
self.valueBox.set(self.table, key: sharedKey, value: sharedEncoder.readBufferNoCopy())
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: sharedKey, secure: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
self.updatedGroupIds.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
12
submodules/Postbox/Postbox/Hash.swift
Normal file
12
submodules/Postbox/Postbox/Hash.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import Foundation
|
||||
import sqlcipher
|
||||
|
||||
public enum HashFunctions {
|
||||
public static func murMurHash32(_ s: String) -> Int32 {
|
||||
return murMurHashString32(s)
|
||||
}
|
||||
|
||||
public static func murMurHash32(_ d: Data) -> Int32 {
|
||||
return murMurHash32Data(d)
|
||||
}
|
||||
}
|
||||
26
submodules/Postbox/Postbox/Info.plist
Normal file
26
submodules/Postbox/Postbox/Info.plist
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
public struct InitialMessageHistoryData {
|
||||
public let peer: Peer?
|
||||
public let chatInterfaceState: PeerChatInterfaceState?
|
||||
public let associatedMessages: [MessageId: Message]
|
||||
}
|
||||
85
submodules/Postbox/Postbox/IntermediateMessage.swift
Normal file
85
submodules/Postbox/Postbox/IntermediateMessage.swift
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import Foundation
|
||||
|
||||
struct IntermediateMessageForwardInfo {
|
||||
let authorId: PeerId?
|
||||
let sourceId: PeerId?
|
||||
let sourceMessageId: MessageId?
|
||||
let date: Int32
|
||||
let authorSignature: String?
|
||||
|
||||
init(authorId: PeerId?, sourceId: PeerId?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?) {
|
||||
self.authorId = authorId
|
||||
self.sourceId = sourceId
|
||||
self.sourceMessageId = sourceMessageId
|
||||
self.date = date
|
||||
self.authorSignature = authorSignature
|
||||
}
|
||||
|
||||
init(_ storeInfo: StoreMessageForwardInfo) {
|
||||
self.authorId = storeInfo.authorId
|
||||
self.sourceId = storeInfo.sourceId
|
||||
self.sourceMessageId = storeInfo.sourceMessageId
|
||||
self.date = storeInfo.date
|
||||
self.authorSignature = storeInfo.authorSignature
|
||||
}
|
||||
}
|
||||
|
||||
class IntermediateMessage {
|
||||
let stableId: UInt32
|
||||
let stableVersion: UInt32
|
||||
let id: MessageId
|
||||
let globallyUniqueId: Int64?
|
||||
let groupingKey: Int64?
|
||||
let groupInfo: MessageGroupInfo?
|
||||
let timestamp: Int32
|
||||
let flags: MessageFlags
|
||||
let tags: MessageTags
|
||||
let globalTags: GlobalMessageTags
|
||||
let localTags: LocalMessageTags
|
||||
let forwardInfo: IntermediateMessageForwardInfo?
|
||||
let authorId: PeerId?
|
||||
let text: String
|
||||
let attributesData: ReadBuffer
|
||||
let embeddedMediaData: ReadBuffer
|
||||
let referencedMedia: [MediaId]
|
||||
|
||||
var index: MessageIndex {
|
||||
return MessageIndex(id: self.id, timestamp: self.timestamp)
|
||||
}
|
||||
|
||||
init(stableId: UInt32, stableVersion: UInt32, id: MessageId, globallyUniqueId: Int64?, groupingKey: Int64?, groupInfo: MessageGroupInfo?, timestamp: Int32, flags: MessageFlags, tags: MessageTags, globalTags: GlobalMessageTags, localTags: LocalMessageTags, forwardInfo: IntermediateMessageForwardInfo?, authorId: PeerId?, text: String, attributesData: ReadBuffer, embeddedMediaData: ReadBuffer, referencedMedia: [MediaId]) {
|
||||
self.stableId = stableId
|
||||
self.stableVersion = stableVersion
|
||||
self.id = id
|
||||
self.globallyUniqueId = globallyUniqueId
|
||||
self.groupingKey = groupingKey
|
||||
self.groupInfo = groupInfo
|
||||
self.timestamp = timestamp
|
||||
self.flags = flags
|
||||
self.tags = tags
|
||||
self.globalTags = globalTags
|
||||
self.localTags = localTags
|
||||
self.forwardInfo = forwardInfo
|
||||
self.authorId = authorId
|
||||
self.text = text
|
||||
self.attributesData = attributesData
|
||||
self.embeddedMediaData = embeddedMediaData
|
||||
self.referencedMedia = referencedMedia
|
||||
}
|
||||
|
||||
func withUpdatedTimestamp(_ timestamp: Int32) -> IntermediateMessage {
|
||||
return IntermediateMessage(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributesData: self.attributesData, embeddedMediaData: self.embeddedMediaData, referencedMedia: self.referencedMedia)
|
||||
}
|
||||
|
||||
func withUpdatedGroupingKey(_ groupingKey: Int64?) -> IntermediateMessage {
|
||||
return IntermediateMessage(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributesData: self.attributesData, embeddedMediaData: self.embeddedMediaData, referencedMedia: self.referencedMedia)
|
||||
}
|
||||
|
||||
func withUpdatedGroupInfo(_ groupInfo: MessageGroupInfo?) -> IntermediateMessage {
|
||||
return IntermediateMessage(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributesData: self.attributesData, embeddedMediaData: self.embeddedMediaData, referencedMedia: self.referencedMedia)
|
||||
}
|
||||
|
||||
func withUpdatedEmbeddedMedia(_ embeddedMedia: ReadBuffer) -> IntermediateMessage {
|
||||
return IntermediateMessage(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributesData: self.attributesData, embeddedMediaData: embeddedMedia, referencedMedia: self.referencedMedia)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import Foundation
|
||||
|
||||
public struct PeerGroupAndNamespace: Hashable {
|
||||
public let groupId: PeerGroupId
|
||||
public let namespace: MessageId.Namespace
|
||||
|
||||
public init(groupId: PeerGroupId, namespace: MessageId.Namespace) {
|
||||
self.groupId = groupId
|
||||
self.namespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
final class InvalidatedGroupMessageStatsTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
|
||||
private func key(groupId: PeerGroupId, namespace: MessageId.Namespace) -> ValueBoxKey {
|
||||
self.sharedKey.setInt32(0, value: groupId.rawValue)
|
||||
self.sharedKey.setInt32(4, value: namespace)
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private var updatedGroupIds: [PeerGroupAndNamespace: Bool] = [:]
|
||||
|
||||
func set(groupId: PeerGroupId, namespace: MessageId.Namespace, needsValidation: Bool, operations: inout [PeerGroupAndNamespace: Bool]) {
|
||||
let key = PeerGroupAndNamespace(groupId: groupId, namespace: namespace)
|
||||
self.updatedGroupIds[key] = needsValidation
|
||||
operations[key] = needsValidation
|
||||
}
|
||||
|
||||
func get() -> Set<PeerGroupAndNamespace> {
|
||||
self.beforeCommit()
|
||||
|
||||
var result = Set<PeerGroupAndNamespace>()
|
||||
self.valueBox.scan(self.table, keys: { key in
|
||||
result.insert(PeerGroupAndNamespace(groupId: PeerGroupId(rawValue: key.getInt32(0)), namespace: key.getInt32(4)))
|
||||
return true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if !self.updatedGroupIds.isEmpty {
|
||||
for (groupIdAndNamespace, needsValidation) in self.updatedGroupIds {
|
||||
if needsValidation {
|
||||
self.valueBox.set(self.table, key: self.key(groupId: groupIdAndNamespace.groupId, namespace: groupIdAndNamespace.namespace), value: MemoryBuffer(data: Data()))
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: self.key(groupId: groupIdAndNamespace.groupId, namespace: groupIdAndNamespace.namespace), secure: false)
|
||||
}
|
||||
}
|
||||
self.updatedGroupIds.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
|
||||
final class MutableInvalidatedMessageHistoryTagSummariesView: MutablePostboxView {
|
||||
private let namespace: MessageId.Namespace
|
||||
private let tagMask: MessageTags
|
||||
|
||||
var entries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
|
||||
|
||||
init(postbox: Postbox, tagMask: MessageTags, namespace: MessageId.Namespace) {
|
||||
self.tagMask = tagMask
|
||||
self.namespace = namespace
|
||||
|
||||
for entry in postbox.invalidatedMessageHistoryTagsSummaryTable.get(tagMask: tagMask, namespace: namespace) {
|
||||
self.entries.insert(entry)
|
||||
}
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
for operation in transaction.currentInvalidateMessageTagSummaries {
|
||||
switch operation {
|
||||
case let .add(entry):
|
||||
if entry.key.namespace == self.namespace && entry.key.tagMask == self.tagMask {
|
||||
self.entries.insert(entry)
|
||||
updated = true
|
||||
}
|
||||
case let .remove(key):
|
||||
if key.namespace == self.namespace && key.tagMask == self.tagMask {
|
||||
for entry in self.entries {
|
||||
if entry.key == key {
|
||||
self.entries.remove(entry)
|
||||
break
|
||||
}
|
||||
}
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return InvalidatedMessageHistoryTagSummariesView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class InvalidatedMessageHistoryTagSummariesView: PostboxView {
|
||||
public let entries: Set<InvalidatedMessageHistoryTagsSummaryEntry>
|
||||
|
||||
init(_ view: MutableInvalidatedMessageHistoryTagSummariesView) {
|
||||
self.entries = view.entries
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import Foundation
|
||||
|
||||
public struct InvalidatedMessageHistoryTagsSummaryKey: Equatable, Hashable {
|
||||
public let peerId: PeerId
|
||||
public let namespace: MessageId.Namespace
|
||||
public let tagMask: MessageTags
|
||||
|
||||
public init(peerId: PeerId, namespace: MessageId.Namespace, tagMask: MessageTags) {
|
||||
self.peerId = peerId
|
||||
self.namespace = namespace
|
||||
self.tagMask = tagMask
|
||||
}
|
||||
}
|
||||
|
||||
public struct InvalidatedMessageHistoryTagsSummaryEntry: Equatable, Hashable {
|
||||
public let key: InvalidatedMessageHistoryTagsSummaryKey
|
||||
public let version: Int32
|
||||
}
|
||||
|
||||
enum InvalidatedMessageHistoryTagsSummaryEntryOperation {
|
||||
case add(InvalidatedMessageHistoryTagsSummaryEntry)
|
||||
case remove(InvalidatedMessageHistoryTagsSummaryKey)
|
||||
}
|
||||
|
||||
final class InvalidatedMessageHistoryTagsSummaryTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private func key(_ key: InvalidatedMessageHistoryTagsSummaryKey) -> ValueBoxKey {
|
||||
let result = ValueBoxKey(length: 4 + 4 + 8)
|
||||
result.setUInt32(0, value: key.tagMask.rawValue)
|
||||
result.setInt32(4, value: key.namespace)
|
||||
result.setInt64(4 + 4, value: key.peerId.toInt64())
|
||||
return result
|
||||
}
|
||||
|
||||
private func lowerBound(tagMask: MessageTags, namespace: MessageId.Namespace) -> ValueBoxKey {
|
||||
let result = ValueBoxKey(length: 4 + 4)
|
||||
result.setUInt32(0, value: tagMask.rawValue)
|
||||
result.setInt32(4, value: namespace)
|
||||
return result
|
||||
}
|
||||
|
||||
private func upperBound(tagMask: MessageTags, namespace: MessageId.Namespace) -> ValueBoxKey {
|
||||
return self.lowerBound(tagMask: tagMask, namespace: namespace).successor
|
||||
}
|
||||
|
||||
func get(tagMask: MessageTags, namespace: MessageId.Namespace) -> [InvalidatedMessageHistoryTagsSummaryEntry] {
|
||||
var entries: [InvalidatedMessageHistoryTagsSummaryEntry] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(tagMask: tagMask, namespace: namespace), end: self.upperBound(tagMask: tagMask, namespace: namespace), values: { key, value in
|
||||
var version: Int32 = 0
|
||||
value.read(&version, offset: 0, length: 4)
|
||||
entries.append(InvalidatedMessageHistoryTagsSummaryEntry(key: InvalidatedMessageHistoryTagsSummaryKey(peerId: PeerId(key.getInt64(4 + 4)), namespace: key.getInt32(4), tagMask: MessageTags(rawValue: key.getUInt32(0))), version: version))
|
||||
return true
|
||||
}, limit: 0)
|
||||
return entries
|
||||
}
|
||||
|
||||
private func get(_ key: InvalidatedMessageHistoryTagsSummaryKey) -> InvalidatedMessageHistoryTagsSummaryEntry? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(key)) {
|
||||
var version: Int32 = 0
|
||||
value.read(&version, offset: 0, length: 4)
|
||||
return InvalidatedMessageHistoryTagsSummaryEntry(key: key, version: version)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func insert(_ key: InvalidatedMessageHistoryTagsSummaryKey, operations: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation]) {
|
||||
var version: Int32 = 0
|
||||
if let entry = self.get(key) {
|
||||
self.remove(entry, operations: &operations)
|
||||
version = entry.version + 1
|
||||
}
|
||||
self.valueBox.set(self.table, key: self.key(key), value: MemoryBuffer(memory: &version, capacity: 4, length: 4, freeWhenDone: false))
|
||||
operations.append(.add(InvalidatedMessageHistoryTagsSummaryEntry(key: key, version: version)))
|
||||
}
|
||||
|
||||
func remove(_ entry: InvalidatedMessageHistoryTagsSummaryEntry, operations: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation]) {
|
||||
if let current = self.get(entry.key), current.version == entry.version {
|
||||
self.valueBox.remove(self.table, key: self.key(entry.key), secure: false)
|
||||
operations.append(.remove(entry.key))
|
||||
}
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
}
|
||||
}
|
||||
14
submodules/Postbox/Postbox/IpcNotifier.h
Normal file
14
submodules/Postbox/Postbox/IpcNotifier.h
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#ifndef IpcNotifier_h
|
||||
#define IpcNotifier_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface RLMNotifier : NSObject
|
||||
|
||||
- (instancetype _Nonnull)initWithBasePath:(NSString * _Nonnull)basePath notify:(void (^ _Nonnull)())notify;
|
||||
- (void)listen;
|
||||
- (void)notifyOtherRealms;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
195
submodules/Postbox/Postbox/IpcNotifier.mm
Normal file
195
submodules/Postbox/Postbox/IpcNotifier.mm
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
#import <sys/event.h>
|
||||
#import <sys/stat.h>
|
||||
#import <sys/time.h>
|
||||
#import <sys/errno.h>
|
||||
#import <unistd.h>
|
||||
|
||||
#import "IpcNotifier.h"
|
||||
|
||||
// Write a byte to a pipe to notify anyone waiting for data on the pipe
|
||||
static void notifyFd(int fd) {
|
||||
while (true) {
|
||||
char c = 0;
|
||||
ssize_t ret = write(fd, &c, 1);
|
||||
if (ret == 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the pipe's buffer is full, we need to read some of the old data in
|
||||
// it to make space. We don't just read in the code waiting for
|
||||
// notifications so that we can notify multiple waiters with a single
|
||||
// write.
|
||||
assert(ret == -1 && errno == EAGAIN);
|
||||
char buff[1024];
|
||||
read(fd, buff, sizeof buff);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A RAII holder for a file descriptor which automatically closes the wrapped fd
|
||||
// when it's deallocated
|
||||
class FdHolder {
|
||||
int fd = -1;
|
||||
void close() {
|
||||
if (fd != -1) {
|
||||
::close(fd);
|
||||
}
|
||||
fd = -1;
|
||||
}
|
||||
|
||||
FdHolder& operator=(FdHolder const&) = delete;
|
||||
FdHolder(FdHolder const&) = delete;
|
||||
|
||||
public:
|
||||
FdHolder() { }
|
||||
~FdHolder() { close(); }
|
||||
operator int() const { return fd; }
|
||||
|
||||
FdHolder& operator=(int newFd) {
|
||||
close();
|
||||
fd = newFd;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Inter-thread and inter-process notifications of changes are done using a
|
||||
// named pipe in the filesystem next to the Realm file. Everyone who wants to be
|
||||
// notified of commits waits for data to become available on the pipe, and anyone
|
||||
// who commits a write transaction writes data to the pipe after releasing the
|
||||
// write lock. Note that no one ever actually *reads* from the pipe: the data
|
||||
// actually written is meaningless, and trying to read from a pipe from multiple
|
||||
// processes at once is fraught with race conditions.
|
||||
|
||||
// When a RLMRealm instance is created, we add a CFRunLoopSource to the current
|
||||
// thread's runloop. On each cycle of the run loop, the run loop checks each of
|
||||
// its sources for work to do, which in the case of CFRunLoopSource is just
|
||||
// checking if CFRunLoopSourceSignal has been called since the last time it ran,
|
||||
// and if so invokes the function pointer supplied when the source is created,
|
||||
// which in our case just invokes `[realm handleExternalChange]`.
|
||||
|
||||
// Listening for external changes is done using kqueue() on a background thread.
|
||||
// kqueue() lets us efficiently wait until the amount of data which can be read
|
||||
// from one or more file descriptors has changed, and tells us which of the file
|
||||
// descriptors it was that changed. We use this to wait on both the shared named
|
||||
// pipe, and a local anonymous pipe. When data is written to the named pipe, we
|
||||
// signal the runloop source and wake up the target runloop, and when data is
|
||||
// written to the anonymous pipe the background thread removes the runloop
|
||||
// source from the runloop and and shuts down.
|
||||
|
||||
@implementation RLMNotifier {
|
||||
// Realm to notify of changes
|
||||
void (^_notify)();
|
||||
// Runloop which notifications are delivered on
|
||||
CFRunLoopRef _runLoop;
|
||||
|
||||
// Read-write file descriptor for the named pipe which is waited on for
|
||||
// changes and written to when a commit is made
|
||||
FdHolder _notifyFd;
|
||||
// File descriptor for the kqueue
|
||||
FdHolder _kq;
|
||||
// The two ends of an anonymous pipe used to notify the kqueue() thread that
|
||||
// it should be shut down.
|
||||
FdHolder _shutdownReadFd;
|
||||
FdHolder _shutdownWriteFd;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBasePath:(NSString *)basePath notify:( void (^ _Nonnull)())notify {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_notify = [notify copy];
|
||||
|
||||
_kq = kqueue();
|
||||
if (_kq == -1) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *path = [basePath stringByAppendingString:@"postbox.note"].UTF8String;
|
||||
|
||||
// Create and open the named pipe
|
||||
int ret = mkfifo(path, 0600);
|
||||
if (ret == -1) {
|
||||
int err = errno;
|
||||
if (err == ENOTSUP) {
|
||||
// Filesystem doesn't support named pipes, so try putting it in tmp instead
|
||||
// Hash collisions are okay here because they just result in doing
|
||||
// extra work, as opposed to correctness problems
|
||||
static NSString *tmpDir = NSTemporaryDirectory();
|
||||
path = [tmpDir stringByAppendingFormat:@"poxtbox_%llu.note", (unsigned long long)[basePath hash]].UTF8String;
|
||||
ret = mkfifo(path, 0600);
|
||||
err = errno;
|
||||
}
|
||||
// the fifo already existing isn't an error
|
||||
if (ret == -1 && err != EEXIST) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
_notifyFd = open(path, O_RDWR);
|
||||
if (_notifyFd == -1) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Make writing to the pipe return -1 when the pipe's buffer is full
|
||||
// rather than blocking until there's space available
|
||||
ret = fcntl(_notifyFd, F_SETFL, O_NONBLOCK);
|
||||
if (ret == -1) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Create the anonymous pipe
|
||||
int pipeFd[2];
|
||||
ret = pipe(pipeFd);
|
||||
if (ret == -1) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_shutdownReadFd = pipeFd[0];
|
||||
_shutdownWriteFd = pipeFd[1];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)listen {
|
||||
// Set up the kqueue
|
||||
// EVFILT_READ indicates that we care about data being available to read
|
||||
// on the given file descriptor.
|
||||
// EV_CLEAR makes it wait for the amount of data available to be read to
|
||||
// change rather than just returning when there is any data to read.
|
||||
struct kevent ke[2];
|
||||
EV_SET(&ke[0], _notifyFd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, 0);
|
||||
EV_SET(&ke[1], _shutdownReadFd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, 0);
|
||||
int ret = kevent(_kq, ke, 2, nullptr, 0, nullptr);
|
||||
assert(ret == 0);
|
||||
|
||||
while (true) {
|
||||
struct kevent event;
|
||||
// Wait for data to become on either fd
|
||||
// Return code is number of bytes available or -1 on error
|
||||
ret = kevent(_kq, nullptr, 0, &event, 1, nullptr);
|
||||
assert(ret >= 0);
|
||||
if (ret == 0) {
|
||||
// Spurious wakeup; just wait again
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check which file descriptor had activity: if it's the shutdown
|
||||
// pipe, then someone called -stop; otherwise it's the named pipe
|
||||
// and someone committed a write transaction
|
||||
if (event.ident == (uint32_t)_shutdownReadFd) {
|
||||
return;
|
||||
}
|
||||
assert(event.ident == (uint32_t)_notifyFd);
|
||||
|
||||
_notify();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stop {
|
||||
notifyFd(_shutdownWriteFd);
|
||||
}
|
||||
|
||||
- (void)notifyOtherRealms {
|
||||
notifyFd(_notifyFd);
|
||||
}
|
||||
@end
|
||||
60
submodules/Postbox/Postbox/IpcPipe.swift
Normal file
60
submodules/Postbox/Postbox/IpcPipe.swift
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import Foundation
|
||||
#if os(macOS)
|
||||
import SwiftSignalKitMac
|
||||
#else
|
||||
import SwiftSignalKit
|
||||
#endif
|
||||
|
||||
func ipcNotify(basePath: String, data: Int64) {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
let path = basePath + ".ipc"
|
||||
let fd = open(path, open(path, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR))
|
||||
if fd != -1 {
|
||||
var value = data
|
||||
write(fd, &value, 8)
|
||||
close(fd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ipcNotifications(basePath: String) -> Signal<Int64, Void> {
|
||||
return Signal { subscriber in
|
||||
let queue = Queue()
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
queue.async {
|
||||
let path = basePath + ".ipc"
|
||||
let fd = open(path, open(path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR))
|
||||
if fd != -1 {
|
||||
let readSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: [.write])
|
||||
|
||||
var previousValue: Int64 = 0
|
||||
readSource.setEventHandler(handler: {
|
||||
subscriber.putNext(Int64.max)
|
||||
/*lseek(fd, 0, SEEK_SET)
|
||||
var value: Int64 = 0
|
||||
if read(fd, &value, 8) == 8 {
|
||||
if previousValue != value {
|
||||
previousValue = value
|
||||
subscriber.putNext(value)
|
||||
}
|
||||
}*/
|
||||
})
|
||||
|
||||
readSource.resume()
|
||||
|
||||
disposable.set(ActionDisposable {
|
||||
queue.async {
|
||||
readSource.cancel()
|
||||
close(fd)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
subscriber.putError(Void())
|
||||
}
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
82
submodules/Postbox/Postbox/ItemCacheMetaTable.swift
Normal file
82
submodules/Postbox/Postbox/ItemCacheMetaTable.swift
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import Foundation
|
||||
|
||||
public typealias ItemCacheCollectionId = Int8
|
||||
|
||||
public struct ItemCacheCollectionSpec {
|
||||
public let lowWaterItemCount: Int32
|
||||
public let highWaterItemCount: Int32
|
||||
|
||||
public init(lowWaterItemCount: Int32, highWaterItemCount: Int32) {
|
||||
self.lowWaterItemCount = lowWaterItemCount
|
||||
self.highWaterItemCount = highWaterItemCount
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemCacheCollectionState: PostboxCoding {
|
||||
let nextAccessIndex: Int32
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
self.nextAccessIndex = decoder.decodeInt32ForKey("i", orElse: 0)
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.nextAccessIndex, forKey: "i")
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemCacheMetaTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
|
||||
private var cachedCollectionStates: [ItemCacheCollectionId: ItemCacheCollectionState] = [:]
|
||||
private var updatedCollectionStateIds = Set<ItemCacheCollectionId>()
|
||||
|
||||
private func key(_ id: ItemCacheCollectionId) -> ValueBoxKey {
|
||||
self.sharedKey.setInt64(0, value: Int64(id))
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private func get(_ id: ItemCacheCollectionId) -> ItemCacheCollectionState? {
|
||||
if let cached = self.cachedCollectionStates[id] {
|
||||
return cached
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(id)), let state = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCacheCollectionState {
|
||||
self.cachedCollectionStates[id] = state
|
||||
return state
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func set(_ id: ItemCacheCollectionId, state: ItemCacheCollectionState) {
|
||||
self.cachedCollectionStates[id] = state
|
||||
self.updatedCollectionStateIds.insert(id)
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedCollectionStates.removeAll()
|
||||
self.updatedCollectionStateIds.removeAll()
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if !self.updatedCollectionStateIds.isEmpty {
|
||||
let sharedEncoder = PostboxEncoder()
|
||||
for id in self.updatedCollectionStateIds {
|
||||
if let state = self.cachedCollectionStates[id] {
|
||||
sharedEncoder.reset()
|
||||
sharedEncoder.encodeRootObject(state)
|
||||
withExtendedLifetime(sharedEncoder, {
|
||||
self.valueBox.set(self.table, key: self.key(id), value: sharedEncoder.readBufferNoCopy())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updatedCollectionStateIds.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
82
submodules/Postbox/Postbox/ItemCacheTable.swift
Normal file
82
submodules/Postbox/Postbox/ItemCacheTable.swift
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import Foundation
|
||||
|
||||
public final class ItemCacheEntryId: Equatable, Hashable {
|
||||
public let collectionId: ItemCacheCollectionId
|
||||
public let key: ValueBoxKey
|
||||
|
||||
public init(collectionId: ItemCacheCollectionId, key: ValueBoxKey) {
|
||||
self.collectionId = collectionId
|
||||
self.key = key
|
||||
}
|
||||
|
||||
public static func ==(lhs: ItemCacheEntryId, rhs: ItemCacheEntryId) -> Bool {
|
||||
return lhs.collectionId == rhs.collectionId && lhs.key == rhs.key
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.collectionId.hashValue &* 31 &+ self.key.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
private enum ItemCacheSection: Int8 {
|
||||
case items = 0
|
||||
case accessIndexToItemId = 1
|
||||
case itemIdToAccessIndex = 2
|
||||
}
|
||||
|
||||
final class ItemCacheTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private func itemKey(id: ItemCacheEntryId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 1 + 1 + id.key.length)
|
||||
key.setInt8(0, value: ItemCacheSection.items.rawValue)
|
||||
key.setInt8(1, value: id.collectionId)
|
||||
memcpy(key.memory.advanced(by: 2), id.key.memory, id.key.length)
|
||||
return key
|
||||
}
|
||||
|
||||
private func itemIdToAccessIndexKey(id: ItemCacheEntryId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 1 + 1 + id.key.length)
|
||||
key.setInt8(0, value: ItemCacheSection.accessIndexToItemId.rawValue)
|
||||
key.setInt8(1, value: id.collectionId)
|
||||
memcpy(key.memory.advanced(by: 2), id.key.memory, id.key.length)
|
||||
return key
|
||||
}
|
||||
|
||||
private func accessIndexToItemId(collectionId: ItemCacheCollectionId, index: Int32) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 1 + 1 + 4)
|
||||
key.setInt8(0, value: ItemCacheSection.accessIndexToItemId.rawValue)
|
||||
key.setInt8(1, value: collectionId)
|
||||
key.setInt32(2, value: index)
|
||||
return key
|
||||
}
|
||||
|
||||
func put(id: ItemCacheEntryId, entry: PostboxCoding, metaTable: ItemCacheMetaTable) {
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(entry)
|
||||
withExtendedLifetime(encoder, {
|
||||
self.valueBox.set(self.table, key: self.itemKey(id: id), value: encoder.readBufferNoCopy())
|
||||
})
|
||||
}
|
||||
|
||||
func retrieve(id: ItemCacheEntryId, metaTable: ItemCacheMetaTable) -> PostboxCoding? {
|
||||
if let value = self.valueBox.get(self.table, key: self.itemKey(id: id)), let entry = PostboxDecoder(buffer: value).decodeRootObject() {
|
||||
return entry
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func remove(id: ItemCacheEntryId, metaTable: ItemCacheMetaTable) {
|
||||
self.valueBox.remove(self.table, key: self.itemKey(id: id), secure: false)
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
|
||||
}
|
||||
}
|
||||
106
submodules/Postbox/Postbox/ItemCollection.swift
Normal file
106
submodules/Postbox/Postbox/ItemCollection.swift
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import Foundation
|
||||
|
||||
public struct ItemCollectionId: Comparable, Hashable {
|
||||
public typealias Namespace = Int32
|
||||
public typealias Id = Int64
|
||||
|
||||
public let namespace: ItemCollectionId.Namespace
|
||||
public let id: ItemCollectionId.Id
|
||||
|
||||
public init(namespace: ItemCollectionId.Namespace, id: ItemCollectionId.Id) {
|
||||
self.namespace = namespace
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public static func ==(lhs: ItemCollectionId, rhs: ItemCollectionId) -> Bool {
|
||||
return lhs.namespace == rhs.namespace && lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public static func <(lhs: ItemCollectionId, rhs: ItemCollectionId) -> Bool {
|
||||
if lhs.namespace == rhs.namespace {
|
||||
return lhs.id < rhs.id
|
||||
} else {
|
||||
return lhs.namespace < rhs.namespace
|
||||
}
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.id.hashValue
|
||||
}
|
||||
|
||||
public static func encodeArrayToBuffer(_ array: [ItemCollectionId], buffer: WriteBuffer) {
|
||||
var length: Int32 = Int32(array.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
for id in array {
|
||||
var idNamespace = id.namespace
|
||||
buffer.write(&idNamespace, offset: 0, length: 4)
|
||||
var idId = id.id
|
||||
buffer.write(&idId, offset: 0, length: 8)
|
||||
}
|
||||
}
|
||||
|
||||
public static func decodeArrayFromBuffer(_ buffer: ReadBuffer) -> [ItemCollectionId] {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, buffer.memory, 4)
|
||||
buffer.offset += 4
|
||||
var i = 0
|
||||
var array: [ItemCollectionId] = []
|
||||
array.reserveCapacity(Int(length))
|
||||
while i < Int(length) {
|
||||
var idNamespace: Int32 = 0
|
||||
buffer.read(&idNamespace, offset: 0, length: 4)
|
||||
var idId: Int64 = 0
|
||||
buffer.read(&idId, offset: 0, length: 8)
|
||||
array.append(ItemCollectionId(namespace: idNamespace, id: idId))
|
||||
i += 1
|
||||
}
|
||||
return array
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ItemCollectionInfo: PostboxCoding {
|
||||
|
||||
}
|
||||
|
||||
public struct ItemCollectionItemIndex: Comparable, Hashable {
|
||||
public typealias Index = Int32
|
||||
public typealias Id = Int64
|
||||
|
||||
public let index: ItemCollectionItemIndex.Index
|
||||
public let id: ItemCollectionItemIndex.Id
|
||||
|
||||
public init(index: ItemCollectionItemIndex.Index, id: ItemCollectionItemIndex.Id) {
|
||||
self.index = index
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public static func ==(lhs: ItemCollectionItemIndex, rhs: ItemCollectionItemIndex) -> Bool {
|
||||
return lhs.index == rhs.index && lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public static func <(lhs: ItemCollectionItemIndex, rhs: ItemCollectionItemIndex) -> Bool {
|
||||
if lhs.index == rhs.index {
|
||||
return lhs.id < rhs.id
|
||||
} else {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
}
|
||||
|
||||
static var lowerBound: ItemCollectionItemIndex {
|
||||
return ItemCollectionItemIndex(index: 0, id: 0)
|
||||
}
|
||||
|
||||
static var upperBound: ItemCollectionItemIndex {
|
||||
return ItemCollectionItemIndex(index: Int32.max, id: Int64.max)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ItemCollectionItem: PostboxCoding {
|
||||
var index: ItemCollectionItemIndex { get }
|
||||
var indexKeys: [MemoryBuffer] { get }
|
||||
}
|
||||
|
||||
public enum ItemCollectionSearchQuery {
|
||||
case exact(ValueBoxKey)
|
||||
case matching([ValueBoxKey])
|
||||
}
|
||||
55
submodules/Postbox/Postbox/ItemCollectionIdsView.swift
Normal file
55
submodules/Postbox/Postbox/ItemCollectionIdsView.swift
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableItemCollectionIdsView: MutablePostboxView {
|
||||
let namespaces: [ItemCollectionId.Namespace]
|
||||
var idsByNamespace: [ItemCollectionId.Namespace: Set<ItemCollectionId>]
|
||||
|
||||
init(postbox: Postbox, namespaces: [ItemCollectionId.Namespace]) {
|
||||
self.namespaces = namespaces
|
||||
|
||||
var idsByNamespace: [ItemCollectionId.Namespace: Set<ItemCollectionId>] = [:]
|
||||
for namespace in namespaces {
|
||||
let ids = postbox.itemCollectionInfoTable.getIds(namespace: namespace)
|
||||
idsByNamespace[namespace] = Set(ids)
|
||||
}
|
||||
self.idsByNamespace = idsByNamespace
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
if transaction.currentItemCollectionInfosOperations.isEmpty {
|
||||
return false
|
||||
}
|
||||
|
||||
var updated = false
|
||||
|
||||
var reloadInfosNamespaces = Set<ItemCollectionId.Namespace>()
|
||||
for operation in transaction.currentItemCollectionInfosOperations {
|
||||
switch operation {
|
||||
case let .replaceInfos(namespace):
|
||||
reloadInfosNamespaces.insert(namespace)
|
||||
}
|
||||
}
|
||||
if !reloadInfosNamespaces.isEmpty {
|
||||
for namespace in self.namespaces {
|
||||
if reloadInfosNamespaces.contains(namespace) {
|
||||
updated = true
|
||||
let ids = postbox.itemCollectionInfoTable.getIds(namespace: namespace)
|
||||
self.idsByNamespace[namespace] = Set(ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return ItemCollectionIdsView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class ItemCollectionIdsView: PostboxView {
|
||||
public let idsByNamespace: [ItemCollectionId.Namespace: Set<ItemCollectionId>]
|
||||
|
||||
init(_ view: MutableItemCollectionIdsView) {
|
||||
self.idsByNamespace = view.idsByNamespace
|
||||
}
|
||||
}
|
||||
172
submodules/Postbox/Postbox/ItemCollectionInfoTable.swift
Normal file
172
submodules/Postbox/Postbox/ItemCollectionInfoTable.swift
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import Foundation
|
||||
|
||||
enum ItemCollectionInfosOperation {
|
||||
case replaceInfos(ItemCollectionId.Namespace)
|
||||
}
|
||||
|
||||
final class ItemCollectionInfoTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 4 + 4 + 8)
|
||||
|
||||
private var cachedInfos: [ItemCollectionId.Namespace: [(Int, ItemCollectionId, ItemCollectionInfo)]] = [:]
|
||||
|
||||
private func key(collectionId: ItemCollectionId, index: Int32) -> ValueBoxKey {
|
||||
self.sharedKey.setInt32(0, value: collectionId.namespace)
|
||||
self.sharedKey.setInt32(4, value: index)
|
||||
self.sharedKey.setInt64(4 + 4, value: collectionId.id)
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private func lowerBound(namespace: ItemCollectionId.Namespace) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: namespace)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(namespace: ItemCollectionId.Namespace) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: namespace)
|
||||
return key.successor
|
||||
}
|
||||
|
||||
|
||||
func getInfos(namespace: ItemCollectionId.Namespace) -> [(Int, ItemCollectionId, ItemCollectionInfo)] {
|
||||
if let cachedInfo = self.cachedInfos[namespace] {
|
||||
return cachedInfo
|
||||
} else {
|
||||
var infos: [(Int, ItemCollectionId, ItemCollectionInfo)] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(namespace: namespace), end: self.upperBound(namespace: namespace), values: { key, value in
|
||||
if let info = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCollectionInfo {
|
||||
infos.append((Int(key.getInt32(4)), ItemCollectionId(namespace: namespace, id: key.getInt64(4 + 4)), info))
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
self.cachedInfos[namespace] = infos
|
||||
return infos
|
||||
}
|
||||
}
|
||||
|
||||
func getIds(namespace: ItemCollectionId.Namespace) -> [ItemCollectionId] {
|
||||
if let cachedInfo = self.cachedInfos[namespace] {
|
||||
return cachedInfo.map { $0.1 }
|
||||
} else {
|
||||
var ids: [ItemCollectionId] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(namespace: namespace), end: self.upperBound(namespace: namespace), keys: { key in
|
||||
ids.append(ItemCollectionId(namespace: namespace, id: key.getInt64(4 + 4)))
|
||||
return true
|
||||
}, limit: 0)
|
||||
return ids
|
||||
}
|
||||
}
|
||||
|
||||
func getIndex(id: ItemCollectionId) -> Int32? {
|
||||
var index: Int32?
|
||||
self.valueBox.range(self.table, start: self.lowerBound(namespace: id.namespace), end: self.upperBound(namespace: id.namespace), keys: { key in
|
||||
let keyId = ItemCollectionId(namespace: id.namespace, id: key.getInt64(4 + 4))
|
||||
if keyId == id {
|
||||
index = key.getInt32(4)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return index
|
||||
}
|
||||
|
||||
func getInfo(id: ItemCollectionId) -> ItemCollectionInfo? {
|
||||
var infoKey: ValueBoxKey?
|
||||
self.valueBox.range(self.table, start: self.lowerBound(namespace: id.namespace), end: self.upperBound(namespace: id.namespace), keys: { key in
|
||||
let keyId = ItemCollectionId(namespace: id.namespace, id: key.getInt64(4 + 4))
|
||||
if keyId == id {
|
||||
infoKey = key
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
if let infoKey = infoKey, let value = self.valueBox.get(self.table, key: infoKey), let info = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCollectionInfo {
|
||||
return info
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func lowerCollectionId(namespaceList: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, index: Int32) -> (ItemCollectionId, Int32)? {
|
||||
var currentNamespace = collectionId.namespace
|
||||
var currentKey = self.key(collectionId: collectionId, index: index)
|
||||
while true {
|
||||
var resultCollectionIdAndIndex: (ItemCollectionId, Int32)?
|
||||
self.valueBox.range(self.table, start: currentKey, end: self.lowerBound(namespace: currentNamespace), keys: { key in
|
||||
resultCollectionIdAndIndex = (ItemCollectionId(namespace: currentNamespace, id: key.getInt64(4 + 4)), key.getInt32(4))
|
||||
return true
|
||||
}, limit: 1)
|
||||
if let resultCollectionIdAndIndex = resultCollectionIdAndIndex {
|
||||
return resultCollectionIdAndIndex
|
||||
} else {
|
||||
let index = namespaceList.index(of: currentNamespace)!
|
||||
if index == 0 {
|
||||
return nil
|
||||
} else {
|
||||
currentNamespace = namespaceList[index - 1]
|
||||
currentKey = self.upperBound(namespace: currentNamespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func higherCollectionId(namespaceList: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, index: Int32) -> (ItemCollectionId, Int32)? {
|
||||
var currentNamespace = collectionId.namespace
|
||||
var currentKey = self.key(collectionId: collectionId, index: index)
|
||||
while true {
|
||||
var resultCollectionIdAndIndex: (ItemCollectionId, Int32)?
|
||||
self.valueBox.range(self.table, start: currentKey, end: self.upperBound(namespace: currentNamespace), keys: { key in
|
||||
resultCollectionIdAndIndex = (ItemCollectionId(namespace: currentNamespace, id: key.getInt64(4 + 4)), key.getInt32(4))
|
||||
return true
|
||||
}, limit: 1)
|
||||
if let resultCollectionIdAndIndex = resultCollectionIdAndIndex {
|
||||
return resultCollectionIdAndIndex
|
||||
} else {
|
||||
let index = namespaceList.index(of: currentNamespace)!
|
||||
if index == namespaceList.count - 1 {
|
||||
return nil
|
||||
} else {
|
||||
currentNamespace = namespaceList[index + 1]
|
||||
currentKey = self.lowerBound(namespace: currentNamespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceInfos(namespace: ItemCollectionId.Namespace, infos: [(ItemCollectionId, ItemCollectionInfo)]) {
|
||||
self.cachedInfos.removeAll()
|
||||
|
||||
var currentCollectionKeys: [ValueBoxKey] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(namespace: namespace), end: self.upperBound(namespace: namespace), keys: { key in
|
||||
currentCollectionKeys.append(key)
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
for key in currentCollectionKeys {
|
||||
self.valueBox.remove(self.table, key: key, secure: false)
|
||||
}
|
||||
|
||||
var index: Int32 = 0
|
||||
let sharedEncoder = PostboxEncoder()
|
||||
for (id, info) in infos {
|
||||
sharedEncoder.reset()
|
||||
sharedEncoder.encodeRootObject(info)
|
||||
withExtendedLifetime(sharedEncoder, {
|
||||
self.valueBox.set(self.table, key: self.key(collectionId: id, index: index), value: sharedEncoder.readBufferNoCopy())
|
||||
})
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedInfos.removeAll()
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
}
|
||||
}
|
||||
65
submodules/Postbox/Postbox/ItemCollectionInfoView.swift
Normal file
65
submodules/Postbox/Postbox/ItemCollectionInfoView.swift
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableItemCollectionInfoView: MutablePostboxView {
|
||||
let id: ItemCollectionId
|
||||
var info: ItemCollectionInfo?
|
||||
|
||||
init(postbox: Postbox, id: ItemCollectionId) {
|
||||
self.id = id
|
||||
let infos = postbox.itemCollectionInfoTable.getInfos(namespace: id.namespace)
|
||||
for (_, infoId, info) in infos {
|
||||
if id == infoId {
|
||||
self.info = info
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
if transaction.currentItemCollectionInfosOperations.isEmpty {
|
||||
return false
|
||||
}
|
||||
|
||||
var updated = false
|
||||
|
||||
var reloadInfosNamespaces = Set<ItemCollectionId.Namespace>()
|
||||
for operation in transaction.currentItemCollectionInfosOperations {
|
||||
switch operation {
|
||||
case let .replaceInfos(namespace):
|
||||
reloadInfosNamespaces.insert(namespace)
|
||||
}
|
||||
}
|
||||
|
||||
if !reloadInfosNamespaces.isEmpty && reloadInfosNamespaces.contains(self.id.namespace) {
|
||||
updated = true
|
||||
|
||||
let infos = postbox.itemCollectionInfoTable.getInfos(namespace: id.namespace)
|
||||
var found = false
|
||||
for (_, infoId, info) in infos {
|
||||
if id == infoId {
|
||||
self.info = info
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
self.info = nil
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return ItemCollectionInfoView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class ItemCollectionInfoView: PostboxView {
|
||||
public let id: ItemCollectionId
|
||||
public let info: ItemCollectionInfo?
|
||||
|
||||
init(_ view: MutableItemCollectionInfoView) {
|
||||
self.id = view.id
|
||||
self.info = view.info
|
||||
}
|
||||
}
|
||||
99
submodules/Postbox/Postbox/ItemCollectionInfosView.swift
Normal file
99
submodules/Postbox/Postbox/ItemCollectionInfosView.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import Foundation
|
||||
|
||||
public final class ItemCollectionInfoEntry {
|
||||
public let id: ItemCollectionId
|
||||
public let info: ItemCollectionInfo
|
||||
public let count: Int32
|
||||
public let firstItem: ItemCollectionItem?
|
||||
|
||||
init(id: ItemCollectionId, info: ItemCollectionInfo, count: Int32, firstItem: ItemCollectionItem?) {
|
||||
self.id = id
|
||||
self.info = info
|
||||
self.count = count
|
||||
self.firstItem = firstItem
|
||||
}
|
||||
}
|
||||
|
||||
final class MutableItemCollectionInfosView: MutablePostboxView {
|
||||
let namespaces: [ItemCollectionId.Namespace]
|
||||
var entriesByNamespace: [ItemCollectionId.Namespace: [ItemCollectionInfoEntry]]
|
||||
|
||||
init(postbox: Postbox, namespaces: [ItemCollectionId.Namespace]) {
|
||||
self.namespaces = namespaces
|
||||
|
||||
var entriesByNamespace: [ItemCollectionId.Namespace: [ItemCollectionInfoEntry]] = [:]
|
||||
for namespace in namespaces {
|
||||
let infos = postbox.itemCollectionInfoTable.getInfos(namespace: namespace)
|
||||
var entries: [ItemCollectionInfoEntry] = []
|
||||
for (_, id, info) in infos {
|
||||
let firstItem = postbox.itemCollectionItemTable.higherItems(collectionId: id, itemIndex: ItemCollectionItemIndex.lowerBound, count: 1).first
|
||||
entries.append(ItemCollectionInfoEntry(id: id, info: info, count: postbox.itemCollectionItemTable.itemCount(collectionId: id), firstItem: firstItem))
|
||||
}
|
||||
entriesByNamespace[namespace] = entries
|
||||
}
|
||||
self.entriesByNamespace = entriesByNamespace
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
if transaction.currentItemCollectionInfosOperations.isEmpty && transaction.currentItemCollectionItemsOperations.isEmpty {
|
||||
return false
|
||||
}
|
||||
|
||||
var updated = false
|
||||
|
||||
var reloadInfosNamespaces = Set<ItemCollectionId.Namespace>()
|
||||
var reloadTopItemCollectionIds = Set<ItemCollectionId>()
|
||||
for operation in transaction.currentItemCollectionInfosOperations {
|
||||
switch operation {
|
||||
case let .replaceInfos(namespace):
|
||||
reloadInfosNamespaces.insert(namespace)
|
||||
}
|
||||
}
|
||||
for (id, operations) in transaction.currentItemCollectionItemsOperations {
|
||||
for operation in operations {
|
||||
switch operation {
|
||||
case .replaceItems:
|
||||
reloadTopItemCollectionIds.insert(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reloadInfosNamespaces.isEmpty {
|
||||
updated = true
|
||||
|
||||
var entriesByNamespace: [ItemCollectionId.Namespace: [ItemCollectionInfoEntry]] = [:]
|
||||
for namespace in self.namespaces {
|
||||
let infos = postbox.itemCollectionInfoTable.getInfos(namespace: namespace)
|
||||
var entries: [ItemCollectionInfoEntry] = []
|
||||
for (_, id, info) in infos {
|
||||
let firstItem = postbox.itemCollectionItemTable.higherItems(collectionId: id, itemIndex: ItemCollectionItemIndex.lowerBound, count: 1).first
|
||||
entries.append(ItemCollectionInfoEntry(id: id, info: info, count: postbox.itemCollectionItemTable.itemCount(collectionId: id), firstItem: firstItem))
|
||||
}
|
||||
entriesByNamespace[namespace] = entries
|
||||
}
|
||||
self.entriesByNamespace = entriesByNamespace
|
||||
} else if !reloadTopItemCollectionIds.isEmpty {
|
||||
for (namespace, entries) in self.entriesByNamespace {
|
||||
for i in 0 ..< entries.count {
|
||||
if reloadTopItemCollectionIds.contains(entries[i].id) {
|
||||
updated = true
|
||||
let firstItem = postbox.itemCollectionItemTable.higherItems(collectionId: entries[i].id, itemIndex: ItemCollectionItemIndex.lowerBound, count: 1).first
|
||||
self.entriesByNamespace[namespace]![i] = ItemCollectionInfoEntry(id: entries[i].id, info: entries[i].info, count: postbox.itemCollectionItemTable.itemCount(collectionId: entries[i].id), firstItem: firstItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return ItemCollectionInfosView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class ItemCollectionInfosView: PostboxView {
|
||||
public let entriesByNamespace: [ItemCollectionId.Namespace: [ItemCollectionInfoEntry]]
|
||||
|
||||
init(_ view: MutableItemCollectionInfosView) {
|
||||
self.entriesByNamespace = view.entriesByNamespace
|
||||
}
|
||||
}
|
||||
259
submodules/Postbox/Postbox/ItemCollectionItemTable.swift
Normal file
259
submodules/Postbox/Postbox/ItemCollectionItemTable.swift
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
import Foundation
|
||||
|
||||
enum ItemCollectionItemsOperation {
|
||||
case replaceItems
|
||||
}
|
||||
|
||||
struct ItemCollectionItemReverseIndexReference: Equatable, Hashable, ReverseIndexReference {
|
||||
let collectionId: ItemCollectionId
|
||||
let itemIndex: ItemCollectionItemIndex
|
||||
|
||||
static func <(lhs: ItemCollectionItemReverseIndexReference, rhs: ItemCollectionItemReverseIndexReference) -> Bool {
|
||||
if lhs.collectionId != rhs.collectionId {
|
||||
return lhs.collectionId < rhs.collectionId
|
||||
} else {
|
||||
return lhs.itemIndex < rhs.itemIndex
|
||||
}
|
||||
}
|
||||
|
||||
static func decodeArray(_ buffer: MemoryBuffer) -> [ItemCollectionItemReverseIndexReference] {
|
||||
assert(buffer.length % (4 + 8 + 4 + 8) == 0)
|
||||
var references: [ItemCollectionItemReverseIndexReference] = []
|
||||
references.reserveCapacity(buffer.length % (4 + 8 + 4 + 8))
|
||||
withExtendedLifetime(buffer, {
|
||||
let readBuffer = ReadBuffer(memoryBufferNoCopy: buffer)
|
||||
for _ in 0 ..< buffer.length / (4 + 8 + 4 + 8) {
|
||||
var collectionIdNamespace: Int32 = 0
|
||||
var collectionIdId: Int64 = 0
|
||||
var indexIdIndex: Int32 = 0
|
||||
var indexIdId: Int64 = 0
|
||||
readBuffer.read(&collectionIdNamespace, offset: 0, length: 4)
|
||||
readBuffer.read(&collectionIdId, offset: 0, length: 8)
|
||||
readBuffer.read(&indexIdIndex, offset: 0, length: 4)
|
||||
readBuffer.read(&indexIdId, offset: 0, length: 8)
|
||||
references.append(ItemCollectionItemReverseIndexReference(collectionId: ItemCollectionId(namespace: collectionIdNamespace, id: collectionIdId), itemIndex: ItemCollectionItemIndex(index: indexIdIndex, id: indexIdId)))
|
||||
}
|
||||
})
|
||||
return references
|
||||
}
|
||||
|
||||
static func encodeArray(_ array: [ItemCollectionItemReverseIndexReference]) -> MemoryBuffer {
|
||||
let buffer = WriteBuffer()
|
||||
for reference in array {
|
||||
var collectionIdNamespace: Int32 = reference.collectionId.namespace
|
||||
var collectionIdId: Int64 = reference.collectionId.id
|
||||
var indexIdIndex: Int32 = reference.itemIndex.index
|
||||
var indexIdId: Int64 = reference.itemIndex.id
|
||||
|
||||
buffer.write(&collectionIdNamespace, offset: 0, length: 4)
|
||||
buffer.write(&collectionIdId, offset: 0, length: 8)
|
||||
buffer.write(&indexIdIndex, offset: 0, length: 4)
|
||||
buffer.write(&indexIdId, offset: 0, length: 8)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemCollectionItemTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let reverseIndexTable: ReverseIndexReferenceTable<ItemCollectionItemReverseIndexReference>
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 4 + 8 + 4 + 8)
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, reverseIndexTable: ReverseIndexReferenceTable<ItemCollectionItemReverseIndexReference>) {
|
||||
self.reverseIndexTable = reverseIndexTable
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(collectionId: ItemCollectionId, index: ItemCollectionItemIndex) -> ValueBoxKey {
|
||||
self.sharedKey.setInt32(0, value: collectionId.namespace)
|
||||
self.sharedKey.setInt64(4, value: collectionId.id)
|
||||
self.sharedKey.setInt32(4 + 8, value: index.index)
|
||||
self.sharedKey.setInt64(4 + 8 + 4, value: index.id)
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private func lowerBound(namespace: ItemCollectionId.Namespace) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: namespace)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(namespace: ItemCollectionId.Namespace) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: namespace)
|
||||
return key.successor
|
||||
}
|
||||
|
||||
private func lowerBound(collectionId: ItemCollectionId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4 + 8)
|
||||
key.setInt32(0, value: collectionId.namespace)
|
||||
key.setInt64(4, value: collectionId.id)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(collectionId: ItemCollectionId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4 + 8)
|
||||
key.setInt32(0, value: collectionId.namespace)
|
||||
key.setInt64(4, value: collectionId.id)
|
||||
return key.successor
|
||||
}
|
||||
|
||||
func lowerItems(collectionId: ItemCollectionId, itemIndex: ItemCollectionItemIndex, count: Int) -> [ItemCollectionItem] {
|
||||
var items: [ItemCollectionItem] = []
|
||||
self.valueBox.range(self.table, start: self.key(collectionId: collectionId, index: itemIndex), end: self.lowerBound(collectionId: collectionId), values: { _, value in
|
||||
if let item = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCollectionItem {
|
||||
items.append(item)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
return true
|
||||
}, limit: count)
|
||||
return items
|
||||
}
|
||||
|
||||
func itemCount(collectionId: ItemCollectionId) -> Int32 {
|
||||
var count: Int32 = 0
|
||||
self.valueBox.range(self.table, start: self.upperBound(collectionId: collectionId), end: self.lowerBound(collectionId: collectionId), keys: { key in
|
||||
let itemIndex = ItemCollectionItemIndex(index: key.getInt32(4 + 8), id: key.getInt64(4 + 8 + 4))
|
||||
count = itemIndex.index + 1
|
||||
return true
|
||||
}, limit: 1)
|
||||
return count
|
||||
}
|
||||
|
||||
func higherItems(collectionId: ItemCollectionId, itemIndex: ItemCollectionItemIndex, count: Int) -> [ItemCollectionItem] {
|
||||
var items: [ItemCollectionItem] = []
|
||||
self.valueBox.range(self.table, start: self.key(collectionId: collectionId, index: itemIndex), end: self.upperBound(collectionId: collectionId), values: { _, value in
|
||||
if let item = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCollectionItem {
|
||||
items.append(item)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
return true
|
||||
}, limit: count)
|
||||
return items
|
||||
}
|
||||
|
||||
func getItems(namespace: ItemCollectionId.Namespace) -> [ItemCollectionId: [ItemCollectionItem]] {
|
||||
var items: [ItemCollectionId: [ItemCollectionItem]] = [:]
|
||||
self.valueBox.range(self.table, start: self.lowerBound(namespace: namespace), end: self.upperBound(namespace: namespace), values: { key, value in
|
||||
let collectionId = ItemCollectionId(namespace: namespace, id: key.getInt64(4))
|
||||
//let itemIndex = ItemCollectionItemIndex(index: key.getInt32(4 + 8), id: key.getInt64(4 + 8 + 4))
|
||||
if let item = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCollectionItem {
|
||||
if items[collectionId] != nil {
|
||||
items[collectionId]!.append(item)
|
||||
} else {
|
||||
items[collectionId] = [item]
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return items
|
||||
}
|
||||
|
||||
func collectionItems(collectionId: ItemCollectionId) -> [ItemCollectionItem] {
|
||||
var items: [ItemCollectionItem] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(collectionId: collectionId), end: self.upperBound(collectionId: collectionId), values: { key, value in
|
||||
//let itemIndex = ItemCollectionItemIndex(index: key.getInt32(4 + 8), id: key.getInt64(4 + 8 + 4))
|
||||
if let item = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCollectionItem {
|
||||
items.append(item)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
return items
|
||||
}
|
||||
|
||||
func replaceItems(collectionId: ItemCollectionId, items: [ItemCollectionItem]) {
|
||||
let updatedIndices = Set(items.map({ $0.index }))
|
||||
var itemByIndex: [ItemCollectionItemIndex: ItemCollectionItem] = [:]
|
||||
for item in items {
|
||||
itemByIndex[item.index] = item
|
||||
}
|
||||
|
||||
var currentIndices = Set<ItemCollectionItemIndex>()
|
||||
|
||||
var removedIndexKeys: [ItemCollectionItemIndex: [MemoryBuffer]] = [:]
|
||||
|
||||
self.valueBox.range(self.table, start: self.lowerBound(collectionId: collectionId), end: self.upperBound(collectionId: collectionId), values: { key, value in
|
||||
let itemIndex = ItemCollectionItemIndex(index: key.getInt32(4 + 8), id: key.getInt64(4 + 8 + 4))
|
||||
currentIndices.insert(itemIndex)
|
||||
|
||||
if !updatedIndices.contains(itemIndex) {
|
||||
if let item = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCollectionItem {
|
||||
if !item.indexKeys.isEmpty {
|
||||
removedIndexKeys[itemIndex] = item.indexKeys
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
let addedIndices = updatedIndices.subtracting(currentIndices)
|
||||
let removedIndices = currentIndices.subtracting(updatedIndices)
|
||||
|
||||
for index in removedIndices {
|
||||
self.valueBox.remove(self.table, key: self.key(collectionId: collectionId, index: index), secure: false)
|
||||
if let indexKeys = removedIndexKeys[index] {
|
||||
self.reverseIndexTable.remove(namespace: ReverseIndexNamespace(collectionId.namespace), reference: ItemCollectionItemReverseIndexReference(collectionId: collectionId, itemIndex: index), tokens: indexKeys.map({ ValueBoxKey($0) }))
|
||||
}
|
||||
}
|
||||
|
||||
let sharedEncoder = PostboxEncoder()
|
||||
for index in addedIndices {
|
||||
let item = itemByIndex[index]!
|
||||
sharedEncoder.reset()
|
||||
sharedEncoder.encodeRootObject(item)
|
||||
withExtendedLifetime(sharedEncoder, {
|
||||
self.valueBox.set(self.table, key: self.key(collectionId: collectionId, index: index), value: sharedEncoder.readBufferNoCopy())
|
||||
})
|
||||
if !item.indexKeys.isEmpty {
|
||||
self.reverseIndexTable.add(namespace: ReverseIndexNamespace(collectionId.namespace), reference: ItemCollectionItemReverseIndexReference(collectionId: collectionId, itemIndex: index), tokens: item.indexKeys.map({ ValueBoxKey($0) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func searchIndexedItems(namespace: ItemCollectionId.Namespace, query: ItemCollectionSearchQuery) -> [ItemCollectionId: [ItemCollectionItem]] {
|
||||
let references: [ItemCollectionItemReverseIndexReference]
|
||||
switch query {
|
||||
case let .exact(token):
|
||||
references = self.reverseIndexTable.exactReferences(namespace: ReverseIndexNamespace(namespace), token: token)
|
||||
case let .matching(tokens):
|
||||
references = Array(self.reverseIndexTable.matchingReferences(namespace: ReverseIndexNamespace(namespace), tokens: tokens))
|
||||
}
|
||||
var resultsByCollectionId: [ItemCollectionId: [(ItemCollectionItemIndex, ItemCollectionItem)]] = [:]
|
||||
for reference in references {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(collectionId: reference.collectionId, index: reference.itemIndex)), let item = PostboxDecoder(buffer: value).decodeRootObject() as? ItemCollectionItem {
|
||||
if resultsByCollectionId[reference.collectionId] == nil {
|
||||
resultsByCollectionId[reference.collectionId] = [(reference.itemIndex, item)]
|
||||
} else {
|
||||
resultsByCollectionId[reference.collectionId]!.append((reference.itemIndex, item))
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
var results: [ItemCollectionId: [ItemCollectionItem]] = [:]
|
||||
for collectionId in resultsByCollectionId.keys {
|
||||
results[collectionId] = resultsByCollectionId[collectionId]!.sorted(by: { $0.0 < $1.0 }).map({ $0.1 })
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
|
||||
}
|
||||
}
|
||||
307
submodules/Postbox/Postbox/ItemCollectionsView.swift
Normal file
307
submodules/Postbox/Postbox/ItemCollectionsView.swift
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
import Foundation
|
||||
|
||||
public struct ItemCollectionViewEntryIndex: Comparable {
|
||||
public let collectionIndex: Int32
|
||||
public let collectionId: ItemCollectionId
|
||||
public let itemIndex: ItemCollectionItemIndex
|
||||
|
||||
public init(collectionIndex: Int32, collectionId: ItemCollectionId, itemIndex: ItemCollectionItemIndex) {
|
||||
self.collectionIndex = collectionIndex
|
||||
self.collectionId = collectionId
|
||||
self.itemIndex = itemIndex
|
||||
}
|
||||
|
||||
public static func ==(lhs: ItemCollectionViewEntryIndex, rhs: ItemCollectionViewEntryIndex) -> Bool {
|
||||
return lhs.collectionIndex == rhs.collectionIndex && lhs.collectionId == rhs.collectionId && lhs.itemIndex == rhs.itemIndex
|
||||
}
|
||||
|
||||
public static func <(lhs: ItemCollectionViewEntryIndex, rhs: ItemCollectionViewEntryIndex) -> Bool {
|
||||
if lhs.collectionIndex == rhs.collectionIndex {
|
||||
if lhs.itemIndex == rhs.itemIndex {
|
||||
return lhs.collectionId < rhs.collectionId
|
||||
} else {
|
||||
return lhs.itemIndex < rhs.itemIndex
|
||||
}
|
||||
} else {
|
||||
return lhs.collectionIndex < rhs.collectionIndex
|
||||
}
|
||||
}
|
||||
|
||||
public static func lowerBound(collectionIndex: Int32, collectionId: ItemCollectionId) -> ItemCollectionViewEntryIndex {
|
||||
return ItemCollectionViewEntryIndex(collectionIndex: collectionIndex, collectionId: collectionId, itemIndex: ItemCollectionItemIndex(index: 0, id: 0))
|
||||
}
|
||||
}
|
||||
|
||||
public struct ItemCollectionViewEntry {
|
||||
public let index: ItemCollectionViewEntryIndex
|
||||
public let item: ItemCollectionItem
|
||||
|
||||
public init(index: ItemCollectionViewEntryIndex, item: ItemCollectionItem) {
|
||||
self.index = index
|
||||
self.item = item
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchLowerEntries(namespaces: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, collectionIndex: Int32, itemIndex: ItemCollectionItemIndex, count: Int, lowerCollectionId: (_ namespaceList: [ItemCollectionId.Namespace], _ collectionId: ItemCollectionId, _ collectionIndex: Int32) -> (ItemCollectionId, Int32)?, lowerItems: (_ collectionId: ItemCollectionId, _ itemIndex: ItemCollectionItemIndex, _ count: Int) -> [ItemCollectionItem]) -> [ItemCollectionViewEntry] {
|
||||
|
||||
var entries: [ItemCollectionViewEntry] = []
|
||||
|
||||
var currentCollectionIndex = collectionIndex
|
||||
var currentCollectionId = collectionId
|
||||
var currentItemIndex = itemIndex
|
||||
|
||||
while true {
|
||||
let remainingCount = count - entries.count
|
||||
assert(remainingCount > 0)
|
||||
let collectionItems = lowerItems(currentCollectionId, currentItemIndex, remainingCount)
|
||||
for item in collectionItems {
|
||||
entries.append(ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: currentCollectionIndex, collectionId: currentCollectionId, itemIndex: item.index), item: item))
|
||||
}
|
||||
if entries.count >= count {
|
||||
break
|
||||
} else {
|
||||
assert(collectionItems.count < remainingCount)
|
||||
|
||||
if let (previousCollectionId, previousCollectionIndex) = lowerCollectionId(namespaces, currentCollectionId, currentCollectionIndex) {
|
||||
currentCollectionIndex = previousCollectionIndex
|
||||
currentCollectionId = previousCollectionId
|
||||
currentItemIndex = ItemCollectionItemIndex.upperBound
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
private func fetchHigherEntries(namespaces: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, collectionIndex: Int32, itemIndex: ItemCollectionItemIndex, count: Int, higherCollectionId: (_ namespaceList: [ItemCollectionId.Namespace], _ collectionId: ItemCollectionId, _ collectionIndex: Int32) -> (ItemCollectionId, Int32)?, higherItems: (_ collectionId: ItemCollectionId, _ itemIndex: ItemCollectionItemIndex, _ count: Int) -> [ItemCollectionItem]) -> [ItemCollectionViewEntry] {
|
||||
|
||||
var entries: [ItemCollectionViewEntry] = []
|
||||
|
||||
var currentCollectionIndex = collectionIndex
|
||||
var currentCollectionId = collectionId
|
||||
var currentItemIndex = itemIndex
|
||||
|
||||
while true {
|
||||
let remainingCount = count - entries.count
|
||||
assert(remainingCount > 0)
|
||||
let collectionItems = higherItems(currentCollectionId, currentItemIndex, remainingCount)
|
||||
for item in collectionItems {
|
||||
entries.append(ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: currentCollectionIndex, collectionId: currentCollectionId, itemIndex: item.index), item: item))
|
||||
}
|
||||
if entries.count >= count {
|
||||
break
|
||||
} else {
|
||||
assert(collectionItems.count < remainingCount)
|
||||
|
||||
if let (nextCollectionId, nextCollectionIndex) = higherCollectionId(namespaces, currentCollectionId, currentCollectionIndex) {
|
||||
currentCollectionIndex = nextCollectionIndex
|
||||
currentCollectionId = nextCollectionId
|
||||
currentItemIndex = ItemCollectionItemIndex.lowerBound
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
private func aroundEntries(namespaces: [ItemCollectionId.Namespace],
|
||||
aroundIndex: ItemCollectionViewEntryIndex?,
|
||||
count: Int,
|
||||
collectionIndexById: (ItemCollectionId) -> Int32?,
|
||||
lowerCollectionId: (_ namespaceList: [ItemCollectionId.Namespace], _ collectionId: ItemCollectionId, _ collectionIndex: Int32) -> (ItemCollectionId, Int32)?,
|
||||
fetchLowerItems: (_ collectionId: ItemCollectionId, _ itemIndex: ItemCollectionItemIndex, _ count: Int) -> [ItemCollectionItem],
|
||||
higherCollectionId: (_ namespaceList: [ItemCollectionId.Namespace], _ collectionId: ItemCollectionId, _ collectionIndex: Int32) -> (ItemCollectionId, Int32)?,
|
||||
fetchHigherItems: (_ collectionId: ItemCollectionId, _ itemIndex: ItemCollectionItemIndex, _ count: Int) -> [ItemCollectionItem]) -> ([ItemCollectionViewEntry], ItemCollectionViewEntry?, ItemCollectionViewEntry?) {
|
||||
var lowerEntries: [ItemCollectionViewEntry] = []
|
||||
var upperEntries: [ItemCollectionViewEntry] = []
|
||||
var lower: ItemCollectionViewEntry?
|
||||
var upper: ItemCollectionViewEntry?
|
||||
|
||||
let selectedAroundIndex: ItemCollectionViewEntryIndex
|
||||
if let aroundIndex = aroundIndex, let aroundCollectionIndex = collectionIndexById(aroundIndex.collectionId) {
|
||||
selectedAroundIndex = ItemCollectionViewEntryIndex(collectionIndex: aroundCollectionIndex, collectionId: aroundIndex.collectionId, itemIndex: aroundIndex.itemIndex)
|
||||
} else {
|
||||
selectedAroundIndex = ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: ItemCollectionId(namespace: namespaces[0], id: 0), itemIndex: ItemCollectionItemIndex.lowerBound)
|
||||
}
|
||||
|
||||
let collectionId: ItemCollectionId = selectedAroundIndex.collectionId
|
||||
let collectionIndex: Int32 = selectedAroundIndex.collectionIndex
|
||||
let itemIndex: ItemCollectionItemIndex = selectedAroundIndex.itemIndex
|
||||
|
||||
lowerEntries.append(contentsOf: fetchLowerEntries(namespaces: namespaces, collectionId: collectionId, collectionIndex: collectionIndex, itemIndex: itemIndex, count: count / 2 + 1, lowerCollectionId: lowerCollectionId, lowerItems: fetchLowerItems))
|
||||
|
||||
let lowerIndices = lowerEntries.map { $0.index }
|
||||
assert(lowerIndices.sorted() == lowerIndices.reversed())
|
||||
|
||||
if lowerEntries.count >= count / 2 + 1 {
|
||||
lower = lowerEntries.last
|
||||
lowerEntries.removeLast()
|
||||
}
|
||||
|
||||
upperEntries.append(contentsOf: fetchHigherEntries(namespaces: namespaces, collectionId: collectionId, collectionIndex: collectionIndex, itemIndex: ItemCollectionItemIndex(index: itemIndex.index, id: max(0, itemIndex.id - 1)), count: count - lowerEntries.count + 1, higherCollectionId: higherCollectionId, higherItems: fetchHigherItems))
|
||||
|
||||
let upperIndices = upperEntries.map { $0.index }
|
||||
assert(upperIndices.sorted() == upperIndices)
|
||||
|
||||
if upperEntries.count >= count - lowerEntries.count + 1 {
|
||||
upper = upperEntries.last
|
||||
upperEntries.removeLast()
|
||||
}
|
||||
|
||||
if lowerEntries.count != 0 && lowerEntries.count + upperEntries.count < count {
|
||||
var additionalLowerEntries: [ItemCollectionViewEntry] = fetchLowerEntries(namespaces: namespaces, collectionId: lowerEntries.last!.index.collectionId, collectionIndex: lowerEntries.last!.index.collectionIndex, itemIndex: lowerEntries.last!.index.itemIndex, count: count - lowerEntries.count - upperEntries.count + 1, lowerCollectionId: lowerCollectionId, lowerItems: fetchLowerItems)
|
||||
|
||||
if additionalLowerEntries.count >= count - lowerEntries.count + upperEntries.count + 1 {
|
||||
lower = additionalLowerEntries.last
|
||||
additionalLowerEntries.removeLast()
|
||||
}
|
||||
lowerEntries.append(contentsOf: additionalLowerEntries)
|
||||
}
|
||||
|
||||
var entries: [ItemCollectionViewEntry] = []
|
||||
entries.append(contentsOf: lowerEntries.reversed())
|
||||
entries.append(contentsOf: upperEntries)
|
||||
return (entries: entries, lower: lower, upper: upper)
|
||||
}
|
||||
|
||||
final class MutableItemCollectionsView {
|
||||
let orderedItemListsViews: [MutableOrderedItemListView]
|
||||
let namespaces: [ItemCollectionId.Namespace]
|
||||
let requestedAroundIndex: ItemCollectionViewEntryIndex?
|
||||
let requestedCount: Int
|
||||
|
||||
var collectionInfos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?)]
|
||||
var entries: [ItemCollectionViewEntry]
|
||||
var lower: ItemCollectionViewEntry?
|
||||
var higher: ItemCollectionViewEntry?
|
||||
|
||||
init(postbox: Postbox, orderedItemListsViews: [MutableOrderedItemListView], namespaces: [ItemCollectionId.Namespace], aroundIndex: ItemCollectionViewEntryIndex?, count: Int) {
|
||||
self.orderedItemListsViews = orderedItemListsViews
|
||||
self.namespaces = namespaces
|
||||
self.requestedAroundIndex = aroundIndex
|
||||
self.requestedCount = count
|
||||
|
||||
self.collectionInfos = []
|
||||
self.entries = []
|
||||
self.lower = nil
|
||||
self.higher = nil
|
||||
|
||||
self.reload(postbox: postbox, aroundIndex: aroundIndex, count: count)
|
||||
}
|
||||
|
||||
private func lowerItems(postbox: Postbox, collectionId: ItemCollectionId, itemIndex: ItemCollectionItemIndex, count: Int) -> [ItemCollectionItem] {
|
||||
return postbox.itemCollectionItemTable.lowerItems(collectionId: collectionId, itemIndex: itemIndex, count: count)
|
||||
}
|
||||
|
||||
private func higherItems(postbox: Postbox, collectionId: ItemCollectionId, itemIndex: ItemCollectionItemIndex, count: Int) -> [ItemCollectionItem] {
|
||||
return postbox.itemCollectionItemTable.higherItems(collectionId: collectionId, itemIndex: itemIndex, count: count)
|
||||
}
|
||||
|
||||
private func lowerCollectionId(postbox: Postbox, namespaceList: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, collectionIndex: Int32) -> (ItemCollectionId, Int32)? {
|
||||
return postbox.itemCollectionInfoTable.lowerCollectionId(namespaceList: namespaceList, collectionId: collectionId, index: collectionIndex)
|
||||
}
|
||||
|
||||
private func higherCollectionId(postbox: Postbox, namespaceList: [ItemCollectionId.Namespace], collectionId: ItemCollectionId, collectionIndex: Int32) -> (ItemCollectionId, Int32)? {
|
||||
return postbox.itemCollectionInfoTable.higherCollectionId(namespaceList: namespaceList, collectionId: collectionId, index: collectionIndex)
|
||||
}
|
||||
|
||||
private func reload(postbox: Postbox, aroundIndex: ItemCollectionViewEntryIndex?, count: Int) {
|
||||
self.collectionInfos = []
|
||||
for namespace in namespaces {
|
||||
for (_, id, info) in postbox.itemCollectionInfoTable.getInfos(namespace: namespace) {
|
||||
let item = self.higherItems(postbox: postbox, collectionId: id, itemIndex: ItemCollectionItemIndex.lowerBound, count: 1).first
|
||||
self.collectionInfos.append((id, info, item))
|
||||
}
|
||||
}
|
||||
|
||||
let (entries, lower, higher) = aroundEntries(namespaces: namespaces,
|
||||
aroundIndex: aroundIndex,
|
||||
count: count, collectionIndexById: { id in
|
||||
return postbox.itemCollectionInfoTable.getIndex(id: id)
|
||||
},
|
||||
lowerCollectionId: { namespaceList, collectionId, collectionIndex in
|
||||
return self.lowerCollectionId(postbox: postbox, namespaceList: namespaceList, collectionId: collectionId, collectionIndex: collectionIndex)
|
||||
},
|
||||
fetchLowerItems: { collectionId, itemIndex, count in
|
||||
return self.lowerItems(postbox: postbox, collectionId: collectionId, itemIndex: itemIndex, count: count)
|
||||
},
|
||||
higherCollectionId: { namespaceList, collectionId, collectionIndex in
|
||||
return self.higherCollectionId(postbox: postbox, namespaceList: namespaceList, collectionId: collectionId, collectionIndex: collectionIndex)
|
||||
},
|
||||
fetchHigherItems: {
|
||||
collectionId, itemIndex, count in
|
||||
return self.higherItems(postbox: postbox, collectionId: collectionId, itemIndex: itemIndex, count: count)
|
||||
})
|
||||
|
||||
self.entries = entries
|
||||
self.lower = lower
|
||||
self.higher = higher
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
|
||||
if !transaction.currentOrderedItemListOperations.isEmpty {
|
||||
for view in self.orderedItemListsViews {
|
||||
if view.replay(postbox: postbox, transaction: transaction) {
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var reloadNamespaces = Set<ItemCollectionId.Namespace>()
|
||||
for operation in transaction.currentItemCollectionInfosOperations {
|
||||
switch operation {
|
||||
case let .replaceInfos(namespace):
|
||||
reloadNamespaces.insert(namespace)
|
||||
}
|
||||
}
|
||||
|
||||
for (id, operations) in transaction.currentItemCollectionItemsOperations {
|
||||
for operation in operations {
|
||||
switch operation {
|
||||
case .replaceItems:
|
||||
reloadNamespaces.insert(id.namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shouldReloadEntries = false
|
||||
if !reloadNamespaces.isEmpty {
|
||||
for namespace in self.namespaces {
|
||||
if reloadNamespaces.contains(namespace) {
|
||||
shouldReloadEntries = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shouldReloadEntries {
|
||||
self.reload(postbox: postbox, aroundIndex: self.requestedAroundIndex, count: self.requestedCount)
|
||||
updated = true
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
public final class ItemCollectionsView {
|
||||
public let orderedItemListsViews: [OrderedItemListView]
|
||||
public let collectionInfos: [(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?)]
|
||||
public let entries: [ItemCollectionViewEntry]
|
||||
public let lower: ItemCollectionViewEntry?
|
||||
public let higher: ItemCollectionViewEntry?
|
||||
|
||||
init(_ mutableView: MutableItemCollectionsView) {
|
||||
self.orderedItemListsViews = mutableView.orderedItemListsViews.map { OrderedItemListView($0) }
|
||||
self.collectionInfos = mutableView.collectionInfos
|
||||
self.entries = mutableView.entries
|
||||
self.lower = mutableView.lower
|
||||
self.higher = mutableView.higher
|
||||
}
|
||||
}
|
||||
26
submodules/Postbox/Postbox/KeychainTable.swift
Normal file
26
submodules/Postbox/Postbox/KeychainTable.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import Foundation
|
||||
|
||||
final class KeychainTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private func key(_ string: String) -> ValueBoxKey {
|
||||
return ValueBoxKey(string)
|
||||
}
|
||||
|
||||
func get(_ key: String) -> Data? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(key)) {
|
||||
return Data(bytes: value.memory.assumingMemoryBound(to: UInt8.self), count: value.length)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func set(_ key: String, value: Data) {
|
||||
self.valueBox.set(self.table, key: self.key(key), value: MemoryBuffer(data: value))
|
||||
}
|
||||
|
||||
func remove(_ key: String) {
|
||||
self.valueBox.remove(self.table, key: self.key(key), secure: false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import Foundation
|
||||
|
||||
enum IntermediateMessageHistoryLocalTagsOperation {
|
||||
case Insert(LocalMessageTags, MessageId)
|
||||
case Remove(LocalMessageTags, MessageId)
|
||||
case Update(LocalMessageTags, MessageId)
|
||||
}
|
||||
|
||||
final class LocalMessageHistoryTagsTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 4 + 4 + 4 + 8)
|
||||
|
||||
private func key(_ id: MessageId, tag: LocalMessageTags) -> ValueBoxKey {
|
||||
assert(tag.isSingleTag)
|
||||
self.sharedKey.setUInt32(0, value: tag.rawValue)
|
||||
self.sharedKey.setInt32(4, value: id.namespace)
|
||||
self.sharedKey.setInt32(4 + 4, value: id.id)
|
||||
self.sharedKey.setInt64(4 + 4 + 4, value: id.peerId.toInt64())
|
||||
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private func lowerBound(tag: LocalMessageTags) -> ValueBoxKey {
|
||||
assert(tag.isSingleTag)
|
||||
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setUInt32(0, value: tag.rawValue)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(tag: LocalMessageTags) -> ValueBoxKey {
|
||||
assert(tag.isSingleTag)
|
||||
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setUInt32(0, value: tag.rawValue)
|
||||
return key.successor
|
||||
}
|
||||
|
||||
func set(id: MessageId, tags: LocalMessageTags, previousTags: LocalMessageTags, operations: inout [IntermediateMessageHistoryLocalTagsOperation]) {
|
||||
let addedTags = tags.subtracting(previousTags)
|
||||
let removedTags = previousTags.subtracting(tags)
|
||||
for tag in addedTags {
|
||||
self.valueBox.set(self.table, key: self.key(id, tag: tag), value: MemoryBuffer())
|
||||
operations.append(.Insert(tag, id))
|
||||
}
|
||||
for tag in removedTags {
|
||||
self.valueBox.remove(self.table, key: self.key(id, tag: tag), secure: false)
|
||||
operations.append(.Remove(tag, id))
|
||||
}
|
||||
}
|
||||
|
||||
func get(tag: LocalMessageTags) -> [MessageId] {
|
||||
assert(tag.isSingleTag)
|
||||
var ids: [MessageId] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(tag: tag), end: self.upperBound(tag: tag), keys: { key in
|
||||
ids.append(MessageId(peerId: PeerId(key.getInt64(4 + 4 + 4)), namespace: key.getInt32(4), id: key.getInt32(4 + 4)))
|
||||
return true
|
||||
}, limit: 0)
|
||||
return ids
|
||||
}
|
||||
}
|
||||
67
submodules/Postbox/Postbox/LocalMessageTagsView.swift
Normal file
67
submodules/Postbox/Postbox/LocalMessageTagsView.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableLocalMessageTagsView: MutablePostboxView {
|
||||
private let tag: LocalMessageTags
|
||||
fileprivate var messages: [MessageId: Message] = [:]
|
||||
|
||||
init(postbox: Postbox, tag: LocalMessageTags) {
|
||||
self.tag = tag
|
||||
for id in postbox.localMessageHistoryTagsTable.get(tag: tag) {
|
||||
if let message = postbox.getMessage(id) {
|
||||
self.messages[message.id] = message
|
||||
} else {
|
||||
//assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
for operation in transaction.currentLocalTagsOperations {
|
||||
switch operation {
|
||||
case let .Insert(tag, id):
|
||||
if tag == self.tag {
|
||||
if let message = postbox.getMessage(id) {
|
||||
self.messages[id] = message
|
||||
updated = true
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
case let .Remove(tag, id):
|
||||
if tag == self.tag {
|
||||
if self.messages[id] != nil {
|
||||
self.messages.removeValue(forKey: id)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
case let .Update(tag, id):
|
||||
if tag == self.tag {
|
||||
if self.messages[id] != nil {
|
||||
if let message = postbox.getMessage(id) {
|
||||
self.messages[id] = message
|
||||
updated = true
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return LocalMessageTagsView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class LocalMessageTagsView: PostboxView {
|
||||
public var messages: [MessageId: Message] = [:]
|
||||
|
||||
init(_ view: MutableLocalMessageTagsView) {
|
||||
self.messages = view.messages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
101
submodules/Postbox/Postbox/ManagedFile.swift
Normal file
101
submodules/Postbox/Postbox/ManagedFile.swift
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import Foundation
|
||||
#if os(macOS)
|
||||
import SwiftSignalKitMac
|
||||
#else
|
||||
import SwiftSignalKit
|
||||
#endif
|
||||
|
||||
public enum ManagedFileMode {
|
||||
case read
|
||||
case readwrite
|
||||
case append
|
||||
}
|
||||
|
||||
private func wrappedWrite(_ fd: Int32, _ data: UnsafeRawPointer, _ count: Int) -> Int {
|
||||
return write(fd, data, count)
|
||||
}
|
||||
|
||||
private func wrappedRead(_ fd: Int32, _ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
|
||||
return read(fd, data, count)
|
||||
}
|
||||
|
||||
public final class ManagedFile {
|
||||
private let queue: Queue
|
||||
private let fd: Int32
|
||||
private let mode: ManagedFileMode
|
||||
|
||||
public init?(queue: Queue, path: String, mode: ManagedFileMode) {
|
||||
assert(queue.isCurrent())
|
||||
self.queue = queue
|
||||
self.mode = mode
|
||||
let fileMode: Int32
|
||||
let accessMode: UInt16
|
||||
switch mode {
|
||||
case .read:
|
||||
fileMode = O_RDONLY
|
||||
accessMode = S_IRUSR
|
||||
case .readwrite:
|
||||
fileMode = O_RDWR | O_CREAT
|
||||
accessMode = S_IRUSR | S_IWUSR
|
||||
case .append:
|
||||
fileMode = O_WRONLY | O_CREAT | O_APPEND
|
||||
accessMode = S_IRUSR | S_IWUSR
|
||||
}
|
||||
let fd = open(path, fileMode, accessMode)
|
||||
if fd >= 0 {
|
||||
self.fd = fd
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
close(self.fd)
|
||||
}
|
||||
|
||||
public func write(_ data: UnsafeRawPointer, count: Int) -> Int {
|
||||
assert(self.queue.isCurrent())
|
||||
return wrappedWrite(self.fd, data, count)
|
||||
}
|
||||
|
||||
public func read(_ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
|
||||
assert(self.queue.isCurrent())
|
||||
return wrappedRead(self.fd, data, count)
|
||||
}
|
||||
|
||||
public func readData(count: Int) -> Data {
|
||||
assert(self.queue.isCurrent())
|
||||
var result = Data(count: count)
|
||||
result.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<Int8>) -> Void in
|
||||
let readCount = self.read(bytes, count)
|
||||
assert(readCount == count)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func seek(position: Int64) {
|
||||
assert(self.queue.isCurrent())
|
||||
lseek(self.fd, position, SEEK_SET)
|
||||
}
|
||||
|
||||
public func truncate(count: Int64) {
|
||||
assert(self.queue.isCurrent())
|
||||
ftruncate(self.fd, count)
|
||||
}
|
||||
|
||||
public func getSize() -> Int? {
|
||||
assert(self.queue.isCurrent())
|
||||
var value = stat()
|
||||
if fstat(self.fd, &value) == 0 {
|
||||
return Int(value.st_size)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func sync() {
|
||||
assert(self.queue.isCurrent())
|
||||
fsync(self.fd)
|
||||
}
|
||||
}
|
||||
51
submodules/Postbox/Postbox/MappedFile.swift
Normal file
51
submodules/Postbox/Postbox/MappedFile.swift
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Foundation
|
||||
|
||||
public final class MappedFile {
|
||||
private var handle: Int32
|
||||
private var currentSize: Int
|
||||
private var memory: UnsafeMutableRawPointer
|
||||
|
||||
public init(path: String) {
|
||||
self.handle = open(path, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR)
|
||||
|
||||
var value = stat()
|
||||
stat(path, &value)
|
||||
self.currentSize = Int(value.st_size)
|
||||
|
||||
self.memory = mmap(nil, self.currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, self.handle, 0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
munmap(self.memory, self.currentSize)
|
||||
close(self.handle)
|
||||
}
|
||||
|
||||
public var size: Int {
|
||||
get {
|
||||
return self.currentSize
|
||||
} set(value) {
|
||||
if value != self.currentSize {
|
||||
munmap(self.memory, self.currentSize)
|
||||
ftruncate(self.handle, off_t(value))
|
||||
self.currentSize = value
|
||||
self.memory = mmap(nil, self.currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, self.handle, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func synchronize() {
|
||||
msync(self.memory, self.currentSize, MS_ASYNC)
|
||||
}
|
||||
|
||||
public func write(at range: Range<Int>, from data: UnsafeRawPointer) {
|
||||
memcpy(self.memory.advanced(by: range.lowerBound), data, range.count)
|
||||
}
|
||||
|
||||
public func read(at range: Range<Int>, to data: UnsafeMutableRawPointer) {
|
||||
memcpy(data, self.memory.advanced(by: range.lowerBound), range.count)
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
memset(self.memory, 0, self.currentSize)
|
||||
}
|
||||
}
|
||||
89
submodules/Postbox/Postbox/Media.swift
Normal file
89
submodules/Postbox/Postbox/Media.swift
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import Foundation
|
||||
|
||||
public struct MediaId: Hashable, PostboxCoding, CustomStringConvertible {
|
||||
public typealias Namespace = Int32
|
||||
public typealias Id = Int64
|
||||
|
||||
public let namespace: Namespace
|
||||
public let id: Id
|
||||
|
||||
public var description: String {
|
||||
get {
|
||||
return "\(namespace):\(id)"
|
||||
}
|
||||
}
|
||||
|
||||
public init(namespace: Namespace, id: Id) {
|
||||
self.namespace = namespace
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public init(_ buffer: ReadBuffer) {
|
||||
var namespace: Int32 = 0
|
||||
var id: Int64 = 0
|
||||
|
||||
memcpy(&namespace, buffer.memory + buffer.offset, 4)
|
||||
self.namespace = namespace
|
||||
memcpy(&id, buffer.memory + (buffer.offset + 4), 8)
|
||||
self.id = id
|
||||
buffer.offset += 12
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.namespace = decoder.decodeInt32ForKey("n", orElse: 0)
|
||||
self.id = decoder.decodeInt64ForKey("i", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.namespace, forKey: "n")
|
||||
encoder.encodeInt64(self.id, forKey: "i")
|
||||
}
|
||||
|
||||
public func encodeToBuffer(_ buffer: WriteBuffer) {
|
||||
var namespace = self.namespace
|
||||
var id = self.id
|
||||
buffer.write(&namespace, offset: 0, length: 4);
|
||||
buffer.write(&id, offset: 0, length: 8);
|
||||
}
|
||||
|
||||
public static func encodeArrayToBuffer(_ array: [MediaId], buffer: WriteBuffer) {
|
||||
var length: Int32 = Int32(array.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
for id in array {
|
||||
id.encodeToBuffer(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
public static func decodeArrayFromBuffer(_ buffer: ReadBuffer) -> [MediaId] {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, buffer.memory, 4)
|
||||
buffer.offset += 4
|
||||
var i = 0
|
||||
var array: [MediaId] = []
|
||||
while i < Int(length) {
|
||||
array.append(MediaId(buffer))
|
||||
i += 1
|
||||
}
|
||||
return array
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AssociatedMediaData: class, PostboxCoding {
|
||||
func isEqual(to: AssociatedMediaData) -> Bool
|
||||
}
|
||||
|
||||
public protocol Media: class, PostboxCoding {
|
||||
var id: MediaId? { get }
|
||||
var peerIds: [PeerId] { get }
|
||||
|
||||
func isLikelyToBeUpdated() -> Bool
|
||||
|
||||
func isEqual(to other: Media) -> Bool
|
||||
func isSemanticallyEqual(to other: Media) -> Bool
|
||||
}
|
||||
|
||||
public extension Media {
|
||||
func isLikelyToBeUpdated() -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
933
submodules/Postbox/Postbox/MediaBox.swift
Normal file
933
submodules/Postbox/Postbox/MediaBox.swift
Normal file
|
|
@ -0,0 +1,933 @@
|
|||
import Foundation
|
||||
#if os(macOS)
|
||||
import SwiftSignalKitMac
|
||||
#else
|
||||
import SwiftSignalKit
|
||||
#endif
|
||||
|
||||
private final class ResourceStatusContext {
|
||||
var status: MediaResourceStatus?
|
||||
let subscribers = Bag<(MediaResourceStatus) -> Void>()
|
||||
let disposable: Disposable
|
||||
|
||||
init(disposable: Disposable) {
|
||||
self.disposable = disposable
|
||||
}
|
||||
}
|
||||
|
||||
private final class ResourceDataContext {
|
||||
var data: MediaResourceData
|
||||
var processedFetch: Bool = false
|
||||
let progresiveDataSubscribers = Bag<(waitUntilFetchStatus: Bool, sink: (MediaResourceData) -> Void)>()
|
||||
let completeDataSubscribers = Bag<(waitUntilFetchStatus: Bool, sink: (MediaResourceData) -> Void)>()
|
||||
|
||||
var fetchDisposable: Disposable?
|
||||
let fetchSubscribers = Bag<Void>()
|
||||
|
||||
init(data: MediaResourceData) {
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
|
||||
public enum ResourceDataRangeMode {
|
||||
case complete
|
||||
case incremental
|
||||
case partial
|
||||
}
|
||||
|
||||
public enum FetchResourceSourceType {
|
||||
case local
|
||||
case remote
|
||||
}
|
||||
|
||||
public enum FetchResourceError {
|
||||
case generic
|
||||
}
|
||||
|
||||
private struct ResourceStorePaths {
|
||||
let partial: String
|
||||
let complete: String
|
||||
}
|
||||
|
||||
public struct MediaResourceData {
|
||||
public let path: String
|
||||
public let offset: Int
|
||||
public let size: Int
|
||||
public let complete: Bool
|
||||
|
||||
public init(path: String, offset: Int, size: Int, complete: Bool) {
|
||||
self.path = path
|
||||
self.offset = offset
|
||||
self.size = size
|
||||
self.complete = complete
|
||||
}
|
||||
}
|
||||
|
||||
public protocol MediaResourceDataFetchCopyLocalItem {
|
||||
func copyTo(url: URL) -> Bool
|
||||
}
|
||||
|
||||
public enum MediaBoxFetchPriority: Int32 {
|
||||
case `default` = 0
|
||||
case elevated = 1
|
||||
}
|
||||
|
||||
public enum MediaResourceDataFetchResult {
|
||||
case dataPart(resourceOffset: Int, data: Data, range: Range<Int>, complete: Bool)
|
||||
case resourceSizeUpdated(Int)
|
||||
case progressUpdated(Float)
|
||||
case replaceHeader(data: Data, range: Range<Int>)
|
||||
case moveLocalFile(path: String)
|
||||
case moveTempFile(file: TempBoxFile)
|
||||
case copyLocalItem(MediaResourceDataFetchCopyLocalItem)
|
||||
case reset
|
||||
}
|
||||
|
||||
public enum MediaResourceDataFetchError {
|
||||
case generic
|
||||
}
|
||||
|
||||
public struct CachedMediaResourceRepresentationResult {
|
||||
public let temporaryPath: String
|
||||
|
||||
public init(temporaryPath: String) {
|
||||
self.temporaryPath = temporaryPath
|
||||
}
|
||||
}
|
||||
|
||||
private struct CachedMediaResourceRepresentationKey: Hashable {
|
||||
let resourceId: MediaResourceId
|
||||
let representation: CachedMediaResourceRepresentation
|
||||
|
||||
static func ==(lhs: CachedMediaResourceRepresentationKey, rhs: CachedMediaResourceRepresentationKey) -> Bool {
|
||||
return lhs.resourceId.isEqual(to: rhs.resourceId) && lhs.representation.isEqual(to: rhs.representation)
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
return self.resourceId.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
private final class CachedMediaResourceRepresentationContext {
|
||||
var currentData: MediaResourceData?
|
||||
let dataSubscribers = Bag<(MediaResourceData) -> Void>()
|
||||
let disposable = MetaDisposable()
|
||||
var initialized = false
|
||||
}
|
||||
|
||||
public enum ResourceDataRequestOption {
|
||||
case complete(waitUntilFetchStatus: Bool)
|
||||
case incremental(waitUntilFetchStatus: Bool)
|
||||
}
|
||||
|
||||
public final class MediaBox {
|
||||
public let basePath: String
|
||||
|
||||
private let statusQueue = Queue()
|
||||
private let concurrentQueue = Queue.concurrentDefaultQueue()
|
||||
private let dataQueue = Queue()
|
||||
private let cacheQueue = Queue()
|
||||
private let timeBasedCleanup: TimeBasedCleanup
|
||||
|
||||
private var statusContexts: [WrappedMediaResourceId: ResourceStatusContext] = [:]
|
||||
private var cachedRepresentationContexts: [CachedMediaResourceRepresentationKey: CachedMediaResourceRepresentationContext] = [:]
|
||||
|
||||
private var fileContexts: [WrappedMediaResourceId: MediaBoxFileContext] = [:]
|
||||
|
||||
private var wrappedFetchResource = Promise<(MediaResource, Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, MediaResourceFetchParameters?) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>>()
|
||||
public var preFetchedResourcePath: (MediaResource) -> String? = { _ in return nil }
|
||||
public var fetchResource: ((MediaResource, Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, MediaResourceFetchParameters?) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>)? {
|
||||
didSet {
|
||||
if let fetchResource = self.fetchResource {
|
||||
wrappedFetchResource.set(.single(fetchResource))
|
||||
} else {
|
||||
wrappedFetchResource.set(.never())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var wrappedFetchCachedResourceRepresentation = Promise<(MediaResource, CachedMediaResourceRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError>>()
|
||||
public var fetchCachedResourceRepresentation: ((MediaResource, CachedMediaResourceRepresentation) -> Signal<CachedMediaResourceRepresentationResult, NoError>)? {
|
||||
didSet {
|
||||
if let fetchCachedResourceRepresentation = self.fetchCachedResourceRepresentation {
|
||||
wrappedFetchCachedResourceRepresentation.set(.single(fetchCachedResourceRepresentation))
|
||||
} else {
|
||||
wrappedFetchCachedResourceRepresentation.set(.never())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy var ensureDirectoryCreated: Void = {
|
||||
try! FileManager.default.createDirectory(atPath: self.basePath, withIntermediateDirectories: true, attributes: nil)
|
||||
try! FileManager.default.createDirectory(atPath: self.basePath + "/cache", withIntermediateDirectories: true, attributes: nil)
|
||||
}()
|
||||
|
||||
public init(basePath: String) {
|
||||
self.basePath = basePath
|
||||
|
||||
self.timeBasedCleanup = TimeBasedCleanup(paths: [
|
||||
self.basePath,
|
||||
self.basePath + "/cache"
|
||||
])
|
||||
|
||||
let _ = self.ensureDirectoryCreated
|
||||
}
|
||||
|
||||
public func setMaxStoreTime(_ maxStoreTime: Int32) {
|
||||
self.timeBasedCleanup.setMaxStoreTime(maxStoreTime)
|
||||
}
|
||||
|
||||
private func fileNameForId(_ id: MediaResourceId) -> String {
|
||||
return "\(id.uniqueId)"
|
||||
}
|
||||
|
||||
private func pathForId(_ id: MediaResourceId) -> String {
|
||||
return "\(self.basePath)/\(fileNameForId(id))"
|
||||
}
|
||||
|
||||
private func storePathsForId(_ id: MediaResourceId) -> ResourceStorePaths {
|
||||
return ResourceStorePaths(partial: "\(self.basePath)/\(fileNameForId(id))_partial", complete: "\(self.basePath)/\(fileNameForId(id))")
|
||||
}
|
||||
|
||||
private func cachedRepresentationPathForId(_ id: MediaResourceId, representation: CachedMediaResourceRepresentation) -> String {
|
||||
return "\(self.basePath)/cache/\(fileNameForId(id)):\(representation.uniqueId)"
|
||||
}
|
||||
|
||||
public func storeResourceData(_ id: MediaResourceId, data: Data) {
|
||||
self.dataQueue.async {
|
||||
let paths = self.storePathsForId(id)
|
||||
let _ = try? data.write(to: URL(fileURLWithPath: paths.complete), options: [.atomic])
|
||||
}
|
||||
}
|
||||
|
||||
public func moveResourceData(_ id: MediaResourceId, fromTempPath: String) {
|
||||
self.dataQueue.async {
|
||||
let paths = self.storePathsForId(id)
|
||||
let _ = try? FileManager.default.moveItem(at: URL(fileURLWithPath: fromTempPath), to: URL(fileURLWithPath: paths.complete))
|
||||
}
|
||||
}
|
||||
|
||||
public func copyResourceData(_ id: MediaResourceId, fromTempPath: String) {
|
||||
self.dataQueue.async {
|
||||
let paths = self.storePathsForId(id)
|
||||
let _ = try? FileManager.default.copyItem(at: URL(fileURLWithPath: fromTempPath), to: URL(fileURLWithPath: paths.complete))
|
||||
}
|
||||
}
|
||||
|
||||
public func moveResourceData(from: MediaResourceId, to: MediaResourceId) {
|
||||
self.dataQueue.async {
|
||||
let pathsFrom = self.storePathsForId(from)
|
||||
let pathsTo = self.storePathsForId(to)
|
||||
link(pathsFrom.partial, pathsTo.partial)
|
||||
link(pathsFrom.complete, pathsTo.complete)
|
||||
}
|
||||
}
|
||||
|
||||
private func maybeCopiedPreFetchedResource(completePath: String, resource: MediaResource) {
|
||||
if let path = self.preFetchedResourcePath(resource) {
|
||||
let _ = try? FileManager.default.copyItem(atPath: path, toPath: completePath)
|
||||
}
|
||||
}
|
||||
|
||||
public func resourceStatus(_ resource: MediaResource, approximateSynchronousValue: Bool = false) -> Signal<MediaResourceStatus, NoError> {
|
||||
let signal = Signal<MediaResourceStatus, NoError> { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.concurrentQueue.async {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
|
||||
if let _ = fileSize(paths.complete) {
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
paths.complete
|
||||
])
|
||||
subscriber.putNext(.Local)
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
self.maybeCopiedPreFetchedResource(completePath: paths.complete, resource: resource)
|
||||
if let _ = fileSize(paths.complete) {
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
paths.complete
|
||||
])
|
||||
subscriber.putNext(.Local)
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
self.statusQueue.async {
|
||||
let resourceId = WrappedMediaResourceId(resource.id)
|
||||
let statusContext: ResourceStatusContext
|
||||
var statusUpdateDisposable: MetaDisposable?
|
||||
if let current = self.statusContexts[resourceId] {
|
||||
statusContext = current
|
||||
} else {
|
||||
let statusUpdateDisposableValue = MetaDisposable()
|
||||
statusContext = ResourceStatusContext(disposable: statusUpdateDisposableValue)
|
||||
self.statusContexts[resourceId] = statusContext
|
||||
statusUpdateDisposable = statusUpdateDisposableValue
|
||||
}
|
||||
|
||||
let index = statusContext.subscribers.add({ status in
|
||||
subscriber.putNext(status)
|
||||
})
|
||||
|
||||
if let status = statusContext.status {
|
||||
subscriber.putNext(status)
|
||||
}
|
||||
|
||||
if let statusUpdateDisposable = statusUpdateDisposable {
|
||||
let statusQueue = self.statusQueue
|
||||
self.dataQueue.async {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resource) {
|
||||
let statusDisposable = fileContext.status(next: { [weak statusContext] value in
|
||||
statusQueue.async {
|
||||
if let current = self.statusContexts[resourceId], current === statusContext, current.status != value {
|
||||
current.status = value
|
||||
for subscriber in current.subscribers.copyItems() {
|
||||
subscriber(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, completed: { [weak statusContext] in
|
||||
statusQueue.async {
|
||||
if let current = self.statusContexts[resourceId], current === statusContext {
|
||||
current.subscribers.remove(index)
|
||||
if current.subscribers.isEmpty {
|
||||
self.statusContexts.removeValue(forKey: resourceId)
|
||||
current.disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, size: resource.size.flatMap(Int32.init))
|
||||
statusUpdateDisposable.set(ActionDisposable {
|
||||
statusDisposable.dispose()
|
||||
releaseContext()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disposable.set(ActionDisposable { [weak statusContext] in
|
||||
self.statusQueue.async {
|
||||
if let current = self.statusContexts[WrappedMediaResourceId(resource.id)], current === statusContext {
|
||||
current.subscribers.remove(index)
|
||||
if current.subscribers.isEmpty {
|
||||
self.statusContexts.removeValue(forKey: WrappedMediaResourceId(resource.id))
|
||||
current.disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
if approximateSynchronousValue {
|
||||
return Signal<Signal<MediaResourceStatus, NoError>, NoError> { subscriber in
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
if let _ = fileSize(paths.complete) {
|
||||
subscriber.putNext(.single(.Local))
|
||||
} else {
|
||||
subscriber.putNext(.single(.Remote) |> then(signal))
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
} |> switchToLatest
|
||||
} else {
|
||||
return signal
|
||||
}
|
||||
}
|
||||
|
||||
public func resourcePath(_ resource: MediaResource) -> String {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
return paths.complete
|
||||
}
|
||||
|
||||
public func completedResourcePath(_ resource: MediaResource, pathExtension: String? = nil) -> String? {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
if let _ = fileSize(paths.complete) {
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
paths.complete
|
||||
])
|
||||
if let pathExtension = pathExtension {
|
||||
let symlinkPath = paths.complete + ".\(pathExtension)"
|
||||
if fileSize(symlinkPath) == nil {
|
||||
let _ = try? FileManager.default.linkItem(atPath: paths.complete, toPath: symlinkPath)
|
||||
}
|
||||
return symlinkPath
|
||||
} else {
|
||||
return paths.complete
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func resourceData(_ resource: MediaResource, pathExtension: String? = nil, option: ResourceDataRequestOption = .complete(waitUntilFetchStatus: false), attemptSynchronously: Bool = false) -> Signal<MediaResourceData, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
let begin: () -> Void = {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
|
||||
var completeSize = fileSize(paths.complete)
|
||||
if completeSize == nil {
|
||||
self.maybeCopiedPreFetchedResource(completePath: paths.complete, resource: resource)
|
||||
completeSize = fileSize(paths.complete)
|
||||
}
|
||||
|
||||
if let completeSize = fileSize(paths.complete) {
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
paths.complete
|
||||
])
|
||||
if let pathExtension = pathExtension {
|
||||
let symlinkPath = paths.complete + ".\(pathExtension)"
|
||||
if fileSize(symlinkPath) == nil {
|
||||
let _ = try? FileManager.default.linkItem(atPath: paths.complete, toPath: symlinkPath)
|
||||
}
|
||||
subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: completeSize, complete: true))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext(MediaResourceData(path: paths.complete, offset: 0, size: completeSize, complete: true))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
} else {
|
||||
if attemptSynchronously, case .complete(false) = option {
|
||||
subscriber.putNext(MediaResourceData(path: paths.partial, offset: 0, size: fileSize(paths.partial) ?? 0, complete: false))
|
||||
}
|
||||
self.dataQueue.async {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resource) {
|
||||
let waitUntilAfterInitialFetch: Bool
|
||||
switch option {
|
||||
case let .complete(waitUntilFetchStatus):
|
||||
waitUntilAfterInitialFetch = waitUntilFetchStatus
|
||||
case let .incremental(waitUntilFetchStatus):
|
||||
waitUntilAfterInitialFetch = waitUntilFetchStatus
|
||||
}
|
||||
let dataDisposable = fileContext.data(range: 0 ..< Int32.max, waitUntilAfterInitialFetch: waitUntilAfterInitialFetch, next: { value in
|
||||
self.dataQueue.async {
|
||||
if value.complete {
|
||||
if let pathExtension = pathExtension {
|
||||
let symlinkPath = paths.complete + ".\(pathExtension)"
|
||||
if fileSize(symlinkPath) == nil {
|
||||
let _ = try? FileManager.default.linkItem(atPath: paths.complete, toPath: symlinkPath)
|
||||
}
|
||||
subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: value.size, complete: true))
|
||||
} else {
|
||||
subscriber.putNext(value)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
disposable.set(ActionDisposable {
|
||||
dataDisposable.dispose()
|
||||
releaseContext()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if attemptSynchronously {
|
||||
begin()
|
||||
} else {
|
||||
self.concurrentQueue.async(begin)
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
private func fileContext(for resource: MediaResource) -> (MediaBoxFileContext, () -> Void)? {
|
||||
assert(self.dataQueue.isCurrent())
|
||||
|
||||
let resourceId = WrappedMediaResourceId(resource.id)
|
||||
|
||||
var context: MediaBoxFileContext?
|
||||
if let current = self.fileContexts[resourceId] {
|
||||
context = current
|
||||
} else {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
paths.complete,
|
||||
paths.partial,
|
||||
paths.partial + ".meta"
|
||||
])
|
||||
if let fileContext = MediaBoxFileContext(queue: self.dataQueue, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") {
|
||||
context = fileContext
|
||||
self.fileContexts[resourceId] = fileContext
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if let context = context {
|
||||
let index = context.addReference()
|
||||
let queue = self.dataQueue
|
||||
return (context, { [weak self, weak context] in
|
||||
queue.async {
|
||||
guard let strongSelf = self, let previousContext = context, let context = strongSelf.fileContexts[resourceId], context === previousContext else {
|
||||
return
|
||||
}
|
||||
context.removeReference(index)
|
||||
if context.isEmpty {
|
||||
strongSelf.fileContexts.removeValue(forKey: resourceId)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchedResourceData(_ resource: MediaResource, in range: Range<Int>, priority: MediaBoxFetchPriority = .default, parameters: MediaResourceFetchParameters?) -> Signal<Void, FetchResourceError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.dataQueue.async {
|
||||
guard let (fileContext, releaseContext) = self.fileContext(for: resource) else {
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
let fetchResource = self.wrappedFetchResource.get()
|
||||
let fetchedDisposable = fileContext.fetched(range: Int32(range.lowerBound) ..< Int32(range.upperBound), priority: priority, fetch: { intervals in
|
||||
return fetchResource
|
||||
|> introduceError(MediaResourceDataFetchError.self)
|
||||
|> mapToSignal { fetch in
|
||||
return fetch(resource, intervals, parameters)
|
||||
}
|
||||
}, error: { _ in
|
||||
subscriber.putCompletion()
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
disposable.set(ActionDisposable {
|
||||
fetchedDisposable.dispose()
|
||||
releaseContext()
|
||||
})
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func resourceData(_ resource: MediaResource, size: Int, in range: Range<Int>, mode: ResourceDataRangeMode = .complete) -> Signal<Data, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.dataQueue.async {
|
||||
guard let (fileContext, releaseContext) = self.fileContext(for: resource) else {
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
let range = Int32(range.lowerBound) ..< Int32(range.upperBound)
|
||||
|
||||
let dataDisposable = fileContext.data(range: range, waitUntilAfterInitialFetch: false, next: { result in
|
||||
if let file = ManagedFile(queue: self.dataQueue, path: result.path, mode: .read), let fileSize = file.getSize() {
|
||||
if result.complete {
|
||||
if result.offset + result.size <= fileSize {
|
||||
if fileSize >= result.offset + result.size {
|
||||
file.seek(position: Int64(result.offset))
|
||||
let resultData = file.readData(count: result.size)
|
||||
subscriber.putNext(resultData)
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
assertionFailure("data.count >= result.offset + result.size")
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
} else {
|
||||
switch mode {
|
||||
case .complete:
|
||||
break
|
||||
case .incremental:
|
||||
break
|
||||
case .partial:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
disposable.set(ActionDisposable {
|
||||
dataDisposable.dispose()
|
||||
releaseContext()
|
||||
})
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func resourceRangesStatus(_ resource: MediaResource) -> Signal<IndexSet, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.dataQueue.async {
|
||||
guard let (fileContext, releaseContext) = self.fileContext(for: resource) else {
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
let statusDisposable = fileContext.rangeStatus(next: { result in
|
||||
subscriber.putNext(result)
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
disposable.set(ActionDisposable {
|
||||
statusDisposable.dispose()
|
||||
releaseContext()
|
||||
})
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchedResource(_ resource: MediaResource, parameters: MediaResourceFetchParameters?, implNext: Bool = false) -> Signal<FetchResourceSourceType, FetchResourceError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.dataQueue.async {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
|
||||
if let _ = fileSize(paths.complete) {
|
||||
if implNext {
|
||||
subscriber.putNext(.local)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resource) {
|
||||
let fetchResource = self.wrappedFetchResource.get()
|
||||
let fetchedDisposable = fileContext.fetchedFullRange(fetch: { ranges in
|
||||
return fetchResource
|
||||
|> introduceError(MediaResourceDataFetchError.self)
|
||||
|> mapToSignal { fetch in
|
||||
return fetch(resource, ranges, parameters)
|
||||
}
|
||||
}, error: { _ in
|
||||
subscriber.putError(.generic)
|
||||
}, completed: {
|
||||
if implNext {
|
||||
subscriber.putNext(.remote)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
disposable.set(ActionDisposable {
|
||||
fetchedDisposable.dispose()
|
||||
releaseContext()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
public func cancelInteractiveResourceFetch(_ resource: MediaResource) {
|
||||
self.dataQueue.async {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resource) {
|
||||
fileContext.cancelFullRangeFetches()
|
||||
releaseContext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func storeCachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, data: Data) {
|
||||
self.dataQueue.async {
|
||||
let path = self.cachedRepresentationPathForId(resource.id, representation: representation)
|
||||
let _ = try? data.write(to: URL(fileURLWithPath: path))
|
||||
}
|
||||
}
|
||||
|
||||
public func cachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, pathExtension: String? = nil, complete: Bool, fetch: Bool = true, attemptSynchronously: Bool = false) -> Signal<MediaResourceData, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
let begin: () -> Void = {
|
||||
let path = self.cachedRepresentationPathForId(resource.id, representation: representation)
|
||||
if let size = fileSize(path) {
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
path
|
||||
])
|
||||
|
||||
if let pathExtension = pathExtension {
|
||||
let symlinkPath = path + ".\(pathExtension)"
|
||||
if fileSize(symlinkPath) == nil {
|
||||
let _ = try? FileManager.default.linkItem(atPath: path, toPath: symlinkPath)
|
||||
}
|
||||
subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: size, complete: true))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext(MediaResourceData(path: path, offset: 0, size: size, complete: true))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
} else if fetch {
|
||||
if attemptSynchronously && complete {
|
||||
subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false))
|
||||
}
|
||||
self.dataQueue.async {
|
||||
let key = CachedMediaResourceRepresentationKey(resourceId: resource.id, representation: representation)
|
||||
let context: CachedMediaResourceRepresentationContext
|
||||
if let currentContext = self.cachedRepresentationContexts[key] {
|
||||
context = currentContext
|
||||
} else {
|
||||
context = CachedMediaResourceRepresentationContext()
|
||||
self.cachedRepresentationContexts[key] = context
|
||||
}
|
||||
|
||||
let index = context.dataSubscribers.add(({ data in
|
||||
if !complete || data.complete {
|
||||
if let pathExtension = pathExtension, data.complete {
|
||||
let symlinkPath = data.path + ".\(pathExtension)"
|
||||
if fileSize(symlinkPath) == nil {
|
||||
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
|
||||
}
|
||||
subscriber.putNext(MediaResourceData(path: symlinkPath, offset: data.offset, size: data.size, complete: data.complete))
|
||||
} else {
|
||||
subscriber.putNext(data)
|
||||
}
|
||||
}
|
||||
if data.complete {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}))
|
||||
if let currentData = context.currentData {
|
||||
if !complete || currentData.complete {
|
||||
subscriber.putNext(currentData)
|
||||
}
|
||||
if currentData.complete {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
} else if !complete {
|
||||
subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false))
|
||||
}
|
||||
|
||||
disposable.set(ActionDisposable { [weak context] in
|
||||
self.dataQueue.async {
|
||||
if let currentContext = self.cachedRepresentationContexts[key], currentContext === context {
|
||||
currentContext.dataSubscribers.remove(index)
|
||||
if currentContext.dataSubscribers.isEmpty {
|
||||
currentContext.disposable.dispose()
|
||||
self.cachedRepresentationContexts.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if !context.initialized {
|
||||
context.initialized = true
|
||||
let signal = self.wrappedFetchCachedResourceRepresentation.get()
|
||||
|> take(1)
|
||||
|> mapToSignal { fetch in
|
||||
return fetch(resource, representation)
|
||||
|> map(Optional.init)
|
||||
}
|
||||
|> deliverOn(self.dataQueue)
|
||||
context.disposable.set(signal.start(next: { [weak self, weak context] next in
|
||||
if let next = next {
|
||||
rename(next.temporaryPath, path)
|
||||
|
||||
if let strongSelf = self, let currentContext = strongSelf.cachedRepresentationContexts[key], currentContext === context {
|
||||
currentContext.disposable.dispose()
|
||||
strongSelf.cachedRepresentationContexts.removeValue(forKey: key)
|
||||
if let size = fileSize(path) {
|
||||
let data = MediaResourceData(path: path, offset: 0, size: size, complete: true)
|
||||
currentContext.currentData = data
|
||||
for subscriber in currentContext.dataSubscribers.copyItems() {
|
||||
subscriber(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let strongSelf = self, let context = strongSelf.cachedRepresentationContexts[key] {
|
||||
let data = MediaResourceData(path: path, offset: 0, size: 0, complete: false)
|
||||
context.currentData = data
|
||||
for subscriber in context.dataSubscribers.copyItems() {
|
||||
subscriber(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscriber.putNext(MediaResourceData(path: path, offset: 0, size: 0, complete: false))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
if attemptSynchronously {
|
||||
begin()
|
||||
} else {
|
||||
self.concurrentQueue.async(begin)
|
||||
}
|
||||
return ActionDisposable {
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func collectResourceCacheUsage(_ ids: [MediaResourceId]) -> Signal<[WrappedMediaResourceId: Int64], NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
var result: [WrappedMediaResourceId: Int64] = [:]
|
||||
for id in ids {
|
||||
let wrappedId = WrappedMediaResourceId(id)
|
||||
let paths = self.storePathsForId(id)
|
||||
if let size = fileSize(paths.complete) {
|
||||
result[wrappedId] = Int64(size)
|
||||
} else if let size = fileSize(paths.partial, useTotalFileAllocatedSize: true) {
|
||||
result[wrappedId] = Int64(size)
|
||||
}
|
||||
}
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func collectOtherResourceUsage(excludeIds: Set<WrappedMediaResourceId>, combinedExcludeIds: Set<WrappedMediaResourceId>) -> Signal<(Int64, [String], Int64), NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
var result: Int64 = 0
|
||||
|
||||
var excludeNames = Set<String>()
|
||||
for id in combinedExcludeIds {
|
||||
let partial = "\(self.fileNameForId(id.id))_partial"
|
||||
let meta = "\(self.fileNameForId(id.id))_meta"
|
||||
let complete = self.fileNameForId(id.id)
|
||||
|
||||
excludeNames.insert(meta)
|
||||
excludeNames.insert(partial)
|
||||
excludeNames.insert(complete)
|
||||
}
|
||||
|
||||
var fileIds = Set<Data>()
|
||||
|
||||
var paths: [String] = []
|
||||
|
||||
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [.fileSizeKey, .fileResourceIdentifierKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) {
|
||||
loop: for url in enumerator {
|
||||
if let url = url as? URL {
|
||||
if excludeNames.contains(url.lastPathComponent) {
|
||||
continue loop
|
||||
}
|
||||
|
||||
if let fileId = (try? url.resourceValues(forKeys: Set([.fileResourceIdentifierKey])))?.fileResourceIdentifier as? Data {
|
||||
if fileIds.contains(fileId) {
|
||||
paths.append(url.lastPathComponent)
|
||||
continue loop
|
||||
}
|
||||
|
||||
if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 {
|
||||
fileIds.insert(fileId)
|
||||
paths.append(url.lastPathComponent)
|
||||
result += Int64(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cacheResult: Int64 = 0
|
||||
|
||||
var excludePrefixes = Set<String>()
|
||||
for id in excludeIds {
|
||||
let cachedRepresentationPrefix = self.fileNameForId(id.id)
|
||||
|
||||
excludePrefixes.insert(cachedRepresentationPrefix)
|
||||
}
|
||||
|
||||
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) {
|
||||
loop: for url in enumerator {
|
||||
if let url = url as? URL {
|
||||
if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) {
|
||||
continue loop
|
||||
}
|
||||
|
||||
if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 {
|
||||
paths.append("cache/" + url.lastPathComponent)
|
||||
cacheResult += Int64(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.putNext((result, paths, cacheResult))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func removeOtherCachedResources(paths: [String]) -> Signal<Void, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
for path in paths {
|
||||
unlink(self.basePath + "/" + path)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func removeCachedResources(_ ids: Set<WrappedMediaResourceId>) -> Signal<Void, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
for id in ids {
|
||||
if self.fileContexts[id] != nil {
|
||||
continue
|
||||
}
|
||||
let paths = self.storePathsForId(id.id)
|
||||
unlink(paths.complete)
|
||||
unlink(paths.partial)
|
||||
unlink(paths.partial + ".meta")
|
||||
self.fileContexts.removeValue(forKey: id)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func clearFileContexts() -> Signal<Void, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
for (id, _) in self.fileContexts {
|
||||
let paths = self.storePathsForId(id.id)
|
||||
unlink(paths.complete)
|
||||
unlink(paths.partial)
|
||||
unlink(paths.partial + ".meta")
|
||||
}
|
||||
self.fileContexts.removeAll()
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func fileConxtets() -> Signal<[(partial: String, complete: String)], NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
var result: [(partial: String, complete: String)] = []
|
||||
for (id, _) in self.fileContexts {
|
||||
let paths = self.storePathsForId(id.id)
|
||||
result.append((partial: paths.partial, complete: paths.complete))
|
||||
}
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
969
submodules/Postbox/Postbox/MediaBoxFile.swift
Normal file
969
submodules/Postbox/Postbox/MediaBoxFile.swift
Normal file
|
|
@ -0,0 +1,969 @@
|
|||
import Foundation
|
||||
#if os(iOS)
|
||||
import SwiftSignalKit
|
||||
#else
|
||||
import SwiftSignalKitMac
|
||||
#endif
|
||||
|
||||
import sqlcipher
|
||||
|
||||
private final class MediaBoxFileMap {
|
||||
fileprivate(set) var sum: Int32
|
||||
private(set) var ranges: IndexSet
|
||||
private(set) var truncationSize: Int32?
|
||||
private(set) var progress: Float?
|
||||
|
||||
init() {
|
||||
self.sum = 0
|
||||
self.ranges = IndexSet()
|
||||
self.truncationSize = nil
|
||||
self.progress = nil
|
||||
}
|
||||
|
||||
init?(fd: ManagedFile) {
|
||||
guard let length = fd.getSize() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var crc: UInt32 = 0
|
||||
var count: Int32 = 0
|
||||
var sum: Int32 = 0
|
||||
var ranges: IndexSet = IndexSet()
|
||||
|
||||
guard fd.read(&crc, 4) == 4 else {
|
||||
return nil
|
||||
}
|
||||
guard fd.read(&count, 4) == 4 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if count < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if count < 0 || length < 4 + 4 + count * 2 * 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var truncationSizeValue: Int32 = 0
|
||||
|
||||
var data = Data(count: Int(4 + count * 2 * 4))
|
||||
let dataCount = data.count
|
||||
if !(data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Bool in
|
||||
guard fd.read(bytes, dataCount) == dataCount else {
|
||||
return false
|
||||
}
|
||||
|
||||
memcpy(&truncationSizeValue, bytes, 4)
|
||||
|
||||
let calculatedCrc = Crc32(bytes, Int32(dataCount))
|
||||
if calculatedCrc != crc {
|
||||
return false
|
||||
}
|
||||
|
||||
var offset = 4
|
||||
for _ in 0 ..< count {
|
||||
var intervalOffset: Int32 = 0
|
||||
var intervalLength: Int32 = 0
|
||||
memcpy(&intervalOffset, bytes.advanced(by: offset), 4)
|
||||
memcpy(&intervalLength, bytes.advanced(by: offset + 4), 4)
|
||||
offset += 8
|
||||
|
||||
ranges.insert(integersIn: Int(intervalOffset) ..< Int(intervalOffset + intervalLength))
|
||||
|
||||
sum += intervalLength
|
||||
}
|
||||
|
||||
return true
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.sum = sum
|
||||
self.ranges = ranges
|
||||
if truncationSizeValue == -1 {
|
||||
self.truncationSize = nil
|
||||
} else {
|
||||
self.truncationSize = truncationSizeValue
|
||||
}
|
||||
}
|
||||
|
||||
func serialize(to file: ManagedFile) {
|
||||
file.seek(position: 0)
|
||||
let buffer = WriteBuffer()
|
||||
var zero: Int32 = 0
|
||||
buffer.write(&zero, offset: 0, length: 4)
|
||||
|
||||
let rangeView = self.ranges.rangeView
|
||||
var count: Int32 = Int32(rangeView.count)
|
||||
buffer.write(&count, offset: 0, length: 4)
|
||||
|
||||
var truncationSizeValue: Int32 = self.truncationSize ?? -1
|
||||
buffer.write(&truncationSizeValue, offset: 0, length: 4)
|
||||
|
||||
for range in rangeView {
|
||||
var intervalOffset = Int32(range.lowerBound)
|
||||
var intervalLength = Int32(range.count)
|
||||
buffer.write(&intervalOffset, offset: 0, length: 4)
|
||||
buffer.write(&intervalLength, offset: 0, length: 4)
|
||||
}
|
||||
var crc: UInt32 = Crc32(buffer.memory.advanced(by: 4 * 2), Int32(buffer.length - 4 * 2))
|
||||
memcpy(buffer.memory, &crc, 4)
|
||||
let written = file.write(buffer.memory, count: buffer.length)
|
||||
assert(written == buffer.length)
|
||||
}
|
||||
|
||||
fileprivate func fill(_ range: Range<Int32>) {
|
||||
let intRange: Range<Int> = Int(range.lowerBound) ..< Int(range.upperBound)
|
||||
let previousCount = self.ranges.count(in: intRange)
|
||||
self.ranges.insert(integersIn: intRange)
|
||||
self.sum += Int32(range.count - previousCount)
|
||||
}
|
||||
|
||||
fileprivate func truncate(_ size: Int32) {
|
||||
self.truncationSize = size
|
||||
}
|
||||
fileprivate func progressUpdated(_ progress: Float) {
|
||||
self.progress = progress
|
||||
}
|
||||
|
||||
fileprivate func reset() {
|
||||
self.truncationSize = nil
|
||||
self.ranges.removeAll()
|
||||
self.sum = 0
|
||||
self.progress = nil
|
||||
}
|
||||
|
||||
fileprivate func contains(_ range: Range<Int32>) -> Bool {
|
||||
let maxValue: Int
|
||||
if let truncationSize = self.truncationSize {
|
||||
maxValue = Int(truncationSize)
|
||||
} else {
|
||||
maxValue = Int.max
|
||||
}
|
||||
let intRange: Range<Int> = Int(range.lowerBound) ..< min(maxValue, Int(range.upperBound))
|
||||
return self.ranges.contains(integersIn: intRange)
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaBoxPartialFileDataRequest {
|
||||
let range: Range<Int32>
|
||||
var waitingUntilAfterInitialFetch: Bool
|
||||
let completion: (MediaResourceData) -> Void
|
||||
|
||||
init(range: Range<Int32>, waitingUntilAfterInitialFetch: Bool, completion: @escaping (MediaResourceData) -> Void) {
|
||||
self.range = range
|
||||
self.waitingUntilAfterInitialFetch = waitingUntilAfterInitialFetch
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
final class MediaBoxPartialFile {
|
||||
private let queue: Queue
|
||||
private let path: String
|
||||
private let metaPath: String
|
||||
private let completePath: String
|
||||
private let completed: (Int32) -> Void
|
||||
private let metadataFd: ManagedFile
|
||||
private let fd: ManagedFile
|
||||
fileprivate let fileMap: MediaBoxFileMap
|
||||
private var dataRequests = Bag<MediaBoxPartialFileDataRequest>()
|
||||
private let missingRanges: MediaBoxFileMissingRanges
|
||||
private let rangeStatusRequests = Bag<((IndexSet) -> Void, () -> Void)>()
|
||||
private let statusRequests = Bag<((MediaResourceStatus) -> Void, Int32?)>()
|
||||
|
||||
private let fullRangeRequests = Bag<Disposable>()
|
||||
|
||||
private var currentFetch: (Promise<[(Range<Int>, MediaBoxFetchPriority)]>, Disposable)?
|
||||
private var processedAtLeastOneFetch: Bool = false
|
||||
|
||||
init?(queue: Queue, path: String, metaPath: String, completePath: String, completed: @escaping (Int32) -> Void) {
|
||||
assert(queue.isCurrent())
|
||||
if let metadataFd = ManagedFile(queue: queue, path: metaPath, mode: .readwrite), let fd = ManagedFile(queue: queue, path: path, mode: .readwrite) {
|
||||
self.queue = queue
|
||||
self.path = path
|
||||
self.metaPath = metaPath
|
||||
self.completePath = completePath
|
||||
self.completed = completed
|
||||
self.metadataFd = metadataFd
|
||||
self.fd = fd
|
||||
if let fileMap = MediaBoxFileMap(fd: self.metadataFd) {
|
||||
self.fileMap = fileMap
|
||||
} else {
|
||||
self.fileMap = MediaBoxFileMap()
|
||||
}
|
||||
self.missingRanges = MediaBoxFileMissingRanges()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.currentFetch?.1.dispose()
|
||||
}
|
||||
|
||||
var storedSize: Int32 {
|
||||
assert(self.queue.isCurrent())
|
||||
return self.fileMap.sum
|
||||
}
|
||||
|
||||
func reset() {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
self.fileMap.reset()
|
||||
self.fileMap.serialize(to: self.metadataFd)
|
||||
|
||||
for request in self.dataRequests.copyItems() {
|
||||
request.completion(MediaResourceData(path: self.path, offset: Int(request.range.lowerBound), size: 0, complete: false))
|
||||
}
|
||||
|
||||
if let updatedRanges = self.missingRanges.reset(fileMap: self.fileMap) {
|
||||
self.updateRequestRanges(updatedRanges, fetch: nil)
|
||||
}
|
||||
|
||||
if !self.rangeStatusRequests.isEmpty {
|
||||
let ranges = self.fileMap.ranges
|
||||
for (f, _) in self.rangeStatusRequests.copyItems() {
|
||||
f(ranges)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateStatuses()
|
||||
}
|
||||
|
||||
func moveLocalFile(tempPath: String) {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
do {
|
||||
try FileManager.default.moveItem(atPath: tempPath, toPath: self.completePath)
|
||||
|
||||
if let size = fileSize(self.completePath) {
|
||||
unlink(self.path)
|
||||
unlink(self.metaPath)
|
||||
|
||||
for (_, completion) in self.missingRanges.clear() {
|
||||
completion()
|
||||
}
|
||||
|
||||
if let (_, disposable) = self.currentFetch {
|
||||
self.currentFetch = nil
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
for request in self.dataRequests.copyItems() {
|
||||
request.completion(MediaResourceData(path: self.completePath, offset: Int(request.range.lowerBound), size: max(0, size - Int(request.range.lowerBound)), complete: true))
|
||||
}
|
||||
self.dataRequests.removeAll()
|
||||
|
||||
for statusRequest in self.statusRequests.copyItems() {
|
||||
statusRequest.0(.Local)
|
||||
}
|
||||
self.statusRequests.removeAll()
|
||||
|
||||
self.completed(self.fileMap.sum)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
} catch {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
func copyLocalItem(_ item: MediaResourceDataFetchCopyLocalItem) {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
do {
|
||||
if item.copyTo(url: URL(fileURLWithPath: self.completePath)) {
|
||||
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if let size = fileSize(self.completePath) {
|
||||
unlink(self.path)
|
||||
unlink(self.metaPath)
|
||||
|
||||
for (_, completion) in self.missingRanges.clear() {
|
||||
completion()
|
||||
}
|
||||
|
||||
if let (_, disposable) = self.currentFetch {
|
||||
self.currentFetch = nil
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
for request in self.dataRequests.copyItems() {
|
||||
request.completion(MediaResourceData(path: self.completePath, offset: Int(request.range.lowerBound), size: max(0, size - Int(request.range.lowerBound)), complete: true))
|
||||
}
|
||||
self.dataRequests.removeAll()
|
||||
|
||||
for statusRequest in self.statusRequests.copyItems() {
|
||||
statusRequest.0(.Local)
|
||||
}
|
||||
self.statusRequests.removeAll()
|
||||
|
||||
self.completed(Int32(size))
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
} catch {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
func truncate(_ size: Int32) {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
let range: Range<Int32> = size ..< Int32.max
|
||||
|
||||
self.fileMap.truncate(size)
|
||||
self.fileMap.serialize(to: self.metadataFd)
|
||||
|
||||
self.checkDataRequestsAfterFill(range: range)
|
||||
}
|
||||
|
||||
func progressUpdated(_ progress: Float) {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
self.fileMap.progressUpdated(progress)
|
||||
self.updateStatuses()
|
||||
}
|
||||
|
||||
func write(offset: Int32, data: Data, dataRange: Range<Int>) {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
self.fd.seek(position: Int64(offset))
|
||||
let written = data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Int in
|
||||
return self.fd.write(bytes.advanced(by: dataRange.lowerBound), count: dataRange.count)
|
||||
}
|
||||
assert(written == dataRange.count)
|
||||
let range: Range<Int32> = offset ..< (offset + Int32(dataRange.count))
|
||||
self.fileMap.fill(range)
|
||||
self.fileMap.serialize(to: self.metadataFd)
|
||||
|
||||
self.checkDataRequestsAfterFill(range: range)
|
||||
}
|
||||
|
||||
func checkDataRequestsAfterFill(range: Range<Int32>) {
|
||||
var removeIndices: [(Int, MediaBoxPartialFileDataRequest)] = []
|
||||
for (index, request) in self.dataRequests.copyItemsWithIndices() {
|
||||
if request.range.overlaps(range) {
|
||||
var maxValue = request.range.upperBound
|
||||
if let truncationSize = self.fileMap.truncationSize {
|
||||
maxValue = truncationSize
|
||||
}
|
||||
if request.range.lowerBound > maxValue {
|
||||
assertionFailure()
|
||||
removeIndices.append((index, request))
|
||||
} else {
|
||||
let intRange: Range<Int> = Int(request.range.lowerBound) ..< Int(min(maxValue, request.range.upperBound))
|
||||
if self.fileMap.ranges.contains(integersIn: intRange) {
|
||||
removeIndices.append((index, request))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !removeIndices.isEmpty {
|
||||
for (index, request) in removeIndices {
|
||||
self.dataRequests.remove(index)
|
||||
var maxValue = request.range.upperBound
|
||||
if let truncationSize = self.fileMap.truncationSize, truncationSize < maxValue {
|
||||
maxValue = truncationSize
|
||||
}
|
||||
request.completion(MediaResourceData(path: self.path, offset: Int(request.range.lowerBound), size: Int(maxValue) - Int(request.range.lowerBound), complete: true))
|
||||
}
|
||||
}
|
||||
|
||||
var isCompleted = false
|
||||
if let truncationSize = self.fileMap.truncationSize, self.fileMap.contains(0 ..< truncationSize) {
|
||||
isCompleted = true
|
||||
}
|
||||
|
||||
if isCompleted {
|
||||
for (_, completion) in self.missingRanges.clear() {
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
if let (updatedRanges, completions) = self.missingRanges.fill(range) {
|
||||
self.updateRequestRanges(updatedRanges, fetch: nil)
|
||||
completions.forEach({ $0() })
|
||||
}
|
||||
}
|
||||
|
||||
if !self.rangeStatusRequests.isEmpty {
|
||||
let ranges = self.fileMap.ranges
|
||||
for (f, completed) in self.rangeStatusRequests.copyItems() {
|
||||
f(ranges)
|
||||
if isCompleted {
|
||||
completed()
|
||||
}
|
||||
}
|
||||
if isCompleted {
|
||||
self.rangeStatusRequests.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
self.updateStatuses()
|
||||
|
||||
if isCompleted {
|
||||
for statusRequest in self.statusRequests.copyItems() {
|
||||
statusRequest.0(.Local)
|
||||
}
|
||||
self.statusRequests.removeAll()
|
||||
self.fd.sync()
|
||||
let linkResult = link(self.path, self.completePath)
|
||||
if linkResult != 0 {
|
||||
//assert(linkResult == 0)
|
||||
}
|
||||
self.completed(self.fileMap.sum)
|
||||
}
|
||||
}
|
||||
|
||||
func read(range: Range<Int32>) -> Data? {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if self.fileMap.contains(range) {
|
||||
self.fd.seek(position: Int64(range.lowerBound))
|
||||
var data = Data(count: range.count)
|
||||
let dataCount = data.count
|
||||
let readBytes = data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<Int8>) -> Int in
|
||||
return self.fd.read(bytes, dataCount)
|
||||
}
|
||||
if readBytes == data.count {
|
||||
return data
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func data(range: Range<Int32>, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if self.fileMap.contains(range) {
|
||||
next(MediaResourceData(path: self.path, offset: Int(range.lowerBound), size: range.count, complete: true))
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
var waitingUntilAfterInitialFetch = false
|
||||
if waitUntilAfterInitialFetch && !self.processedAtLeastOneFetch {
|
||||
waitingUntilAfterInitialFetch = true
|
||||
} else {
|
||||
next(MediaResourceData(path: self.path, offset: Int(range.lowerBound), size: 0, complete: false))
|
||||
}
|
||||
|
||||
let index = self.dataRequests.add(MediaBoxPartialFileDataRequest(range: range, waitingUntilAfterInitialFetch: waitingUntilAfterInitialFetch, completion: { data in
|
||||
next(data)
|
||||
}))
|
||||
|
||||
let queue = self.queue
|
||||
return ActionDisposable { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.dataRequests.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetched(range: Range<Int32>, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
if self.fileMap.contains(range) {
|
||||
completed()
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let (index, updatedRanges) = self.missingRanges.addRequest(fileMap: self.fileMap, range: range, priority: priority, error: error, completion: {
|
||||
completed()
|
||||
})
|
||||
if let updatedRanges = updatedRanges {
|
||||
self.updateRequestRanges(updatedRanges, fetch: fetch)
|
||||
}
|
||||
|
||||
let queue = self.queue
|
||||
return ActionDisposable { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
if let updatedRanges = strongSelf.missingRanges.removeRequest(fileMap: strongSelf.fileMap, index: index) {
|
||||
strongSelf.updateRequestRanges(updatedRanges, fetch: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchedFullRange(fetch: @escaping (Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
let queue = self.queue
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
let index = self.fullRangeRequests.add(disposable)
|
||||
self.updateStatuses()
|
||||
|
||||
disposable.set(self.fetched(range: 0 ..< Int32.max, priority: .default, fetch: fetch, error: { e in
|
||||
error(e)
|
||||
}, completed: { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.fullRangeRequests.remove(index)
|
||||
if strongSelf.fullRangeRequests.isEmpty {
|
||||
strongSelf.updateStatuses()
|
||||
}
|
||||
}
|
||||
completed()
|
||||
}
|
||||
}))
|
||||
|
||||
return ActionDisposable { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.fullRangeRequests.remove(index)
|
||||
disposable.dispose()
|
||||
if strongSelf.fullRangeRequests.isEmpty {
|
||||
strongSelf.updateStatuses()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelFullRangeFetches() {
|
||||
self.fullRangeRequests.copyItems().forEach({ $0.dispose() })
|
||||
self.fullRangeRequests.removeAll()
|
||||
|
||||
self.updateStatuses()
|
||||
}
|
||||
|
||||
private func updateStatuses() {
|
||||
if !self.statusRequests.isEmpty {
|
||||
for (f, size) in self.statusRequests.copyItems() {
|
||||
let status = self.immediateStatus(size: size)
|
||||
f(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rangeStatus(next: @escaping (IndexSet) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
next(self.fileMap.ranges)
|
||||
if let truncationSize = self.fileMap.truncationSize, self.fileMap.contains(0 ..< truncationSize) {
|
||||
completed()
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let index = self.rangeStatusRequests.add((next, completed))
|
||||
|
||||
let queue = self.queue
|
||||
return ActionDisposable { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.rangeStatusRequests.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func immediateStatus(size: Int32?) -> MediaResourceStatus {
|
||||
let status: MediaResourceStatus
|
||||
if self.fullRangeRequests.isEmpty {
|
||||
if let truncationSize = self.fileMap.truncationSize, self.fileMap.sum == truncationSize {
|
||||
status = .Local
|
||||
} else {
|
||||
status = .Remote
|
||||
}
|
||||
} else {
|
||||
let progress: Float
|
||||
if let truncationSize = self.fileMap.truncationSize, truncationSize != 0 {
|
||||
progress = Float(self.fileMap.sum) / Float(truncationSize)
|
||||
} else if let size = size {
|
||||
progress = Float(self.fileMap.sum) / Float(size)
|
||||
} else {
|
||||
progress = self.fileMap.progress ?? 0.0
|
||||
}
|
||||
status = .Fetching(isActive: true, progress: progress)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int32?) -> Disposable {
|
||||
let index = self.statusRequests.add((next, size))
|
||||
|
||||
let value = self.immediateStatus(size: size)
|
||||
next(value)
|
||||
if case .Local = value {
|
||||
completed()
|
||||
return EmptyDisposable
|
||||
} else {
|
||||
let queue = self.queue
|
||||
return ActionDisposable { [weak self] in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.statusRequests.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRequestRanges(_ intervals: [(Range<Int>, MediaBoxFetchPriority)], fetch: ((Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>)?) {
|
||||
assert(self.queue.isCurrent())
|
||||
|
||||
#if DEBUG
|
||||
for interval in intervals {
|
||||
assert(!interval.0.isEmpty)
|
||||
}
|
||||
#endif
|
||||
if intervals.isEmpty {
|
||||
if let (_, disposable) = self.currentFetch {
|
||||
self.currentFetch = nil
|
||||
disposable.dispose()
|
||||
}
|
||||
} else {
|
||||
if let (promise, _) = self.currentFetch {
|
||||
promise.set(.single(intervals))
|
||||
} else if let fetch = fetch {
|
||||
let promise = Promise<[(Range<Int>, MediaBoxFetchPriority)]>()
|
||||
let disposable = MetaDisposable()
|
||||
self.currentFetch = (promise, disposable)
|
||||
disposable.set((fetch(promise.get())
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] data in
|
||||
if let strongSelf = self {
|
||||
switch data {
|
||||
case .reset:
|
||||
if !strongSelf.fileMap.ranges.isEmpty {
|
||||
strongSelf.reset()
|
||||
}
|
||||
case let .resourceSizeUpdated(size):
|
||||
strongSelf.truncate(Int32(size))
|
||||
case let .dataPart(resourceOffset, data, range, complete):
|
||||
if !data.isEmpty {
|
||||
strongSelf.write(offset: Int32(resourceOffset), data: data, dataRange: range)
|
||||
}
|
||||
if complete {
|
||||
if let maxOffset = strongSelf.fileMap.ranges.rangeView.reversed().first?.upperBound {
|
||||
let maxValue = max(resourceOffset + range.count, maxOffset)
|
||||
strongSelf.truncate(Int32(maxValue))
|
||||
}
|
||||
}
|
||||
case let .replaceHeader(data, range):
|
||||
strongSelf.write(offset: 0, data: data, dataRange: range)
|
||||
case let .moveLocalFile(path):
|
||||
strongSelf.moveLocalFile(tempPath: path)
|
||||
case let .moveTempFile(file):
|
||||
strongSelf.moveLocalFile(tempPath: file.path)
|
||||
TempBox.shared.dispose(file)
|
||||
case let .copyLocalItem(item):
|
||||
strongSelf.copyLocalItem(item)
|
||||
case let .progressUpdated(progress):
|
||||
strongSelf.progressUpdated(progress)
|
||||
}
|
||||
if !strongSelf.processedAtLeastOneFetch {
|
||||
strongSelf.processedAtLeastOneFetch = true
|
||||
for request in strongSelf.dataRequests.copyItems() {
|
||||
if request.waitingUntilAfterInitialFetch {
|
||||
request.waitingUntilAfterInitialFetch = false
|
||||
|
||||
if strongSelf.fileMap.contains(request.range) {
|
||||
request.completion(MediaResourceData(path: strongSelf.path, offset: Int(request.range.lowerBound), size: request.range.count, complete: true))
|
||||
} else {
|
||||
request.completion(MediaResourceData(path: strongSelf.path, offset: Int(request.range.lowerBound), size: 0, complete: false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, error: { [weak self] e in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
for (error, _) in strongSelf.missingRanges.clear() {
|
||||
error(e)
|
||||
}
|
||||
}))
|
||||
promise.set(.single(intervals))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class MediaBoxFileMissingRange {
|
||||
var range: Range<Int32>
|
||||
let priority: MediaBoxFetchPriority
|
||||
var remainingRanges: IndexSet
|
||||
let error: (MediaResourceDataFetchError) -> Void
|
||||
let completion: () -> Void
|
||||
|
||||
init(range: Range<Int32>, priority: MediaBoxFetchPriority, error: @escaping (MediaResourceDataFetchError) -> Void, completion: @escaping () -> Void) {
|
||||
self.range = range
|
||||
self.priority = priority
|
||||
let intRange: Range<Int> = Int(range.lowerBound) ..< Int(range.upperBound)
|
||||
self.remainingRanges = IndexSet(integersIn: intRange)
|
||||
self.error = error
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
private final class MediaBoxFileMissingRanges {
|
||||
private var requestedRanges = Bag<MediaBoxFileMissingRange>()
|
||||
|
||||
private var missingRanges = IndexSet()
|
||||
|
||||
func clear() -> [((MediaResourceDataFetchError) -> Void, () -> Void)] {
|
||||
let errorsAndCompletions = self.requestedRanges.copyItems().map({ ($0.error, $0.completion) })
|
||||
self.requestedRanges.removeAll()
|
||||
return errorsAndCompletions
|
||||
}
|
||||
|
||||
func reset(fileMap: MediaBoxFileMap) -> [(Range<Int>, MediaBoxFetchPriority)]? {
|
||||
return self.update(fileMap: fileMap)
|
||||
}
|
||||
|
||||
private func missingRequestedIntervals() -> [(Range<Int>, MediaBoxFetchPriority)] {
|
||||
var resolvedIntervals: [(Range<Int>, MediaBoxFetchPriority)] = []
|
||||
for item in self.requestedRanges.copyItems() {
|
||||
var requestedInterval = IndexSet(integersIn: Int(item.range.lowerBound) ..< Int(item.range.upperBound))
|
||||
requestedInterval.formIntersection(self.missingRanges)
|
||||
for range in requestedInterval.rangeView {
|
||||
if !range.isEmpty {
|
||||
resolvedIntervals.append((range, item.priority))
|
||||
}
|
||||
}
|
||||
}
|
||||
outer: while resolvedIntervals.count > 1 {
|
||||
var hadOverlap = false
|
||||
for i in 0 ..< resolvedIntervals.count {
|
||||
inner: for j in 0 ..< resolvedIntervals.count {
|
||||
if i == j {
|
||||
continue inner
|
||||
}
|
||||
if resolvedIntervals[i].0.overlaps(resolvedIntervals[j].0) {
|
||||
hadOverlap = true
|
||||
if resolvedIntervals[i].0 == resolvedIntervals[j].0 {
|
||||
if resolvedIntervals[i].1.rawValue > resolvedIntervals[j].1.rawValue {
|
||||
resolvedIntervals.remove(at: j)
|
||||
} else {
|
||||
resolvedIntervals.remove(at: i)
|
||||
}
|
||||
continue outer
|
||||
}
|
||||
let lowerBound: Int
|
||||
let lowerPriority: MediaBoxFetchPriority
|
||||
if resolvedIntervals[i].0.lowerBound == resolvedIntervals[j].0.lowerBound {
|
||||
lowerBound = resolvedIntervals[i].0.lowerBound
|
||||
lowerPriority = MediaBoxFetchPriority(rawValue: max(resolvedIntervals[i].1.rawValue, resolvedIntervals[j].1.rawValue))!
|
||||
} else if resolvedIntervals[i].0.lowerBound < resolvedIntervals[j].0.lowerBound {
|
||||
lowerBound = resolvedIntervals[i].0.lowerBound
|
||||
lowerPriority = resolvedIntervals[i].1
|
||||
} else {
|
||||
lowerBound = resolvedIntervals[j].0.lowerBound
|
||||
lowerPriority = resolvedIntervals[j].1
|
||||
}
|
||||
let upperBound: Int
|
||||
let upperPriority: MediaBoxFetchPriority
|
||||
if resolvedIntervals[i].0.upperBound == resolvedIntervals[j].0.upperBound {
|
||||
upperBound = resolvedIntervals[i].0.upperBound
|
||||
upperPriority = MediaBoxFetchPriority(rawValue: max(resolvedIntervals[i].1.rawValue, resolvedIntervals[j].1.rawValue))!
|
||||
} else if resolvedIntervals[i].0.upperBound > resolvedIntervals[j].0.upperBound {
|
||||
upperBound = resolvedIntervals[i].0.upperBound
|
||||
upperPriority = resolvedIntervals[i].1
|
||||
} else {
|
||||
upperBound = resolvedIntervals[j].0.upperBound
|
||||
upperPriority = resolvedIntervals[j].1
|
||||
}
|
||||
let middleBound = max(resolvedIntervals[i].0.lowerBound, resolvedIntervals[j].0.lowerBound)
|
||||
resolvedIntervals[i] = (lowerBound ..< middleBound, lowerPriority)
|
||||
resolvedIntervals[j] = (middleBound ..< upperBound, upperPriority)
|
||||
if resolvedIntervals[i].0.isEmpty || resolvedIntervals[j].0.isEmpty {
|
||||
if j > i {
|
||||
if resolvedIntervals[j].0.isEmpty {
|
||||
resolvedIntervals.remove(at: j)
|
||||
}
|
||||
if resolvedIntervals[i].0.isEmpty {
|
||||
resolvedIntervals.remove(at: i)
|
||||
}
|
||||
} else {
|
||||
if resolvedIntervals[i].0.isEmpty {
|
||||
resolvedIntervals.remove(at: i)
|
||||
}
|
||||
if resolvedIntervals[j].0.isEmpty {
|
||||
resolvedIntervals.remove(at: j)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hadOverlap {
|
||||
break
|
||||
}
|
||||
}
|
||||
return resolvedIntervals
|
||||
}
|
||||
|
||||
func fill(_ range: Range<Int32>) -> ([(Range<Int>, MediaBoxFetchPriority)], [() -> Void])? {
|
||||
let intRange: Range<Int> = Int(range.lowerBound) ..< Int(range.upperBound)
|
||||
if self.missingRanges.intersects(integersIn: intRange) {
|
||||
self.missingRanges.remove(integersIn: intRange)
|
||||
var completions: [() -> Void] = []
|
||||
for (index, item) in self.requestedRanges.copyItemsWithIndices() {
|
||||
if item.range.overlaps(range) {
|
||||
item.remainingRanges.remove(integersIn: intRange)
|
||||
if item.remainingRanges.isEmpty {
|
||||
self.requestedRanges.remove(index)
|
||||
completions.append(item.completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (self.missingRequestedIntervals(), completions)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func addRequest(fileMap: MediaBoxFileMap, range: Range<Int32>, priority: MediaBoxFetchPriority, error: @escaping (MediaResourceDataFetchError) -> Void, completion: @escaping () -> Void) -> (Int, [(Range<Int>, MediaBoxFetchPriority)]?) {
|
||||
let index = self.requestedRanges.add(MediaBoxFileMissingRange(range: range, priority: priority, error: error, completion: completion))
|
||||
|
||||
return (index, self.update(fileMap: fileMap))
|
||||
}
|
||||
|
||||
func removeRequest(fileMap: MediaBoxFileMap, index: Int) -> [(Range<Int>, MediaBoxFetchPriority)]? {
|
||||
self.requestedRanges.remove(index)
|
||||
return self.update(fileMap: fileMap)
|
||||
}
|
||||
|
||||
private func update(fileMap: MediaBoxFileMap) -> [(Range<Int>, MediaBoxFetchPriority)]? {
|
||||
var requested = IndexSet()
|
||||
for item in self.requestedRanges.copyItems() {
|
||||
let intRange: Range<Int> = Int(item.range.lowerBound) ..< Int(item.range.upperBound)
|
||||
requested.insert(integersIn: intRange)
|
||||
}
|
||||
requested.subtract(fileMap.ranges)
|
||||
if requested != self.missingRanges {
|
||||
self.missingRanges = requested
|
||||
return self.missingRequestedIntervals()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private enum MediaBoxFileContent {
|
||||
case complete(String, Int)
|
||||
case partial(MediaBoxPartialFile)
|
||||
}
|
||||
|
||||
final class MediaBoxFileContext {
|
||||
private let queue: Queue
|
||||
private let path: String
|
||||
private let partialPath: String
|
||||
private let metaPath: String
|
||||
|
||||
private var content: MediaBoxFileContent
|
||||
|
||||
private let references = CounterBag()
|
||||
|
||||
var isEmpty: Bool {
|
||||
return self.references.isEmpty
|
||||
}
|
||||
|
||||
init?(queue: Queue, path: String, partialPath: String, metaPath: String) {
|
||||
assert(queue.isCurrent())
|
||||
|
||||
self.queue = queue
|
||||
self.path = path
|
||||
self.partialPath = partialPath
|
||||
self.metaPath = metaPath
|
||||
|
||||
var completeImpl: ((Int32) -> Void)?
|
||||
if let size = fileSize(path) {
|
||||
self.content = .complete(path, size)
|
||||
} else if let file = MediaBoxPartialFile(queue: queue, path: partialPath, metaPath: metaPath, completePath: path, completed: { size in
|
||||
completeImpl?(size)
|
||||
}) {
|
||||
self.content = .partial(file)
|
||||
completeImpl = { [weak self] size in
|
||||
queue.async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.content = .complete(path, Int(size))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func addReference() -> Int {
|
||||
return self.references.add()
|
||||
}
|
||||
|
||||
func removeReference(_ index: Int) {
|
||||
self.references.remove(index)
|
||||
}
|
||||
|
||||
func data(range: Range<Int32>, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable {
|
||||
switch self.content {
|
||||
case let .complete(path, size):
|
||||
next(MediaResourceData(path: path, offset: Int(range.lowerBound), size: min(Int(range.upperBound), size) - Int(range.lowerBound), complete: true))
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.data(range: range, waitUntilAfterInitialFetch: waitUntilAfterInitialFetch, next: next)
|
||||
}
|
||||
}
|
||||
|
||||
func fetched(range: Range<Int32>, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
switch self.content {
|
||||
case .complete:
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.fetched(range: range, priority: priority, fetch: fetch, error: error, completed: completed)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchedFullRange(fetch: @escaping (Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
switch self.content {
|
||||
case .complete:
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.fetchedFullRange(fetch: fetch, error: error, completed: completed)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelFullRangeFetches() {
|
||||
switch self.content {
|
||||
case .complete:
|
||||
break
|
||||
case let .partial(file):
|
||||
file.cancelFullRangeFetches()
|
||||
}
|
||||
}
|
||||
|
||||
func rangeStatus(next: @escaping (IndexSet) -> Void, completed: @escaping () -> Void) -> Disposable {
|
||||
switch self.content {
|
||||
case let .complete(_, size):
|
||||
next(IndexSet(integersIn: 0 ..< size))
|
||||
completed()
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.rangeStatus(next: next, completed: completed)
|
||||
}
|
||||
}
|
||||
|
||||
func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int32?) -> Disposable {
|
||||
switch self.content {
|
||||
case .complete:
|
||||
next(.Local)
|
||||
return EmptyDisposable
|
||||
case let .partial(file):
|
||||
return file.status(next: next, completed: completed, size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
71
submodules/Postbox/Postbox/MediaResource.swift
Normal file
71
submodules/Postbox/Postbox/MediaResource.swift
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import Foundation
|
||||
|
||||
public protocol MediaResourceId {
|
||||
var uniqueId: String { get }
|
||||
var hashValue: Int { get }
|
||||
func isEqual(to: MediaResourceId) -> Bool
|
||||
}
|
||||
|
||||
public struct WrappedMediaResourceId: Hashable {
|
||||
public let id: MediaResourceId
|
||||
|
||||
public init(_ id: MediaResourceId) {
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public static func ==(lhs: WrappedMediaResourceId, rhs: WrappedMediaResourceId) -> Bool {
|
||||
return lhs.id.isEqual(to: rhs.id)
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.id.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
public func anyHashableFromMediaResourceId(_ id: MediaResourceId) -> AnyHashable {
|
||||
return AnyHashable(WrappedMediaResourceId(id))
|
||||
}
|
||||
|
||||
public protocol MediaResource {
|
||||
var id: MediaResourceId { get }
|
||||
var size: Int? { get }
|
||||
var streamable: Bool { get }
|
||||
var headerSize: Int32 { get }
|
||||
|
||||
func isEqual(to: MediaResource) -> Bool
|
||||
}
|
||||
|
||||
public extension MediaResource {
|
||||
var size: Int? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var streamable: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var headerSize: Int32 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
public protocol CachedMediaResourceRepresentation {
|
||||
var uniqueId: String { get }
|
||||
func isEqual(to: CachedMediaResourceRepresentation) -> Bool
|
||||
}
|
||||
|
||||
public protocol MediaResourceFetchTag {
|
||||
}
|
||||
|
||||
public protocol MediaResourceFetchInfo {
|
||||
}
|
||||
|
||||
public struct MediaResourceFetchParameters {
|
||||
public let tag: MediaResourceFetchTag?
|
||||
public let info: MediaResourceFetchInfo?
|
||||
|
||||
public init(tag: MediaResourceFetchTag?, info: MediaResourceFetchInfo?) {
|
||||
self.tag = tag
|
||||
self.info = info
|
||||
}
|
||||
}
|
||||
38
submodules/Postbox/Postbox/MediaResourceStatus.swift
Normal file
38
submodules/Postbox/Postbox/MediaResourceStatus.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import Foundation
|
||||
#if os(macOS)
|
||||
import SwiftSignalKitMac
|
||||
#else
|
||||
import SwiftSignalKit
|
||||
#endif
|
||||
|
||||
public enum MediaResourceStatus: Equatable {
|
||||
case Remote
|
||||
case Local
|
||||
case Fetching(isActive: Bool, progress: Float)
|
||||
}
|
||||
|
||||
public func ==(lhs: MediaResourceStatus, rhs: MediaResourceStatus) -> Bool {
|
||||
switch lhs {
|
||||
case .Remote:
|
||||
switch rhs {
|
||||
case .Remote:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .Local:
|
||||
switch rhs {
|
||||
case .Local:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .Fetching(lhsIsActive, lhsProgress):
|
||||
switch rhs {
|
||||
case let .Fetching(rhsIsActive, rhsProgress):
|
||||
return lhsIsActive == rhsIsActive && lhsProgress.isEqual(to: rhsProgress)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
728
submodules/Postbox/Postbox/Message.swift
Normal file
728
submodules/Postbox/Postbox/Message.swift
Normal file
|
|
@ -0,0 +1,728 @@
|
|||
import Foundation
|
||||
|
||||
public struct MessageId: Hashable, Comparable, CustomStringConvertible {
|
||||
public typealias Namespace = Int32
|
||||
public typealias Id = Int32
|
||||
|
||||
public let peerId: PeerId
|
||||
public let namespace: Namespace
|
||||
public let id: Id
|
||||
|
||||
public var description: String {
|
||||
get {
|
||||
return "\(namespace):\(id)"
|
||||
}
|
||||
}
|
||||
|
||||
public init(peerId: PeerId, namespace: Namespace, id: Id) {
|
||||
self.peerId = peerId
|
||||
self.namespace = namespace
|
||||
self.id = id
|
||||
}
|
||||
|
||||
public init(_ buffer: ReadBuffer) {
|
||||
var peerIdNamespaceValue: Int32 = 0
|
||||
memcpy(&peerIdNamespaceValue, buffer.memory + buffer.offset, 4)
|
||||
var peerIdIdValue: Int32 = 0
|
||||
memcpy(&peerIdIdValue, buffer.memory + (buffer.offset + 4), 4)
|
||||
self.peerId = PeerId(namespace: peerIdNamespaceValue, id: peerIdIdValue)
|
||||
|
||||
var namespaceValue: Int32 = 0
|
||||
memcpy(&namespaceValue, buffer.memory + (buffer.offset + 8), 4)
|
||||
self.namespace = namespaceValue
|
||||
var idValue: Int32 = 0
|
||||
memcpy(&idValue, buffer.memory + (buffer.offset + 12), 4)
|
||||
self.id = idValue
|
||||
|
||||
buffer.offset += 16
|
||||
}
|
||||
|
||||
public func encodeToBuffer(_ buffer: WriteBuffer) {
|
||||
var peerIdNamespace = self.peerId.namespace
|
||||
var peerIdId = self.peerId.id
|
||||
var namespace = self.namespace
|
||||
var id = self.id
|
||||
buffer.write(&peerIdNamespace, offset: 0, length: 4);
|
||||
buffer.write(&peerIdId, offset: 0, length: 4);
|
||||
buffer.write(&namespace, offset: 0, length: 4);
|
||||
buffer.write(&id, offset: 0, length: 4);
|
||||
}
|
||||
|
||||
public static func encodeArrayToBuffer(_ array: [MessageId], buffer: WriteBuffer) {
|
||||
var length: Int32 = Int32(array.count)
|
||||
buffer.write(&length, offset: 0, length: 4)
|
||||
for id in array {
|
||||
id.encodeToBuffer(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
public static func decodeArrayFromBuffer(_ buffer: ReadBuffer) -> [MessageId] {
|
||||
var length: Int32 = 0
|
||||
memcpy(&length, buffer.memory, 4)
|
||||
buffer.offset += 4
|
||||
var i = 0
|
||||
var array: [MessageId] = []
|
||||
while i < Int(length) {
|
||||
array.append(MessageId(buffer))
|
||||
i += 1
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
public static func <(lhs: MessageId, rhs: MessageId) -> Bool {
|
||||
if lhs.namespace == rhs.namespace {
|
||||
if lhs.id == rhs.id {
|
||||
return lhs.peerId < rhs.peerId
|
||||
} else {
|
||||
return lhs.id < rhs.id
|
||||
}
|
||||
} else {
|
||||
return lhs.namespace < rhs.namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageIndex: Comparable, Hashable {
|
||||
public let id: MessageId
|
||||
public let timestamp: Int32
|
||||
|
||||
public init(id: MessageId, timestamp: Int32) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
public func predecessor() -> MessageIndex {
|
||||
if self.id.id != 0 {
|
||||
return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace, id: self.id.id - 1), timestamp: self.timestamp)
|
||||
} else if self.id.namespace != 0 {
|
||||
return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace - 1, id: Int32.max - 1), timestamp: self.timestamp)
|
||||
} else if self.timestamp != 0 {
|
||||
return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: Int32(Int8.max) - 1, id: Int32.max - 1), timestamp: self.timestamp - 1)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public func successor() -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: self.id.namespace, id: self.id.id == Int32.max ? self.id.id : (self.id.id + 1)), timestamp: self.timestamp)
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.id.hashValue
|
||||
}
|
||||
|
||||
public static func absoluteUpperBound() -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: PeerId(namespace: Int32(Int8.max), id: Int32.max), namespace: Int32(Int8.max), id: Int32.max), timestamp: Int32.max)
|
||||
}
|
||||
|
||||
public static func absoluteLowerBound() -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: 0), timestamp: 0)
|
||||
}
|
||||
|
||||
public static func lowerBound(peerId: PeerId) -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: 0)
|
||||
}
|
||||
|
||||
public static func lowerBound(peerId: PeerId, namespace: MessageId.Namespace) -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: 0), timestamp: 0)
|
||||
}
|
||||
|
||||
public static func upperBound(peerId: PeerId) -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: peerId, namespace: Int32(Int8.max), id: Int32.max), timestamp: Int32.max)
|
||||
}
|
||||
|
||||
public static func upperBound(peerId: PeerId, namespace: MessageId.Namespace) -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: Int32.max), timestamp: Int32.max)
|
||||
}
|
||||
|
||||
public static func upperBound(peerId: PeerId, timestamp: Int32, namespace: MessageId.Namespace) -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: Int32.max), timestamp: timestamp)
|
||||
}
|
||||
|
||||
func withPeerId(_ peerId: PeerId) -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: peerId, namespace: self.id.namespace, id: self.id.id), timestamp: self.timestamp)
|
||||
}
|
||||
|
||||
func withNamespace(_ namespace: MessageId.Namespace) -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: self.id.peerId, namespace: namespace, id: self.id.id), timestamp: self.timestamp)
|
||||
}
|
||||
|
||||
public static func <(lhs: MessageIndex, rhs: MessageIndex) -> Bool {
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return lhs.timestamp < rhs.timestamp
|
||||
}
|
||||
|
||||
if lhs.id.namespace != rhs.id.namespace {
|
||||
return lhs.id.namespace < rhs.id.namespace
|
||||
}
|
||||
|
||||
return lhs.id.id < rhs.id.id
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListIndex: Comparable, Hashable {
|
||||
public let pinningIndex: UInt16?
|
||||
public let messageIndex: MessageIndex
|
||||
|
||||
public init(pinningIndex: UInt16?, messageIndex: MessageIndex) {
|
||||
self.pinningIndex = pinningIndex
|
||||
self.messageIndex = messageIndex
|
||||
}
|
||||
|
||||
public static func <(lhs: ChatListIndex, rhs: ChatListIndex) -> Bool {
|
||||
if let lhsPinningIndex = lhs.pinningIndex, let rhsPinningIndex = rhs.pinningIndex {
|
||||
if lhsPinningIndex > rhsPinningIndex {
|
||||
return true
|
||||
} else if lhsPinningIndex < rhsPinningIndex {
|
||||
return false
|
||||
}
|
||||
} else if lhs.pinningIndex != nil {
|
||||
return false
|
||||
} else if rhs.pinningIndex != nil {
|
||||
return true
|
||||
}
|
||||
return lhs.messageIndex < rhs.messageIndex
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.messageIndex.hashValue
|
||||
}
|
||||
|
||||
public static var absoluteUpperBound: ChatListIndex {
|
||||
return ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex.absoluteUpperBound())
|
||||
}
|
||||
|
||||
public static var absoluteLowerBound: ChatListIndex {
|
||||
return ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex.absoluteLowerBound())
|
||||
}
|
||||
|
||||
public var predecessor: ChatListIndex {
|
||||
return ChatListIndex(pinningIndex: self.pinningIndex, messageIndex: self.messageIndex.predecessor())
|
||||
}
|
||||
|
||||
public var successor: ChatListIndex {
|
||||
return ChatListIndex(pinningIndex: self.pinningIndex, messageIndex: self.messageIndex.successor())
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageTags: OptionSet, Sequence, Hashable {
|
||||
public var rawValue: UInt32
|
||||
|
||||
public init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
public static let All = MessageTags(rawValue: 0xffffffff)
|
||||
|
||||
public var containsSingleElement: Bool {
|
||||
var hasOne = false
|
||||
for i in 0 ..< 31 {
|
||||
let tag = (self.rawValue >> UInt32(i)) & 1
|
||||
if tag != 0 {
|
||||
if hasOne {
|
||||
return false
|
||||
} else {
|
||||
hasOne = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasOne
|
||||
}
|
||||
|
||||
public func makeIterator() -> AnyIterator<MessageTags> {
|
||||
var index = 0
|
||||
return AnyIterator { () -> MessageTags? in
|
||||
while index < 31 {
|
||||
let currentTags = self.rawValue >> UInt32(index)
|
||||
let tag = MessageTags(rawValue: 1 << UInt32(index))
|
||||
index += 1
|
||||
if currentTags == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if (currentTags & 1) != 0 {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct GlobalMessageTags: OptionSet, Sequence, Hashable {
|
||||
public var rawValue: UInt32
|
||||
|
||||
public init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
var isSingleTag: Bool {
|
||||
let t = Int32(bitPattern: self.rawValue)
|
||||
return t != 0 && t == (t & (-t))
|
||||
}
|
||||
|
||||
public func makeIterator() -> AnyIterator<GlobalMessageTags> {
|
||||
var index = 0
|
||||
return AnyIterator { () -> GlobalMessageTags? in
|
||||
while index < 31 {
|
||||
let currentTags = self.rawValue >> UInt32(index)
|
||||
let tag = GlobalMessageTags(rawValue: 1 << UInt32(index))
|
||||
index += 1
|
||||
if currentTags == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if (currentTags & 1) != 0 {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.rawValue.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
public struct LocalMessageTags: OptionSet, Sequence, Hashable {
|
||||
public var rawValue: UInt32
|
||||
|
||||
public init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
var isSingleTag: Bool {
|
||||
let t = Int32(bitPattern: self.rawValue)
|
||||
return t != 0 && t == (t & (-t))
|
||||
}
|
||||
|
||||
public func makeIterator() -> AnyIterator<LocalMessageTags> {
|
||||
var index = 0
|
||||
return AnyIterator { () -> LocalMessageTags? in
|
||||
while index < 31 {
|
||||
let currentTags = self.rawValue >> UInt32(index)
|
||||
let tag = LocalMessageTags(rawValue: 1 << UInt32(index))
|
||||
index += 1
|
||||
if currentTags == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if (currentTags & 1) != 0 {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.rawValue.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageFlags: OptionSet {
|
||||
public var rawValue: UInt32
|
||||
|
||||
public init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
public init(_ flags: StoreMessageFlags) {
|
||||
var rawValue: UInt32 = 0
|
||||
|
||||
if flags.contains(StoreMessageFlags.Unsent) {
|
||||
rawValue |= MessageFlags.Unsent.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(StoreMessageFlags.Failed) {
|
||||
rawValue |= MessageFlags.Failed.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(StoreMessageFlags.Incoming) {
|
||||
rawValue |= MessageFlags.Incoming.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(StoreMessageFlags.TopIndexable) {
|
||||
rawValue |= MessageFlags.TopIndexable.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(StoreMessageFlags.Sending) {
|
||||
rawValue |= MessageFlags.Sending.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(StoreMessageFlags.CanBeGroupedIntoFeed) {
|
||||
rawValue |= MessageFlags.CanBeGroupedIntoFeed.rawValue
|
||||
}
|
||||
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let Unsent = MessageFlags(rawValue: 1)
|
||||
public static let Failed = MessageFlags(rawValue: 2)
|
||||
public static let Incoming = MessageFlags(rawValue: 4)
|
||||
public static let TopIndexable = MessageFlags(rawValue: 16)
|
||||
public static let Sending = MessageFlags(rawValue: 32)
|
||||
public static let CanBeGroupedIntoFeed = MessageFlags(rawValue: 64)
|
||||
|
||||
}
|
||||
|
||||
public struct StoreMessageForwardInfo {
|
||||
public let authorId: PeerId?
|
||||
public let sourceId: PeerId?
|
||||
public let sourceMessageId: MessageId?
|
||||
public let date: Int32
|
||||
public let authorSignature: String?
|
||||
|
||||
public init(authorId: PeerId?, sourceId: PeerId?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?) {
|
||||
self.authorId = authorId
|
||||
self.sourceId = sourceId
|
||||
self.sourceMessageId = sourceMessageId
|
||||
self.date = date
|
||||
self.authorSignature = authorSignature
|
||||
}
|
||||
|
||||
public init(_ info: MessageForwardInfo) {
|
||||
self.init(authorId: info.author?.id, sourceId: info.source?.id, sourceMessageId: info.sourceMessageId, date: info.date, authorSignature: info.authorSignature)
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageForwardInfo: Equatable {
|
||||
public let author: Peer?
|
||||
public let source: Peer?
|
||||
public let sourceMessageId: MessageId?
|
||||
public let date: Int32
|
||||
public let authorSignature: String?
|
||||
|
||||
public init(author: Peer?, source: Peer?, sourceMessageId: MessageId?, date: Int32, authorSignature: String?) {
|
||||
self.author = author
|
||||
self.source = source
|
||||
self.sourceMessageId = sourceMessageId
|
||||
self.date = date
|
||||
self.authorSignature = authorSignature
|
||||
}
|
||||
|
||||
public static func ==(lhs: MessageForwardInfo, rhs: MessageForwardInfo) -> Bool {
|
||||
if !arePeersEqual(lhs.author, rhs.author) {
|
||||
return false
|
||||
}
|
||||
if let lhsSource = lhs.source, let rhsSource = rhs.source {
|
||||
if !lhsSource.isEqual(rhsSource) {
|
||||
return false
|
||||
}
|
||||
} else if (lhs.source == nil) != (rhs.source == nil) {
|
||||
return false
|
||||
}
|
||||
if lhs.sourceMessageId != rhs.sourceMessageId {
|
||||
return false
|
||||
}
|
||||
if lhs.date != rhs.date {
|
||||
return false
|
||||
}
|
||||
if lhs.authorSignature != rhs.authorSignature {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public protocol MessageAttribute: class, PostboxCoding {
|
||||
var associatedPeerIds: [PeerId] { get }
|
||||
var associatedMessageIds: [MessageId] { get }
|
||||
}
|
||||
|
||||
public extension MessageAttribute {
|
||||
var associatedPeerIds: [PeerId] {
|
||||
return []
|
||||
}
|
||||
|
||||
var associatedMessageIds: [MessageId] {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageGroupInfo: Equatable {
|
||||
public let stableId: UInt32
|
||||
}
|
||||
|
||||
public final class Message {
|
||||
public let stableId: UInt32
|
||||
public let stableVersion: UInt32
|
||||
|
||||
public let id: MessageId
|
||||
public let globallyUniqueId: Int64?
|
||||
public let groupingKey: Int64?
|
||||
public let groupInfo: MessageGroupInfo?
|
||||
public let timestamp: Int32
|
||||
public let flags: MessageFlags
|
||||
public let tags: MessageTags
|
||||
public let globalTags: GlobalMessageTags
|
||||
public let localTags: LocalMessageTags
|
||||
public let forwardInfo: MessageForwardInfo?
|
||||
public let author: Peer?
|
||||
public let text: String
|
||||
public let attributes: [MessageAttribute]
|
||||
public let media: [Media]
|
||||
public let peers: SimpleDictionary<PeerId, Peer>
|
||||
public let associatedMessages: SimpleDictionary<MessageId, Message>
|
||||
public let associatedMessageIds: [MessageId]
|
||||
|
||||
public var index: MessageIndex {
|
||||
return MessageIndex(id: self.id, timestamp: self.timestamp)
|
||||
}
|
||||
|
||||
public init(stableId: UInt32, stableVersion: UInt32, id: MessageId, globallyUniqueId: Int64?, groupingKey: Int64?, groupInfo: MessageGroupInfo?, timestamp: Int32, flags: MessageFlags, tags: MessageTags, globalTags: GlobalMessageTags, localTags: LocalMessageTags, forwardInfo: MessageForwardInfo?, author: Peer?, text: String, attributes: [MessageAttribute], media: [Media], peers: SimpleDictionary<PeerId, Peer>, associatedMessages: SimpleDictionary<MessageId, Message>, associatedMessageIds: [MessageId]) {
|
||||
self.stableId = stableId
|
||||
self.stableVersion = stableVersion
|
||||
self.id = id
|
||||
self.globallyUniqueId = globallyUniqueId
|
||||
self.groupingKey = groupingKey
|
||||
self.groupInfo = groupInfo
|
||||
self.timestamp = timestamp
|
||||
self.flags = flags
|
||||
self.tags = tags
|
||||
self.globalTags = globalTags
|
||||
self.localTags = localTags
|
||||
self.forwardInfo = forwardInfo
|
||||
self.author = author
|
||||
self.text = text
|
||||
self.attributes = attributes
|
||||
self.media = media
|
||||
self.peers = peers
|
||||
self.associatedMessages = associatedMessages
|
||||
self.associatedMessageIds = associatedMessageIds
|
||||
}
|
||||
|
||||
public func withUpdatedMedia(_ media: [Media]) -> Message {
|
||||
return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds)
|
||||
}
|
||||
|
||||
public func withUpdatedFlags(_ flags: MessageFlags) -> Message {
|
||||
return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds)
|
||||
}
|
||||
|
||||
func withUpdatedGroupInfo(_ groupInfo: MessageGroupInfo?) -> Message {
|
||||
return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds)
|
||||
}
|
||||
|
||||
func withUpdatedAssociatedMessages(_ associatedMessages: SimpleDictionary<MessageId, Message>) -> Message {
|
||||
return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: associatedMessages, associatedMessageIds: self.associatedMessageIds)
|
||||
}
|
||||
}
|
||||
|
||||
public struct StoreMessageFlags: OptionSet {
|
||||
public var rawValue: UInt32
|
||||
|
||||
public init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
public init(_ flags: MessageFlags) {
|
||||
var rawValue: UInt32 = 0
|
||||
|
||||
if flags.contains(.Unsent) {
|
||||
rawValue |= StoreMessageFlags.Unsent.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(.Failed) {
|
||||
rawValue |= StoreMessageFlags.Failed.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(.Incoming) {
|
||||
rawValue |= StoreMessageFlags.Incoming.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(.TopIndexable) {
|
||||
rawValue |= StoreMessageFlags.TopIndexable.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(.Sending) {
|
||||
rawValue |= StoreMessageFlags.Sending.rawValue
|
||||
}
|
||||
|
||||
if flags.contains(.CanBeGroupedIntoFeed) {
|
||||
rawValue |= StoreMessageFlags.CanBeGroupedIntoFeed.rawValue
|
||||
}
|
||||
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let Unsent = StoreMessageFlags(rawValue: 1)
|
||||
public static let Failed = StoreMessageFlags(rawValue: 2)
|
||||
public static let Incoming = StoreMessageFlags(rawValue: 4)
|
||||
public static let TopIndexable = StoreMessageFlags(rawValue: 16)
|
||||
public static let Sending = StoreMessageFlags(rawValue: 32)
|
||||
public static let CanBeGroupedIntoFeed = StoreMessageFlags(rawValue: 64)
|
||||
}
|
||||
|
||||
public enum StoreMessageId {
|
||||
case Id(MessageId)
|
||||
case Partial(PeerId, MessageId.Namespace)
|
||||
|
||||
public var peerId: PeerId {
|
||||
switch self {
|
||||
case let .Id(id):
|
||||
return id.peerId
|
||||
case let .Partial(peerId, _):
|
||||
return peerId
|
||||
}
|
||||
}
|
||||
|
||||
public var namespace: MessageId.Namespace {
|
||||
switch self {
|
||||
case let .Id(id):
|
||||
return id.namespace
|
||||
case let .Partial(_, namespace):
|
||||
return namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class StoreMessage {
|
||||
public let id: StoreMessageId
|
||||
public let timestamp: Int32
|
||||
public let globallyUniqueId: Int64?
|
||||
public let groupingKey: Int64?
|
||||
public let flags: StoreMessageFlags
|
||||
public let tags: MessageTags
|
||||
public let globalTags: GlobalMessageTags
|
||||
public let localTags: LocalMessageTags
|
||||
public let forwardInfo: StoreMessageForwardInfo?
|
||||
public let authorId: PeerId?
|
||||
public let text: String
|
||||
public let attributes: [MessageAttribute]
|
||||
public let media: [Media]
|
||||
|
||||
public init(id: MessageId, globallyUniqueId: Int64?, groupingKey: Int64?, timestamp: Int32, flags: StoreMessageFlags, tags: MessageTags, globalTags: GlobalMessageTags, localTags: LocalMessageTags, forwardInfo: StoreMessageForwardInfo?, authorId: PeerId?, text: String, attributes: [MessageAttribute], media: [Media]) {
|
||||
self.id = .Id(id)
|
||||
self.globallyUniqueId = globallyUniqueId
|
||||
self.groupingKey = groupingKey
|
||||
self.timestamp = timestamp
|
||||
self.flags = flags
|
||||
self.tags = tags
|
||||
self.globalTags = globalTags
|
||||
self.localTags = localTags
|
||||
self.forwardInfo = forwardInfo
|
||||
self.authorId = authorId
|
||||
self.text = text
|
||||
self.attributes = attributes
|
||||
self.media = media
|
||||
}
|
||||
|
||||
public init(peerId: PeerId, namespace: MessageId.Namespace, globallyUniqueId: Int64?, groupingKey: Int64?, timestamp: Int32, flags: StoreMessageFlags, tags: MessageTags, globalTags: GlobalMessageTags, localTags: LocalMessageTags, forwardInfo: StoreMessageForwardInfo?, authorId: PeerId?, text: String, attributes: [MessageAttribute], media: [Media]) {
|
||||
self.id = .Partial(peerId, namespace)
|
||||
self.timestamp = timestamp
|
||||
self.globallyUniqueId = globallyUniqueId
|
||||
self.groupingKey = groupingKey
|
||||
self.flags = flags
|
||||
self.tags = tags
|
||||
self.globalTags = globalTags
|
||||
self.localTags = localTags
|
||||
self.forwardInfo = forwardInfo
|
||||
self.authorId = authorId
|
||||
self.text = text
|
||||
self.attributes = attributes
|
||||
self.media = media
|
||||
}
|
||||
|
||||
public init(id: StoreMessageId, globallyUniqueId: Int64?, groupingKey: Int64?, timestamp: Int32, flags: StoreMessageFlags, tags: MessageTags, globalTags: GlobalMessageTags, localTags: LocalMessageTags, forwardInfo: StoreMessageForwardInfo?, authorId: PeerId?, text: String, attributes: [MessageAttribute], media: [Media]) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.globallyUniqueId = globallyUniqueId
|
||||
self.groupingKey = groupingKey
|
||||
self.flags = flags
|
||||
self.tags = tags
|
||||
self.globalTags = globalTags
|
||||
self.localTags = localTags
|
||||
self.forwardInfo = forwardInfo
|
||||
self.authorId = authorId
|
||||
self.text = text
|
||||
self.attributes = attributes
|
||||
self.media = media
|
||||
}
|
||||
|
||||
public var index: MessageIndex? {
|
||||
if case let .Id(id) = self.id {
|
||||
return MessageIndex(id: id, timestamp: self.timestamp)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func withUpdatedFlags(_ flags: StoreMessageFlags) -> StoreMessage {
|
||||
if flags == self.flags {
|
||||
return self
|
||||
} else {
|
||||
return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, timestamp: self.timestamp, flags: flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: attributes, media: self.media)
|
||||
}
|
||||
}
|
||||
|
||||
public func withUpdatedAttributes(_ attributes: [MessageAttribute]) -> StoreMessage {
|
||||
return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: attributes, media: self.media)
|
||||
}
|
||||
|
||||
public func withUpdatedLocalTags(_ localTags: LocalMessageTags) -> StoreMessage {
|
||||
if localTags == self.localTags {
|
||||
return self
|
||||
} else {
|
||||
return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: self.attributes, media: self.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class InternalStoreMessage {
|
||||
let id: MessageId
|
||||
let timestamp: Int32
|
||||
let globallyUniqueId: Int64?
|
||||
let groupingKey: Int64?
|
||||
let flags: StoreMessageFlags
|
||||
let tags: MessageTags
|
||||
let globalTags: GlobalMessageTags
|
||||
let localTags: LocalMessageTags
|
||||
let forwardInfo: StoreMessageForwardInfo?
|
||||
let authorId: PeerId?
|
||||
let text: String
|
||||
let attributes: [MessageAttribute]
|
||||
let media: [Media]
|
||||
|
||||
var index: MessageIndex {
|
||||
return MessageIndex(id: self.id, timestamp: self.timestamp)
|
||||
}
|
||||
|
||||
init(id: MessageId, timestamp: Int32, globallyUniqueId: Int64?, groupingKey: Int64?, flags: StoreMessageFlags, tags: MessageTags, globalTags: GlobalMessageTags, localTags: LocalMessageTags, forwardInfo: StoreMessageForwardInfo?, authorId: PeerId?, text: String, attributes: [MessageAttribute], media: [Media]) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.globallyUniqueId = globallyUniqueId
|
||||
self.groupingKey = groupingKey
|
||||
self.flags = flags
|
||||
self.tags = tags
|
||||
self.globalTags = globalTags
|
||||
self.localTags = localTags
|
||||
self.forwardInfo = forwardInfo
|
||||
self.authorId = authorId
|
||||
self.text = text
|
||||
self.attributes = attributes
|
||||
self.media = media
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import Foundation
|
||||
|
||||
final class MessageGloballyUniqueIdTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8 + 8)
|
||||
private let sharedBuffer = WriteBuffer()
|
||||
|
||||
private func key(peerId: PeerId, id: Int64) -> ValueBoxKey {
|
||||
self.sharedKey.setInt64(0, value: peerId.toInt64())
|
||||
self.sharedKey.setInt64(8, value: id)
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
func set(peerId: PeerId, globallyUniqueId: Int64, id: MessageId) {
|
||||
self.sharedBuffer.reset()
|
||||
var idPeerId: Int64 = id.peerId.toInt64()
|
||||
var idNamespace: Int32 = id.namespace
|
||||
var idId: Int32 = id.id
|
||||
self.sharedBuffer.write(&idPeerId, offset: 0, length: 8)
|
||||
self.sharedBuffer.write(&idNamespace, offset: 0, length: 4)
|
||||
self.sharedBuffer.write(&idId, offset: 0, length: 4)
|
||||
self.valueBox.set(self.table, key: self.key(peerId: peerId, id: globallyUniqueId), value: self.sharedBuffer)
|
||||
}
|
||||
|
||||
func get(peerId: PeerId, globallyUniqueId: Int64) -> MessageId? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(peerId: peerId, id: globallyUniqueId)) {
|
||||
var idPeerId: Int64 = 0
|
||||
var idNamespace: Int32 = 0
|
||||
var idId: Int32 = 0
|
||||
value.read(&idPeerId, offset: 0, length: 8)
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
return MessageId(peerId: PeerId(idPeerId), namespace: idNamespace, id: idId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func remove(peerId: PeerId, globallyUniqueId: Int64) {
|
||||
self.valueBox.remove(self.table, key: self.key(peerId: peerId, id: globallyUniqueId), secure: false)
|
||||
}
|
||||
}
|
||||
51
submodules/Postbox/Postbox/MessageHistoryAnchorIndex.swift
Normal file
51
submodules/Postbox/Postbox/MessageHistoryAnchorIndex.swift
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Foundation
|
||||
|
||||
public enum MessageHistoryAnchorIndex: Comparable {
|
||||
case message(MessageIndex)
|
||||
case lowerBound
|
||||
case upperBound
|
||||
|
||||
public static func <(lhs: MessageHistoryAnchorIndex, rhs: MessageHistoryAnchorIndex) -> Bool {
|
||||
switch lhs {
|
||||
case let .message(lhsIndex):
|
||||
switch rhs {
|
||||
case let .message(rhsIndex):
|
||||
return lhsIndex < rhsIndex
|
||||
case .lowerBound:
|
||||
return false
|
||||
case .upperBound:
|
||||
return true
|
||||
}
|
||||
case .lowerBound:
|
||||
if case .lowerBound = rhs {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
case .upperBound:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func isLess(than: MessageIndex) -> Bool {
|
||||
switch self {
|
||||
case .lowerBound:
|
||||
return true
|
||||
case .upperBound:
|
||||
return false
|
||||
case let .message(index):
|
||||
return index < than
|
||||
}
|
||||
}
|
||||
|
||||
public func isLessOrEqual(to: MessageIndex) -> Bool {
|
||||
switch self {
|
||||
case .lowerBound:
|
||||
return true
|
||||
case .upperBound:
|
||||
return false
|
||||
case let .message(index):
|
||||
return index <= to
|
||||
}
|
||||
}
|
||||
}
|
||||
429
submodules/Postbox/Postbox/MessageHistoryHoleIndexTable.swift
Normal file
429
submodules/Postbox/Postbox/MessageHistoryHoleIndexTable.swift
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
import Foundation
|
||||
|
||||
struct MessageHistoryIndexHoleOperationKey: Hashable {
|
||||
let peerId: PeerId
|
||||
let namespace: MessageId.Namespace
|
||||
let space: MessageHistoryHoleSpace
|
||||
}
|
||||
|
||||
enum MessageHistoryIndexHoleOperation {
|
||||
case insert(ClosedRange<MessageId.Id>)
|
||||
case remove(ClosedRange<MessageId.Id>)
|
||||
}
|
||||
|
||||
public enum MessageHistoryHoleSpace: Equatable, Hashable {
|
||||
case everywhere
|
||||
case tag(MessageTags)
|
||||
}
|
||||
|
||||
private func addOperation(_ operation: MessageHistoryIndexHoleOperation, peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, to operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
|
||||
let key = MessageHistoryIndexHoleOperationKey(peerId: peerId, namespace: namespace, space: space)
|
||||
if operations[key] == nil {
|
||||
operations[key] = []
|
||||
}
|
||||
operations[key]!.append(operation)
|
||||
}
|
||||
|
||||
private func decomposeKey(_ key: ValueBoxKey) -> (id: MessageId, space: MessageHistoryHoleSpace) {
|
||||
let tag = MessageTags(rawValue: key.getUInt32(8 + 4))
|
||||
let space: MessageHistoryHoleSpace
|
||||
if tag.rawValue == 0 {
|
||||
space = .everywhere
|
||||
} else {
|
||||
space = .tag(tag)
|
||||
}
|
||||
return (MessageId(peerId: PeerId(key.getInt64(0)), namespace: key.getInt32(8), id: key.getInt32(8 + 4 + 4)), space)
|
||||
}
|
||||
|
||||
private func decodeValue(value: ReadBuffer, peerId: PeerId, namespace: MessageId.Namespace) -> MessageId {
|
||||
var id: Int32 = 0
|
||||
value.read(&id, offset: 0, length: 4)
|
||||
return MessageId(peerId: peerId, namespace: namespace, id: id)
|
||||
}
|
||||
|
||||
final class MessageHistoryHoleIndexTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
let metadataTable: MessageHistoryMetadataTable
|
||||
let seedConfiguration: SeedConfiguration
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, metadataTable: MessageHistoryMetadataTable, seedConfiguration: SeedConfiguration) {
|
||||
self.seedConfiguration = seedConfiguration
|
||||
self.metadataTable = metadataTable
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(id: MessageId, space: MessageHistoryHoleSpace) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8 + 4 + 4 + 4)
|
||||
key.setInt64(0, value: id.peerId.toInt64())
|
||||
key.setInt32(8, value: id.namespace)
|
||||
let tagValue: UInt32
|
||||
switch space {
|
||||
case .everywhere:
|
||||
tagValue = 0
|
||||
case let .tag(tag):
|
||||
tagValue = tag.rawValue
|
||||
}
|
||||
key.setUInt32(8 + 4, value: tagValue)
|
||||
key.setInt32(8 + 4 + 4, value: id.id)
|
||||
return key
|
||||
}
|
||||
|
||||
private func lowerBound(peerId: PeerId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8)
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(peerId: PeerId) -> ValueBoxKey {
|
||||
return self.lowerBound(peerId: peerId).successor
|
||||
}
|
||||
|
||||
private func lowerBound(peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8 + 4 + 4)
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
key.setInt32(8, value: namespace)
|
||||
let tagValue: UInt32
|
||||
switch space {
|
||||
case .everywhere:
|
||||
tagValue = 0
|
||||
case let .tag(tag):
|
||||
tagValue = tag.rawValue
|
||||
}
|
||||
key.setUInt32(8 + 4, value: tagValue)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8 + 4 + 4)
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
key.setInt32(8, value: namespace)
|
||||
let tagValue: UInt32
|
||||
switch space {
|
||||
case .everywhere:
|
||||
tagValue = 0
|
||||
case let .tag(tag):
|
||||
tagValue = tag.rawValue
|
||||
}
|
||||
key.setUInt32(8 + 4, value: tagValue)
|
||||
return key.successor
|
||||
}
|
||||
|
||||
private func namespaceLowerBound(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
|
||||
}
|
||||
|
||||
private func namespaceUpperBound(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
|
||||
}
|
||||
|
||||
private func ensureInitialized(peerId: PeerId) {
|
||||
if !self.metadataTable.isInitialized(peerId) {
|
||||
self.metadataTable.setInitialized(peerId)
|
||||
if let tagsByNamespace = self.seedConfiguration.messageHoles[peerId.namespace] {
|
||||
for (namespace, _) in tagsByNamespace {
|
||||
var operations: [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]] = [:]
|
||||
self.add(peerId: peerId, namespace: namespace, space: .everywhere, range: 1 ... (Int32.max - 1), operations: &operations)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func existingNamespaces(peerId: PeerId, holeSpace: MessageHistoryHoleSpace) -> Set<MessageId.Namespace> {
|
||||
self.ensureInitialized(peerId: peerId)
|
||||
|
||||
var result = Set<MessageId.Namespace>()
|
||||
var currentLowerBound = self.lowerBound(peerId: peerId)
|
||||
let upperBound = self.upperBound(peerId: peerId)
|
||||
while true {
|
||||
var idAndSpace: (MessageId, MessageHistoryHoleSpace)?
|
||||
self.valueBox.range(self.table, start: currentLowerBound, end: upperBound, keys: { key in
|
||||
idAndSpace = decomposeKey(key)
|
||||
return false
|
||||
}, limit: 1)
|
||||
if let (id, space) = idAndSpace {
|
||||
if space == holeSpace {
|
||||
result.insert(id.namespace)
|
||||
}
|
||||
currentLowerBound = self.upperBound(peerId: peerId, namespace: id.namespace, space: space)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func scanSpaces(peerId: PeerId, namespace: MessageId.Namespace) -> [MessageHistoryHoleSpace] {
|
||||
self.ensureInitialized(peerId: peerId)
|
||||
|
||||
var currentLowerBound = self.namespaceLowerBound(peerId: peerId, namespace: namespace)
|
||||
var result: [MessageHistoryHoleSpace] = []
|
||||
while true {
|
||||
var found = false
|
||||
self.valueBox.range(self.table, start: currentLowerBound, end: self.namespaceUpperBound(peerId: peerId, namespace: namespace), keys: { key in
|
||||
let space = decomposeKey(key).space
|
||||
result.append(space)
|
||||
currentLowerBound = self.upperBound(peerId: peerId, namespace: namespace, space: space)
|
||||
found = true
|
||||
return false
|
||||
}, limit: 1)
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
}
|
||||
assert(Set(result).count == result.count)
|
||||
return result
|
||||
}
|
||||
|
||||
func containing(id: MessageId) -> [MessageHistoryHoleSpace: ClosedRange<MessageId.Id>] {
|
||||
self.ensureInitialized(peerId: id.peerId)
|
||||
|
||||
var result: [MessageHistoryHoleSpace: ClosedRange<MessageId.Id>] = [:]
|
||||
for space in self.scanSpaces(peerId: id.peerId, namespace: id.namespace) {
|
||||
self.valueBox.range(self.table, start: self.key(id: id, space: space), end: self.upperBound(peerId: id.peerId, namespace: id.namespace, space: space), values: { key, value in
|
||||
let (upperId, keySpace) = decomposeKey(key)
|
||||
assert(keySpace == space)
|
||||
assert(upperId.peerId == id.peerId)
|
||||
assert(upperId.namespace == id.namespace)
|
||||
let lowerId = decodeValue(value: value, peerId: id.peerId, namespace: id.namespace)
|
||||
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
|
||||
result[space] = holeRange
|
||||
return false
|
||||
}, limit: 1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func closest(peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>) -> IndexSet {
|
||||
self.ensureInitialized(peerId: peerId)
|
||||
|
||||
var result = IndexSet()
|
||||
|
||||
func processIntersectingRange(_ key: ValueBoxKey, _ value: ReadBuffer) {
|
||||
let (upperId, keySpace) = decomposeKey(key)
|
||||
assert(keySpace == space)
|
||||
assert(upperId.peerId == peerId)
|
||||
assert(upperId.namespace == namespace)
|
||||
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
|
||||
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
|
||||
if holeRange.overlaps(range) {
|
||||
result.insert(integersIn: Int(holeRange.lowerBound) ... Int(holeRange.upperBound))
|
||||
}
|
||||
}
|
||||
|
||||
func processEdgeRange(_ key: ValueBoxKey, _ value: ReadBuffer) {
|
||||
let (upperId, keySpace) = decomposeKey(key)
|
||||
assert(keySpace == space)
|
||||
assert(upperId.peerId == peerId)
|
||||
assert(upperId.namespace == namespace)
|
||||
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
|
||||
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
|
||||
result.insert(integersIn: Int(holeRange.lowerBound) ... Int(holeRange.upperBound))
|
||||
}
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: range.lowerBound), space: space).predecessor, end: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space).successor, values: { key, value in
|
||||
processIntersectingRange(key, value)
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space), end: self.upperBound(peerId: peerId, namespace: namespace, space: space), values: { key, value in
|
||||
processIntersectingRange(key, value)
|
||||
return true
|
||||
}, limit: 1)
|
||||
|
||||
if !result.contains(Int(range.lowerBound)) {
|
||||
self.valueBox.range(self.table, start: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: range.lowerBound), space: space), end: self.lowerBound(peerId: peerId, namespace: namespace, space: space), values: { key, value in
|
||||
processEdgeRange(key, value)
|
||||
return true
|
||||
}, limit: 1)
|
||||
}
|
||||
if !result.contains(Int(range.upperBound)) {
|
||||
self.valueBox.range(self.table, start: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space), end: self.upperBound(peerId: peerId, namespace: namespace, space: space), values: { key, value in
|
||||
processEdgeRange(key, value)
|
||||
return true
|
||||
}, limit: 1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func add(peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>, operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
|
||||
self.ensureInitialized(peerId: peerId)
|
||||
|
||||
self.addInternal(peerId: peerId, namespace: namespace, space: space, range: range, operations: &operations)
|
||||
|
||||
switch space {
|
||||
case .everywhere:
|
||||
if let namespaceHoleTags = self.seedConfiguration.messageHoles[peerId.namespace]?[namespace] {
|
||||
for tag in namespaceHoleTags {
|
||||
self.addInternal(peerId: peerId, namespace: namespace, space: .tag(tag), range: range, operations: &operations)
|
||||
}
|
||||
}
|
||||
case .tag:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func addInternal(peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>, operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
|
||||
let clippedLowerBound = max(1, range.lowerBound)
|
||||
let clippedUpperBound = min(Int32.max - 1, range.upperBound)
|
||||
if clippedLowerBound > clippedUpperBound {
|
||||
return
|
||||
}
|
||||
let clippedRange = clippedLowerBound ... clippedUpperBound
|
||||
|
||||
var removedIndices = IndexSet()
|
||||
var insertedIndices = IndexSet()
|
||||
var removeKeys: [Int32] = []
|
||||
var insertRanges = IndexSet()
|
||||
|
||||
var alreadyMapped = false
|
||||
|
||||
func processRange(_ key: ValueBoxKey, _ value: ReadBuffer) {
|
||||
let (upperId, keySpace) = decomposeKey(key)
|
||||
assert(keySpace == space)
|
||||
assert(upperId.peerId == peerId)
|
||||
assert(upperId.namespace == namespace)
|
||||
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
|
||||
let holeRange: ClosedRange<Int32> = lowerId.id ... upperId.id
|
||||
if clippedRange.lowerBound >= holeRange.lowerBound && clippedRange.upperBound <= holeRange.upperBound {
|
||||
alreadyMapped = true
|
||||
return
|
||||
} else if clippedRange.overlaps(holeRange) || (holeRange.upperBound != Int32.max && clippedRange.lowerBound == holeRange.upperBound + 1) || clippedRange.upperBound == holeRange.lowerBound - 1 {
|
||||
removeKeys.append(upperId.id)
|
||||
let unionRange: ClosedRange = min(clippedRange.lowerBound, holeRange.lowerBound) ... max(clippedRange.upperBound, holeRange.upperBound)
|
||||
insertRanges.insert(integersIn: Int(unionRange.lowerBound) ... Int(unionRange.upperBound))
|
||||
}
|
||||
}
|
||||
|
||||
let lowerScanBound = max(0, clippedRange.lowerBound - 2)
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: lowerScanBound), space: space), end: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: clippedRange.upperBound), space: space).successor, values: { key, value in
|
||||
processRange(key, value)
|
||||
if alreadyMapped {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
if !alreadyMapped {
|
||||
self.valueBox.range(self.table, start: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: clippedRange.upperBound), space: space), end: self.upperBound(peerId: peerId, namespace: namespace, space: space), values: { key, value in
|
||||
processRange(key, value)
|
||||
if alreadyMapped {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, limit: 1)
|
||||
}
|
||||
|
||||
if alreadyMapped {
|
||||
return
|
||||
}
|
||||
|
||||
insertRanges.insert(integersIn: Int(clippedRange.lowerBound) ... Int(clippedRange.upperBound))
|
||||
insertedIndices.insert(integersIn: Int(clippedRange.lowerBound) ... Int(clippedRange.upperBound))
|
||||
|
||||
for id in removeKeys {
|
||||
self.valueBox.remove(self.table, key: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: id), space: space), secure: false)
|
||||
}
|
||||
|
||||
for insertRange in insertRanges.rangeView {
|
||||
let closedRange: ClosedRange<MessageId.Id> = Int32(insertRange.lowerBound) ... Int32(insertRange.upperBound - 1)
|
||||
var lowerBound: Int32 = closedRange.lowerBound
|
||||
self.valueBox.set(self.table, key: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: closedRange.upperBound), space: space), value: MemoryBuffer(memory: &lowerBound, capacity: 4, length: 4, freeWhenDone: false))
|
||||
}
|
||||
|
||||
addOperation(.insert(clippedRange), peerId: peerId, namespace: namespace, space: space, to: &operations)
|
||||
}
|
||||
|
||||
func remove(peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>, operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
|
||||
self.ensureInitialized(peerId: peerId)
|
||||
|
||||
self.removeInternal(peerId: peerId, namespace: namespace, space: space, range: range, operations: &operations)
|
||||
|
||||
switch space {
|
||||
case .everywhere:
|
||||
if let namespaceHoleTags = self.seedConfiguration.messageHoles[peerId.namespace]?[namespace] {
|
||||
for tag in namespaceHoleTags {
|
||||
self.removeInternal(peerId: peerId, namespace: namespace, space: .tag(tag), range: range, operations: &operations)
|
||||
}
|
||||
}
|
||||
case .tag:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func removeInternal(peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace, range: ClosedRange<MessageId.Id>, operations: inout [MessageHistoryIndexHoleOperationKey: [MessageHistoryIndexHoleOperation]]) {
|
||||
var removedIndices = IndexSet()
|
||||
var insertedIndices = IndexSet()
|
||||
var removeKeys: [Int32] = []
|
||||
var insertRanges = IndexSet()
|
||||
|
||||
func processRange(_ key: ValueBoxKey, _ value: ReadBuffer) {
|
||||
let (upperId, keySpace) = decomposeKey(key)
|
||||
assert(keySpace == space)
|
||||
assert(upperId.peerId == peerId)
|
||||
assert(upperId.namespace == namespace)
|
||||
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
|
||||
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
|
||||
if range.lowerBound <= holeRange.lowerBound && range.upperBound >= holeRange.upperBound {
|
||||
removeKeys.append(upperId.id)
|
||||
} else if range.overlaps(holeRange) {
|
||||
removeKeys.append(upperId.id)
|
||||
var holeIndices = IndexSet(integersIn: Int(holeRange.lowerBound) ... Int(holeRange.upperBound))
|
||||
holeIndices.remove(integersIn: Int(range.lowerBound) ... Int(range.upperBound))
|
||||
insertRanges.formUnion(holeIndices)
|
||||
}
|
||||
}
|
||||
|
||||
let lowerScanBound = max(0, range.lowerBound - 2)
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: lowerScanBound), space: space), end: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space).successor, values: { key, value in
|
||||
processRange(key, value)
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
self.valueBox.range(self.table, start: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: range.upperBound), space: space), end: self.upperBound(peerId: peerId, namespace: namespace, space: space), values: { key, value in
|
||||
processRange(key, value)
|
||||
return true
|
||||
}, limit: 1)
|
||||
|
||||
for id in removeKeys {
|
||||
self.valueBox.remove(self.table, key: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: id), space: space), secure: false)
|
||||
}
|
||||
|
||||
for insertRange in insertRanges.rangeView {
|
||||
let closedRange: ClosedRange<MessageId.Id> = Int32(insertRange.lowerBound) ... Int32(insertRange.upperBound - 1)
|
||||
var lowerBound: Int32 = closedRange.lowerBound
|
||||
self.valueBox.set(self.table, key: self.key(id: MessageId(peerId: peerId, namespace: namespace, id: closedRange.upperBound), space: space), value: MemoryBuffer(memory: &lowerBound, capacity: 4, length: 4, freeWhenDone: false))
|
||||
}
|
||||
|
||||
if !removeKeys.isEmpty {
|
||||
addOperation(.remove(range), peerId: peerId, namespace: namespace, space: space, to: &operations)
|
||||
}
|
||||
}
|
||||
|
||||
func debugList(peerId: PeerId, namespace: MessageId.Namespace, space: MessageHistoryHoleSpace) -> [ClosedRange<MessageId.Id>] {
|
||||
var result: [ClosedRange<MessageId.Id>] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(peerId: peerId, namespace: namespace, space: space), end: self.upperBound(peerId: peerId, namespace: namespace, space: space), values: { key, value in
|
||||
let (upperId, keySpace) = decomposeKey(key)
|
||||
assert(keySpace == space)
|
||||
assert(upperId.peerId == peerId)
|
||||
assert(upperId.namespace == namespace)
|
||||
let lowerId = decodeValue(value: value, peerId: peerId, namespace: namespace)
|
||||
let holeRange: ClosedRange<MessageId.Id> = lowerId.id ... upperId.id
|
||||
result.append(holeRange)
|
||||
return true
|
||||
}, limit: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
37
submodules/Postbox/Postbox/MessageHistoryHolesView.swift
Normal file
37
submodules/Postbox/Postbox/MessageHistoryHolesView.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import Foundation
|
||||
|
||||
public struct MessageHistoryHolesViewEntry: Equatable, Hashable {
|
||||
public let hole: MessageHistoryViewHole
|
||||
public let direction: MessageHistoryViewRelativeHoleDirection
|
||||
public let space: MessageHistoryHoleSpace
|
||||
|
||||
public init(hole: MessageHistoryViewHole, direction: MessageHistoryViewRelativeHoleDirection, space: MessageHistoryHoleSpace) {
|
||||
self.hole = hole
|
||||
self.direction = direction
|
||||
self.space = space
|
||||
}
|
||||
}
|
||||
|
||||
final class MutableMessageHistoryHolesView {
|
||||
fileprivate var entries = Set<MessageHistoryHolesViewEntry>()
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func update(_ holes: Set<MessageHistoryHolesViewEntry>) -> Bool {
|
||||
if self.entries != holes {
|
||||
self.entries = holes
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class MessageHistoryHolesView {
|
||||
public let entries: Set<MessageHistoryHolesViewEntry>
|
||||
|
||||
init(_ mutableView: MutableMessageHistoryHolesView) {
|
||||
self.entries = mutableView.entries
|
||||
}
|
||||
}
|
||||
347
submodules/Postbox/Postbox/MessageHistoryIndexTable.swift
Normal file
347
submodules/Postbox/Postbox/MessageHistoryIndexTable.swift
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import Foundation
|
||||
|
||||
public enum AddMessagesLocation {
|
||||
case Random
|
||||
case UpperHistoryBlock
|
||||
}
|
||||
|
||||
enum MessageHistoryIndexOperation {
|
||||
case InsertMessage(InternalStoreMessage)
|
||||
case InsertExistingMessage(InternalStoreMessage)
|
||||
case Remove(index: MessageIndex)
|
||||
case Update(MessageIndex, InternalStoreMessage)
|
||||
case UpdateTimestamp(MessageIndex, Int32)
|
||||
}
|
||||
|
||||
private let HistoryEntryTypeMask: Int8 = 1
|
||||
private let HistoryEntryTypeMessage: Int8 = 0
|
||||
private let HistoryEntryMessageFlagIncoming: Int8 = 1 << 1
|
||||
|
||||
private func readHistoryIndexEntry(_ peerId: PeerId, namespace: MessageId.Namespace, key: ValueBoxKey, value: ReadBuffer) -> MessageIndex {
|
||||
var flags: Int8 = 0
|
||||
value.read(&flags, offset: 0, length: 1)
|
||||
var timestamp: Int32 = 0
|
||||
value.read(×tamp, offset: 0, length: 4)
|
||||
let index = MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: key.getInt32(8 + 4)), timestamp: timestamp)
|
||||
return index
|
||||
}
|
||||
|
||||
private func modifyHistoryIndexEntryTimestamp(value: ReadBuffer, timestamp: Int32) -> MemoryBuffer {
|
||||
let buffer = WriteBuffer()
|
||||
buffer.write(value.memory.advanced(by: 0), offset: 0, length: 1)
|
||||
var varTimestamp: Int32 = timestamp
|
||||
buffer.write(&varTimestamp, offset: 0, length: 4)
|
||||
buffer.write(value.memory.advanced(by: 5), offset: 0, length: value.length - 5)
|
||||
return buffer
|
||||
}
|
||||
|
||||
final class MessageHistoryIndexTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable
|
||||
private let globalMessageIdsTable: GlobalMessageIdsTable
|
||||
private let metadataTable: MessageHistoryMetadataTable
|
||||
private let seedConfiguration: SeedConfiguration
|
||||
|
||||
private var cachedExistingNamespaces: [PeerId: Set<MessageId.Namespace>] = [:]
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, messageHistoryHoleIndexTable: MessageHistoryHoleIndexTable, globalMessageIdsTable: GlobalMessageIdsTable, metadataTable: MessageHistoryMetadataTable, seedConfiguration: SeedConfiguration) {
|
||||
self.messageHistoryHoleIndexTable = messageHistoryHoleIndexTable
|
||||
self.globalMessageIdsTable = globalMessageIdsTable
|
||||
self.seedConfiguration = seedConfiguration
|
||||
self.metadataTable = metadataTable
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
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)
|
||||
key.setInt32(8 + 4, value: id.id)
|
||||
return key
|
||||
}
|
||||
|
||||
private func lowerBound(peerId: PeerId) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8)
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
return key
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private func upperBound(peerId: PeerId) -> ValueBoxKey {
|
||||
return self.lowerBound(peerId: peerId).successor
|
||||
}
|
||||
|
||||
func addMessages(_ messages: [InternalStoreMessage], operations: inout [MessageHistoryIndexOperation]) {
|
||||
if messages.count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for message in messages {
|
||||
let index = MessageIndex(id: message.id, timestamp: message.timestamp)
|
||||
|
||||
if let currentIndex = self.getIndex(index.id) {
|
||||
if currentIndex.timestamp == index.timestamp {
|
||||
operations.append(.InsertExistingMessage(message))
|
||||
} else {
|
||||
self.justRemove(currentIndex, operations: &operations)
|
||||
self.justInsertMessage(message, operations: &operations)
|
||||
}
|
||||
} else {
|
||||
self.justInsertMessage(message, operations: &operations)
|
||||
|
||||
self.cachedExistingNamespaces[message.id.peerId]?.insert(message.id.namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeMessage(_ id: MessageId, operations: inout [MessageHistoryIndexOperation]) {
|
||||
if let index = self.getIndex(id) {
|
||||
self.justRemove(index, operations: &operations)
|
||||
}
|
||||
}
|
||||
|
||||
func removeMessagesInRange(peerId: PeerId, namespace: MessageId.Namespace, minId: MessageId.Id, maxId: MessageId.Id, operations: inout [MessageHistoryIndexOperation]) {
|
||||
if minId > maxId {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
var removeMessageIds: [MessageId] = []
|
||||
self.valueBox.range(self.table, start: self.key(MessageId(peerId: peerId, namespace: namespace, id: minId)).predecessor, end: self.key(MessageId(peerId: peerId, namespace: namespace, id: maxId)).successor, values: { key, value in
|
||||
let index = readHistoryIndexEntry(peerId, namespace: namespace, key: key, value: value)
|
||||
removeMessageIds.append(index.id)
|
||||
return true
|
||||
}, limit: 0)
|
||||
for id in removeMessageIds {
|
||||
self.removeMessage(id, operations: &operations)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMessage(_ id: MessageId, message: InternalStoreMessage, operations: inout [MessageHistoryIndexOperation]) {
|
||||
if let previousIndex = self.getIndex(id) {
|
||||
if previousIndex != message.index {
|
||||
var intermediateOperations: [MessageHistoryIndexOperation] = []
|
||||
self.removeMessage(id, operations: &intermediateOperations)
|
||||
self.addMessages([message], operations: &intermediateOperations)
|
||||
|
||||
for operation in intermediateOperations {
|
||||
switch operation {
|
||||
case let .Remove(index) where index == previousIndex:
|
||||
operations.append(.Update(previousIndex, message))
|
||||
case let .InsertMessage(insertMessage) where insertMessage.index == message.index:
|
||||
break
|
||||
case let .InsertExistingMessage(insertMessage) where insertMessage.index == message.index:
|
||||
operations.removeAll()
|
||||
operations.append(.Remove(index: previousIndex))
|
||||
operations.append(.Update(insertMessage.index, message))
|
||||
default:
|
||||
operations.append(operation)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
operations.append(.Update(previousIndex, message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateTimestamp(_ id: MessageId, timestamp: Int32, operations: inout [MessageHistoryIndexOperation]) {
|
||||
if let previousData = self.valueBox.get(self.table, key: self.key(id)), let previousIndex = self.getIndex(id), previousIndex.timestamp != timestamp {
|
||||
let updatedEntry = modifyHistoryIndexEntryTimestamp(value: previousData, timestamp: timestamp)
|
||||
self.valueBox.remove(self.table, key: self.key(id), secure: false)
|
||||
self.valueBox.set(self.table, key: self.key(id), value: updatedEntry)
|
||||
|
||||
operations.append(.UpdateTimestamp(MessageIndex(id: id, timestamp: previousIndex.timestamp), timestamp))
|
||||
}
|
||||
}
|
||||
|
||||
private func justInsertMessage(_ message: InternalStoreMessage, operations: inout [MessageHistoryIndexOperation]) {
|
||||
let index = MessageIndex(id: message.id, timestamp: message.timestamp)
|
||||
|
||||
let value = WriteBuffer()
|
||||
var flags: Int8 = HistoryEntryTypeMessage
|
||||
if message.flags.contains(.Incoming) {
|
||||
flags |= HistoryEntryMessageFlagIncoming
|
||||
}
|
||||
var timestamp: Int32 = index.timestamp
|
||||
value.write(&flags, offset: 0, length: 1)
|
||||
value.write(×tamp, offset: 0, length: 4)
|
||||
self.valueBox.set(self.table, key: self.key(index.id), value: value)
|
||||
|
||||
operations.append(.InsertMessage(message))
|
||||
|
||||
if self.seedConfiguration.globalMessageIdsPeerIdNamespaces.contains(GlobalMessageIdsNamespace(peerIdNamespace: index.id.peerId.namespace, messageIdNamespace: index.id.namespace)) {
|
||||
self.globalMessageIdsTable.set(index.id.id, id: index.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func justRemove(_ index: MessageIndex, operations: inout [MessageHistoryIndexOperation]) {
|
||||
self.valueBox.remove(self.table, key: self.key(index.id), secure: false)
|
||||
|
||||
operations.append(.Remove(index: index))
|
||||
if self.seedConfiguration.globalMessageIdsPeerIdNamespaces.contains(GlobalMessageIdsNamespace(peerIdNamespace: index.id.peerId.namespace, messageIdNamespace: index.id.namespace)) {
|
||||
self.globalMessageIdsTable.remove(index.id.id)
|
||||
}
|
||||
}
|
||||
|
||||
func getIndex(_ id: MessageId) -> MessageIndex? {
|
||||
let key = self.key(id)
|
||||
if let value = self.valueBox.get(self.table, key: key) {
|
||||
return readHistoryIndexEntry(id.peerId, namespace: id.namespace, key: key, value: value)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func top(_ peerId: PeerId, namespace: MessageId.Namespace) -> MessageIndex? {
|
||||
var index: MessageIndex?
|
||||
self.valueBox.range(self.table, start: self.upperBound(peerId, namespace: namespace), end: self.lowerBound(peerId, namespace: namespace), values: { key, value in
|
||||
index = readHistoryIndexEntry(peerId, namespace: namespace, key: key, value: value)
|
||||
return false
|
||||
}, limit: 1)
|
||||
return index
|
||||
}
|
||||
|
||||
func exists(_ id: MessageId) -> Bool {
|
||||
return self.valueBox.exists(self.table, key: self.key(id))
|
||||
}
|
||||
|
||||
func incomingMessageCountInRange(_ peerId: PeerId, namespace: MessageId.Namespace, minId: MessageId.Id, maxId: MessageId.Id) -> (Int, Bool) {
|
||||
var count = 0
|
||||
var holes = false
|
||||
if minId <= maxId {
|
||||
self.valueBox.range(self.table, start: self.key(MessageId(peerId: peerId, namespace: namespace, id: minId)).predecessor, end: self.key(MessageId(peerId: peerId, namespace: namespace, id: maxId)).successor, values: { _, value in
|
||||
var flags: Int8 = 0
|
||||
value.read(&flags, offset: 0, length: 1)
|
||||
if (flags & HistoryEntryMessageFlagIncoming) != 0 {
|
||||
count += 1
|
||||
}
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
holes = !self.messageHistoryHoleIndexTable.closest(peerId: peerId, namespace: namespace, space: .everywhere, range: minId ... maxId).isEmpty
|
||||
}
|
||||
|
||||
return (count, holes)
|
||||
}
|
||||
|
||||
func incomingMessageCountInIds(_ peerId: PeerId, namespace: MessageId.Namespace, ids: [MessageId.Id]) -> (Int, Bool) {
|
||||
var count = 0
|
||||
var holes = false
|
||||
|
||||
for id in ids {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(MessageId(peerId: peerId, namespace: namespace, id: id))) {
|
||||
var flags: Int8 = 0
|
||||
value.read(&flags, offset: 0, length: 1)
|
||||
if (flags & HistoryEntryMessageFlagIncoming) != 0 {
|
||||
count += 1
|
||||
}
|
||||
if !self.messageHistoryHoleIndexTable.containing(id: MessageId(peerId: peerId, namespace: namespace, id: id)).isEmpty {
|
||||
holes = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (count, holes)
|
||||
}
|
||||
|
||||
func indexForId(higherThan id: MessageId) -> MessageIndex? {
|
||||
var result: MessageIndex?
|
||||
self.valueBox.range(self.table, start: self.key(id), end: self.upperBound(id.peerId, namespace: id.namespace), values: { key, value in
|
||||
result = readHistoryIndexEntry(id.peerId, namespace: id.namespace, key: key, value: value)
|
||||
return false
|
||||
}, limit: 1)
|
||||
return result
|
||||
}
|
||||
|
||||
func earlierEntries(id: MessageId, count: Int) -> [MessageIndex] {
|
||||
var entries: [MessageIndex] = []
|
||||
let key = self.key(id)
|
||||
self.valueBox.range(self.table, start: key, end: self.lowerBound(id.peerId, namespace: id.namespace), values: { key, value in
|
||||
entries.append(readHistoryIndexEntry(id.peerId, namespace: id.namespace, key: key, value: value))
|
||||
return true
|
||||
}, limit: count)
|
||||
return entries
|
||||
}
|
||||
|
||||
func existingNamespaces(peerId: PeerId) -> Set<MessageId.Namespace> {
|
||||
if let cached = self.cachedExistingNamespaces[peerId] {
|
||||
return cached
|
||||
} else {
|
||||
let namespaces = Set(self.fetchExistingNamespaces(peerId: peerId))
|
||||
self.cachedExistingNamespaces[peerId] = namespaces
|
||||
return namespaces
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchExistingNamespaces(peerId: PeerId) -> [MessageId.Namespace] {
|
||||
var result: [MessageId.Namespace] = []
|
||||
var lowerBound = self.lowerBound(peerId: peerId)
|
||||
let upperBound = self.upperBound(peerId: peerId)
|
||||
while true {
|
||||
var namespace: MessageId.Namespace?
|
||||
self.valueBox.range(self.table, start: lowerBound, end: upperBound, keys: { key in
|
||||
assert(key.getInt64(0) == peerId.toInt64())
|
||||
namespace = key.getInt32(8)
|
||||
return false
|
||||
}, limit: 1)
|
||||
if let namespace = namespace {
|
||||
result.append(namespace)
|
||||
lowerBound = self.lowerBound(peerId, namespace: namespace + 1)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func debugList(_ peerId: PeerId, namespace: MessageId.Namespace) -> [MessageIndex] {
|
||||
var list: [MessageIndex] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(peerId, namespace: namespace), end: self.upperBound(peerId, namespace: namespace), values: { key, value in
|
||||
list.append(readHistoryIndexEntry(peerId, namespace: namespace, key: key, value: value))
|
||||
|
||||
return true
|
||||
}, limit: 0)
|
||||
return list
|
||||
}
|
||||
|
||||
func closestIndex(id: MessageId) -> MessageIndex? {
|
||||
if let index = self.getIndex(id) {
|
||||
return index
|
||||
} else {
|
||||
var index: MessageIndex?
|
||||
self.valueBox.range(self.table, start: self.key(id).successor, end: self.lowerBound(id.peerId, namespace: id.namespace), values: { key, value in
|
||||
index = readHistoryIndexEntry(id.peerId, namespace: id.namespace, key: key, value: value)
|
||||
return true
|
||||
}, limit: 1)
|
||||
|
||||
if index == nil {
|
||||
self.valueBox.range(self.table, start: self.key(id).predecessor, end: self.upperBound(id.peerId, namespace: id.namespace), values: { key, value in
|
||||
index = readHistoryIndexEntry(id.peerId, namespace: id.namespace, key: key, value: value)
|
||||
return true
|
||||
}, limit: 1)
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedExistingNamespaces.removeAll()
|
||||
}
|
||||
}
|
||||
357
submodules/Postbox/Postbox/MessageHistoryMetadataTable.swift
Normal file
357
submodules/Postbox/Postbox/MessageHistoryMetadataTable.swift
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
import Foundation
|
||||
|
||||
private enum MetadataPrefix: Int8 {
|
||||
case ChatListInitialized = 0
|
||||
case PeerNextMessageIdByNamespace = 2
|
||||
case NextStableMessageId = 3
|
||||
case ChatListTotalUnreadState = 4
|
||||
case NextPeerOperationLogIndex = 5
|
||||
case ChatListGroupInitialized = 6
|
||||
case GroupFeedIndexInitialized = 7
|
||||
case ShouldReindexUnreadCounts = 8
|
||||
case PeerHistoryInitialized = 9
|
||||
}
|
||||
|
||||
public struct ChatListTotalUnreadCounters: PostboxCoding, Equatable {
|
||||
public var messageCount: Int32
|
||||
public var chatCount: Int32
|
||||
|
||||
public init(messageCount: Int32, chatCount: Int32) {
|
||||
self.messageCount = messageCount
|
||||
self.chatCount = chatCount
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.messageCount = decoder.decodeInt32ForKey("m", orElse: 0)
|
||||
self.chatCount = decoder.decodeInt32ForKey("c", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.messageCount, forKey: "m")
|
||||
encoder.encodeInt32(self.chatCount, forKey: "c")
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatListTotalUnreadStateCategory: Int32 {
|
||||
case filtered = 0
|
||||
case raw = 1
|
||||
}
|
||||
|
||||
public enum ChatListTotalUnreadStateStats: Int32 {
|
||||
case messages = 0
|
||||
case chats = 1
|
||||
}
|
||||
|
||||
private struct InitializedChatListKey: Hashable {
|
||||
let groupId: PeerGroupId
|
||||
}
|
||||
|
||||
final class MessageHistoryMetadataTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
let sharedPeerHistoryInitializedKey = ValueBoxKey(length: 8 + 1)
|
||||
let sharedGroupFeedIndexInitializedKey = ValueBoxKey(length: 4 + 1)
|
||||
let sharedChatListGroupHistoryInitializedKey = ValueBoxKey(length: 4 + 1)
|
||||
let sharedPeerNextMessageIdByNamespaceKey = ValueBoxKey(length: 8 + 1 + 4)
|
||||
let sharedBuffer = WriteBuffer()
|
||||
|
||||
private var initializedChatList = Set<InitializedChatListKey>()
|
||||
private var initializedHistoryPeerIds = Set<PeerId>()
|
||||
private var initializedGroupFeedIndexIds = Set<PeerGroupId>()
|
||||
|
||||
private var peerNextMessageIdByNamespace: [PeerId: [MessageId.Namespace: MessageId.Id]] = [:]
|
||||
private var updatedPeerNextMessageIdByNamespace: [PeerId: Set<MessageId.Namespace>] = [:]
|
||||
|
||||
private var nextMessageStableId: UInt32?
|
||||
private var nextMessageStableIdUpdated = false
|
||||
|
||||
private var chatListTotalUnreadState: ChatListTotalUnreadState?
|
||||
private var chatListTotalUnreadStateUpdated = false
|
||||
|
||||
private var nextPeerOperationLogIndex: UInt32?
|
||||
private var nextPeerOperationLogIndexUpdated = false
|
||||
|
||||
private var currentPinnedChatPeerIds: Set<PeerId>?
|
||||
private var currentPinnedChatPeerIdsUpdated = false
|
||||
|
||||
private func peerHistoryInitializedKey(_ id: PeerId) -> ValueBoxKey {
|
||||
self.sharedPeerHistoryInitializedKey.setInt64(0, value: id.toInt64())
|
||||
self.sharedPeerHistoryInitializedKey.setInt8(8, value: MetadataPrefix.PeerHistoryInitialized.rawValue)
|
||||
return self.sharedPeerHistoryInitializedKey
|
||||
}
|
||||
|
||||
private func groupFeedIndexInitializedKey(_ id: PeerGroupId) -> ValueBoxKey {
|
||||
self.sharedGroupFeedIndexInitializedKey.setInt32(0, value: id.rawValue)
|
||||
self.sharedGroupFeedIndexInitializedKey.setInt8(4, value: MetadataPrefix.GroupFeedIndexInitialized.rawValue)
|
||||
return self.sharedGroupFeedIndexInitializedKey
|
||||
}
|
||||
|
||||
private func chatListGroupInitializedKey(_ key: InitializedChatListKey) -> ValueBoxKey {
|
||||
self.sharedChatListGroupHistoryInitializedKey.setInt32(0, value: key.groupId.rawValue)
|
||||
self.sharedChatListGroupHistoryInitializedKey.setInt8(8, value: MetadataPrefix.ChatListGroupInitialized.rawValue)
|
||||
return self.sharedChatListGroupHistoryInitializedKey
|
||||
}
|
||||
|
||||
private func peerNextMessageIdByNamespaceKey(_ id: PeerId, namespace: MessageId.Namespace) -> ValueBoxKey {
|
||||
self.sharedPeerNextMessageIdByNamespaceKey.setInt64(0, value: id.toInt64())
|
||||
self.sharedPeerNextMessageIdByNamespaceKey.setInt8(8, value: MetadataPrefix.PeerNextMessageIdByNamespace.rawValue)
|
||||
self.sharedPeerNextMessageIdByNamespaceKey.setInt32(8 + 1, value: namespace)
|
||||
|
||||
return self.sharedPeerNextMessageIdByNamespaceKey
|
||||
}
|
||||
|
||||
private func key(_ prefix: MetadataPrefix) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 1)
|
||||
key.setInt8(0, value: prefix.rawValue)
|
||||
return key
|
||||
}
|
||||
|
||||
func setInitializedChatList(groupId: PeerGroupId) {
|
||||
switch groupId {
|
||||
case .root:
|
||||
self.valueBox.set(self.table, key: self.key(MetadataPrefix.ChatListInitialized), value: MemoryBuffer())
|
||||
case .group:
|
||||
self.valueBox.set(self.table, key: self.chatListGroupInitializedKey(InitializedChatListKey(groupId: groupId)), value: MemoryBuffer())
|
||||
}
|
||||
self.initializedChatList.insert(InitializedChatListKey(groupId: groupId))
|
||||
}
|
||||
|
||||
func isInitializedChatList(groupId: PeerGroupId) -> Bool {
|
||||
let key = InitializedChatListKey(groupId: groupId)
|
||||
if self.initializedChatList.contains(key) {
|
||||
return true
|
||||
} else {
|
||||
switch groupId {
|
||||
case .root:
|
||||
if self.valueBox.exists(self.table, key: self.key(MetadataPrefix.ChatListInitialized)) {
|
||||
self.initializedChatList.insert(key)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .group:
|
||||
if self.valueBox.exists(self.table, key: self.chatListGroupInitializedKey(key)) {
|
||||
self.initializedChatList.insert(key)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setShouldReindexUnreadCounts(value: Bool) {
|
||||
if value {
|
||||
self.valueBox.set(self.table, key: self.key(MetadataPrefix.ShouldReindexUnreadCounts), value: MemoryBuffer())
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: self.key(MetadataPrefix.ShouldReindexUnreadCounts), secure: false)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldReindexUnreadCounts() -> Bool {
|
||||
if self.valueBox.exists(self.table, key: self.key(MetadataPrefix.ShouldReindexUnreadCounts)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func setInitialized(_ peerId: PeerId) {
|
||||
self.initializedHistoryPeerIds.insert(peerId)
|
||||
self.sharedBuffer.reset()
|
||||
self.valueBox.set(self.table, key: self.peerHistoryInitializedKey(peerId), value: self.sharedBuffer)
|
||||
}
|
||||
|
||||
func isInitialized(_ peerId: PeerId) -> Bool {
|
||||
if self.initializedHistoryPeerIds.contains(peerId) {
|
||||
return true
|
||||
} else {
|
||||
if self.valueBox.exists(self.table, key: self.peerHistoryInitializedKey(peerId)) {
|
||||
self.initializedHistoryPeerIds.insert(peerId)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setGroupFeedIndexInitialized(_ groupId: PeerGroupId) {
|
||||
self.initializedGroupFeedIndexIds.insert(groupId)
|
||||
self.sharedBuffer.reset()
|
||||
self.valueBox.set(self.table, key: self.groupFeedIndexInitializedKey(groupId), value: self.sharedBuffer)
|
||||
}
|
||||
|
||||
func isGroupFeedIndexInitialized(_ groupId: PeerGroupId) -> Bool {
|
||||
if self.initializedGroupFeedIndexIds.contains(groupId) {
|
||||
return true
|
||||
} else {
|
||||
if self.valueBox.exists(self.table, key: self.groupFeedIndexInitializedKey(groupId)) {
|
||||
self.initializedGroupFeedIndexIds.insert(groupId)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNextMessageIdAndIncrement(_ peerId: PeerId, namespace: MessageId.Namespace) -> MessageId {
|
||||
if let messageIdByNamespace = self.peerNextMessageIdByNamespace[peerId] {
|
||||
if let nextId = messageIdByNamespace[namespace] {
|
||||
self.peerNextMessageIdByNamespace[peerId]![namespace] = nextId + 1
|
||||
if updatedPeerNextMessageIdByNamespace[peerId] != nil {
|
||||
updatedPeerNextMessageIdByNamespace[peerId]!.insert(namespace)
|
||||
} else {
|
||||
updatedPeerNextMessageIdByNamespace[peerId] = Set<MessageId.Namespace>([namespace])
|
||||
}
|
||||
return MessageId(peerId: peerId, namespace: namespace, id: nextId)
|
||||
} else {
|
||||
var nextId: Int32 = 1
|
||||
if let value = self.valueBox.get(self.table, key: self.peerNextMessageIdByNamespaceKey(peerId, namespace: namespace)) {
|
||||
value.read(&nextId, offset: 0, length: 4)
|
||||
}
|
||||
self.peerNextMessageIdByNamespace[peerId]![namespace] = nextId + 1
|
||||
if updatedPeerNextMessageIdByNamespace[peerId] != nil {
|
||||
updatedPeerNextMessageIdByNamespace[peerId]!.insert(namespace)
|
||||
} else {
|
||||
updatedPeerNextMessageIdByNamespace[peerId] = Set<MessageId.Namespace>([namespace])
|
||||
}
|
||||
return MessageId(peerId: peerId, namespace: namespace, id: nextId)
|
||||
}
|
||||
} else {
|
||||
var nextId: Int32 = 1
|
||||
if let value = self.valueBox.get(self.table, key: self.peerNextMessageIdByNamespaceKey(peerId, namespace: namespace)) {
|
||||
value.read(&nextId, offset: 0, length: 4)
|
||||
}
|
||||
|
||||
self.peerNextMessageIdByNamespace[peerId] = [namespace: nextId + 1]
|
||||
if updatedPeerNextMessageIdByNamespace[peerId] != nil {
|
||||
updatedPeerNextMessageIdByNamespace[peerId]!.insert(namespace)
|
||||
} else {
|
||||
updatedPeerNextMessageIdByNamespace[peerId] = Set<MessageId.Namespace>([namespace])
|
||||
}
|
||||
return MessageId(peerId: peerId, namespace: namespace, id: nextId)
|
||||
}
|
||||
}
|
||||
|
||||
func getNextStableMessageIndexId() -> UInt32 {
|
||||
if let nextId = self.nextMessageStableId {
|
||||
self.nextMessageStableId = nextId + 1
|
||||
self.nextMessageStableIdUpdated = true
|
||||
return nextId
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.NextStableMessageId)) {
|
||||
var nextId: UInt32 = 0
|
||||
value.read(&nextId, offset: 0, length: 4)
|
||||
self.nextMessageStableId = nextId + 1
|
||||
self.nextMessageStableIdUpdated = true
|
||||
return nextId
|
||||
} else {
|
||||
let nextId: UInt32 = 1
|
||||
self.nextMessageStableId = nextId + 1
|
||||
self.nextMessageStableIdUpdated = true
|
||||
return nextId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNextPeerOperationLogIndex() -> UInt32 {
|
||||
if let nextId = self.nextPeerOperationLogIndex {
|
||||
self.nextPeerOperationLogIndex = nextId + 1
|
||||
self.nextPeerOperationLogIndexUpdated = true
|
||||
return nextId
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.NextPeerOperationLogIndex)) {
|
||||
var nextId: UInt32 = 0
|
||||
value.read(&nextId, offset: 0, length: 4)
|
||||
self.nextPeerOperationLogIndex = nextId + 1
|
||||
self.nextPeerOperationLogIndexUpdated = true
|
||||
return nextId
|
||||
} else {
|
||||
let nextId: UInt32 = 1
|
||||
self.nextPeerOperationLogIndex = nextId + 1
|
||||
self.nextPeerOperationLogIndexUpdated = true
|
||||
return nextId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getChatListTotalUnreadState() -> ChatListTotalUnreadState {
|
||||
if let cached = self.chatListTotalUnreadState {
|
||||
return cached
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.ChatListTotalUnreadState)), let state = PostboxDecoder(buffer: value).decodeObjectForKey("_", decoder: {
|
||||
ChatListTotalUnreadState(decoder: $0)
|
||||
}) as? ChatListTotalUnreadState {
|
||||
self.chatListTotalUnreadState = state
|
||||
return state
|
||||
} else {
|
||||
let state = ChatListTotalUnreadState(absoluteCounters: [:], filteredCounters: [:])
|
||||
self.chatListTotalUnreadState = state
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setChatListTotalUnreadState(_ state: ChatListTotalUnreadState) {
|
||||
let current = self.getChatListTotalUnreadState()
|
||||
if current != state {
|
||||
self.chatListTotalUnreadState = state
|
||||
self.chatListTotalUnreadStateUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.initializedChatList.removeAll()
|
||||
self.initializedHistoryPeerIds.removeAll()
|
||||
self.peerNextMessageIdByNamespace.removeAll()
|
||||
self.updatedPeerNextMessageIdByNamespace.removeAll()
|
||||
self.nextMessageStableId = nil
|
||||
self.nextMessageStableIdUpdated = false
|
||||
self.chatListTotalUnreadState = nil
|
||||
self.chatListTotalUnreadStateUpdated = false
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
let sharedBuffer = WriteBuffer()
|
||||
for (peerId, namespaces) in self.updatedPeerNextMessageIdByNamespace {
|
||||
for namespace in namespaces {
|
||||
if let messageIdByNamespace = self.peerNextMessageIdByNamespace[peerId], let maxId = messageIdByNamespace[namespace] {
|
||||
sharedBuffer.reset()
|
||||
var mutableMaxId = maxId
|
||||
sharedBuffer.write(&mutableMaxId, offset: 0, length: 4)
|
||||
self.valueBox.set(self.table, key: self.peerNextMessageIdByNamespaceKey(peerId, namespace: namespace), value: sharedBuffer)
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: self.peerNextMessageIdByNamespaceKey(peerId, namespace: namespace), secure: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.updatedPeerNextMessageIdByNamespace.removeAll()
|
||||
|
||||
if self.nextMessageStableIdUpdated {
|
||||
if let nextMessageStableId = self.nextMessageStableId {
|
||||
var nextId: UInt32 = nextMessageStableId
|
||||
self.valueBox.set(self.table, key: self.key(.NextStableMessageId), value: MemoryBuffer(memory: &nextId, capacity: 4, length: 4, freeWhenDone: false))
|
||||
self.nextMessageStableIdUpdated = false
|
||||
}
|
||||
}
|
||||
|
||||
if self.nextPeerOperationLogIndexUpdated {
|
||||
if let nextPeerOperationLogIndex = self.nextPeerOperationLogIndex {
|
||||
var nextId: UInt32 = nextPeerOperationLogIndex
|
||||
self.valueBox.set(self.table, key: self.key(.NextPeerOperationLogIndex), value: MemoryBuffer(memory: &nextId, capacity: 4, length: 4, freeWhenDone: false))
|
||||
self.nextPeerOperationLogIndexUpdated = false
|
||||
}
|
||||
}
|
||||
|
||||
if self.chatListTotalUnreadStateUpdated {
|
||||
if let state = self.chatListTotalUnreadState {
|
||||
let buffer = PostboxEncoder()
|
||||
buffer.encodeObject(state, forKey: "_")
|
||||
self.valueBox.set(self.table, key: self.key(.ChatListTotalUnreadState), value: buffer.readBufferNoCopy())
|
||||
}
|
||||
self.chatListTotalUnreadStateUpdated = false
|
||||
}
|
||||
}
|
||||
}
|
||||
10
submodules/Postbox/Postbox/MessageHistoryOperation.swift
Normal file
10
submodules/Postbox/Postbox/MessageHistoryOperation.swift
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import Foundation
|
||||
|
||||
enum MessageHistoryOperation {
|
||||
case InsertMessage(IntermediateMessage)
|
||||
case Remove([(MessageIndex, MessageTags)])
|
||||
case UpdateReadState(PeerId, CombinedPeerReadState)
|
||||
case UpdateEmbeddedMedia(MessageIndex, ReadBuffer)
|
||||
case UpdateTimestamp(MessageIndex, Int32)
|
||||
case UpdateGroupInfos([MessageId: MessageGroupInfo])
|
||||
}
|
||||
572
submodules/Postbox/Postbox/MessageHistoryReadStateTable.swift
Normal file
572
submodules/Postbox/Postbox/MessageHistoryReadStateTable.swift
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
import Foundation
|
||||
|
||||
private let traceReadStates = false
|
||||
|
||||
enum ApplyInteractiveMaxReadIdResult {
|
||||
case None
|
||||
case Push(thenSync: Bool)
|
||||
}
|
||||
|
||||
private final class InternalPeerReadStates {
|
||||
var namespaces: [MessageId.Namespace: PeerReadState]
|
||||
|
||||
init(namespaces: [MessageId.Namespace: PeerReadState]) {
|
||||
self.namespaces = namespaces
|
||||
}
|
||||
}
|
||||
|
||||
final class MessageHistoryReadStateTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private let seedConfiguration: SeedConfiguration
|
||||
|
||||
private var cachedPeerReadStates: [PeerId: InternalPeerReadStates?] = [:]
|
||||
private var updatedInitialPeerReadStates: [PeerId: [MessageId.Namespace: PeerReadState]] = [:]
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
|
||||
private func key(_ id: PeerId) -> ValueBoxKey {
|
||||
self.sharedKey.setInt64(0, value: id.toInt64())
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, seedConfiguration: SeedConfiguration) {
|
||||
self.seedConfiguration = seedConfiguration
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func get(_ id: PeerId) -> InternalPeerReadStates? {
|
||||
if let states = self.cachedPeerReadStates[id] {
|
||||
return states
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(id)) {
|
||||
var count: Int32 = 0
|
||||
value.read(&count, offset: 0, length: 4)
|
||||
var stateByNamespace: [MessageId.Namespace: PeerReadState] = [:]
|
||||
for _ in 0 ..< count {
|
||||
var namespaceId: Int32 = 0
|
||||
value.read(&namespaceId, offset: 0, length: 4)
|
||||
|
||||
let state: PeerReadState
|
||||
var kind: Int8 = 0
|
||||
value.read(&kind, offset: 0, length: 1)
|
||||
if kind == 0 {
|
||||
var maxIncomingReadId: Int32 = 0
|
||||
var maxOutgoingReadId: Int32 = 0
|
||||
var maxKnownId: Int32 = 0
|
||||
var count: Int32 = 0
|
||||
|
||||
value.read(&maxIncomingReadId, offset: 0, length: 4)
|
||||
value.read(&maxOutgoingReadId, offset: 0, length: 4)
|
||||
value.read(&maxKnownId, offset: 0, length: 4)
|
||||
value.read(&count, offset: 0, length: 4)
|
||||
|
||||
var flags: Int32 = 0
|
||||
value.read(&flags, offset: 0, length: 4)
|
||||
let markedUnread = (flags & (1 << 0)) != 0
|
||||
|
||||
state = .idBased(maxIncomingReadId: maxIncomingReadId, maxOutgoingReadId: maxOutgoingReadId, maxKnownId: maxKnownId, count: count, markedUnread: markedUnread)
|
||||
} else {
|
||||
var maxIncomingReadTimestamp: Int32 = 0
|
||||
var maxIncomingReadIdPeerId: Int64 = 0
|
||||
var maxIncomingReadIdNamespace: Int32 = 0
|
||||
var maxIncomingReadIdId: Int32 = 0
|
||||
|
||||
var maxOutgoingReadTimestamp: Int32 = 0
|
||||
var maxOutgoingReadIdPeerId: Int64 = 0
|
||||
var maxOutgoingReadIdNamespace: Int32 = 0
|
||||
var maxOutgoingReadIdId: Int32 = 0
|
||||
|
||||
var count: Int32 = 0
|
||||
|
||||
value.read(&maxIncomingReadTimestamp, offset: 0, length: 4)
|
||||
value.read(&maxIncomingReadIdPeerId, offset: 0, length: 8)
|
||||
value.read(&maxIncomingReadIdNamespace, offset: 0, length: 4)
|
||||
value.read(&maxIncomingReadIdId, offset: 0, length: 4)
|
||||
|
||||
value.read(&maxOutgoingReadTimestamp, offset: 0, length: 4)
|
||||
value.read(&maxOutgoingReadIdPeerId, offset: 0, length: 8)
|
||||
value.read(&maxOutgoingReadIdNamespace, offset: 0, length: 4)
|
||||
value.read(&maxOutgoingReadIdId, offset: 0, length: 4)
|
||||
|
||||
value.read(&count, offset: 0, length: 4)
|
||||
|
||||
var flags: Int32 = 0
|
||||
value.read(&flags, offset: 0, length: 4)
|
||||
let markedUnread = (flags & (1 << 0)) != 0
|
||||
|
||||
state = .indexBased(maxIncomingReadIndex: MessageIndex(id: MessageId(peerId: PeerId(maxIncomingReadIdPeerId), namespace: maxIncomingReadIdNamespace, id: maxIncomingReadIdId), timestamp: maxIncomingReadTimestamp), maxOutgoingReadIndex: MessageIndex(id: MessageId(peerId: PeerId(maxOutgoingReadIdPeerId), namespace: maxOutgoingReadIdNamespace, id: maxOutgoingReadIdId), timestamp: maxOutgoingReadTimestamp), count: count, markedUnread: markedUnread)
|
||||
}
|
||||
stateByNamespace[namespaceId] = state
|
||||
}
|
||||
let states = InternalPeerReadStates(namespaces: stateByNamespace)
|
||||
self.cachedPeerReadStates[id] = states
|
||||
return states
|
||||
} else {
|
||||
self.cachedPeerReadStates[id] = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getCombinedState(_ peerId: PeerId) -> CombinedPeerReadState? {
|
||||
if let states = self.get(peerId) {
|
||||
return CombinedPeerReadState(states: states.namespaces.map({$0}))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func markReadStatesAsUpdated(_ peerId: PeerId, namespaces: [MessageId.Namespace: PeerReadState]) {
|
||||
if self.updatedInitialPeerReadStates[peerId] == nil {
|
||||
self.updatedInitialPeerReadStates[peerId] = namespaces
|
||||
}
|
||||
}
|
||||
|
||||
func resetStates(_ peerId: PeerId, namespaces: [MessageId.Namespace: PeerReadState]) -> CombinedPeerReadState? {
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] resetStates peerId: \(peerId), namespaces: \(namespaces)")
|
||||
}
|
||||
|
||||
if let states = self.get(peerId) {
|
||||
var updated = false
|
||||
for (namespace, state) in namespaces {
|
||||
if states.namespaces[namespace] == nil || states.namespaces[namespace]! != state {
|
||||
self.markReadStatesAsUpdated(peerId, namespaces: states.namespaces)
|
||||
updated = true
|
||||
}
|
||||
states.namespaces[namespace] = state
|
||||
}
|
||||
if updated {
|
||||
return CombinedPeerReadState(states: states.namespaces.map({$0}))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
self.markReadStatesAsUpdated(peerId, namespaces: [:])
|
||||
let states = InternalPeerReadStates(namespaces: namespaces)
|
||||
self.cachedPeerReadStates[peerId] = states
|
||||
return CombinedPeerReadState(states: states.namespaces.map({$0}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func addIncomingMessages(_ peerId: PeerId, indices: Set<MessageIndex>) -> (CombinedPeerReadState?, Bool) {
|
||||
var indicesByNamespace: [MessageId.Namespace: [MessageIndex]] = [:]
|
||||
for index in indices {
|
||||
if indicesByNamespace[index.id.namespace] != nil {
|
||||
indicesByNamespace[index.id.namespace]!.append(index)
|
||||
} else {
|
||||
indicesByNamespace[index.id.namespace] = [index]
|
||||
}
|
||||
}
|
||||
|
||||
if let states = self.get(peerId) {
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] addIncomingMessages peerId: \(peerId), indices: \(indices) (before: \(states.namespaces))")
|
||||
}
|
||||
|
||||
var updated = false
|
||||
let invalidated = false
|
||||
for (namespace, namespaceIndices) in indicesByNamespace {
|
||||
let currentState = states.namespaces[namespace] ?? self.seedConfiguration.defaultMessageNamespaceReadStates[namespace]
|
||||
|
||||
if let currentState = currentState {
|
||||
var addedUnreadCount: Int32 = 0
|
||||
for index in namespaceIndices {
|
||||
switch currentState {
|
||||
case let .idBased(maxIncomingReadId, _, maxKnownId, _, _):
|
||||
if index.id.id > maxKnownId && index.id.id > maxIncomingReadId {
|
||||
addedUnreadCount += 1
|
||||
}
|
||||
case let .indexBased(maxIncomingReadIndex, _, _, _):
|
||||
if index > maxIncomingReadIndex {
|
||||
addedUnreadCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if addedUnreadCount != 0 {
|
||||
self.markReadStatesAsUpdated(peerId, namespaces: states.namespaces)
|
||||
|
||||
states.namespaces[namespace] = currentState.withAddedCount(addedUnreadCount)
|
||||
updated = true
|
||||
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] added \(addedUnreadCount)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (updated ? CombinedPeerReadState(states: states.namespaces.map({$0})) : nil, invalidated)
|
||||
} else {
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] addIncomingMessages peerId: \(peerId), just invalidated)")
|
||||
}
|
||||
return (nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteMessages(_ peerId: PeerId, indices: [MessageIndex], incomingStatsInIndices: (PeerId, MessageId.Namespace, [MessageIndex]) -> (Int, Bool)) -> (CombinedPeerReadState?, Bool) {
|
||||
var indicesByNamespace: [MessageId.Namespace: [MessageIndex]] = [:]
|
||||
for index in indices {
|
||||
if indicesByNamespace[index.id.namespace] != nil {
|
||||
indicesByNamespace[index.id.namespace]!.append(index)
|
||||
} else {
|
||||
indicesByNamespace[index.id.namespace] = [index]
|
||||
}
|
||||
}
|
||||
|
||||
if let states = self.get(peerId) {
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] deleteMessages peerId: \(peerId), ids: \(indices) (before: \(states.namespaces))")
|
||||
}
|
||||
|
||||
var updated = false
|
||||
var invalidate = false
|
||||
for (namespace, namespaceIndices) in indicesByNamespace {
|
||||
if let currentState = states.namespaces[namespace] {
|
||||
var unreadIndices: [MessageIndex] = []
|
||||
for index in namespaceIndices {
|
||||
if !currentState.isIncomingMessageIndexRead(index) {
|
||||
unreadIndices.append(index)
|
||||
}
|
||||
}
|
||||
|
||||
let (knownCount, holes) = incomingStatsInIndices(peerId, namespace, unreadIndices)
|
||||
if holes {
|
||||
invalidate = true
|
||||
}
|
||||
|
||||
self.markReadStatesAsUpdated(peerId, namespaces: states.namespaces)
|
||||
|
||||
var updatedState = currentState.withAddedCount(Int32(-knownCount))
|
||||
if updatedState.count < 0 {
|
||||
invalidate = true
|
||||
updatedState = currentState.withAddedCount(-updatedState.count)
|
||||
}
|
||||
|
||||
states.namespaces[namespace] = updatedState
|
||||
updated = true
|
||||
} else {
|
||||
invalidate = true
|
||||
}
|
||||
}
|
||||
|
||||
return (updated ? CombinedPeerReadState(states: states.namespaces.map({$0})) : nil, invalidate)
|
||||
} else {
|
||||
return (nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
func applyIncomingMaxReadId(_ messageId: MessageId, incomingStatsInRange: (MessageId.Namespace, MessageId.Id, MessageId.Id) -> (count: Int, holes: Bool), topMessageId: (MessageId.Id, Bool)?) -> (CombinedPeerReadState?, Bool) {
|
||||
if let states = self.get(messageId.peerId), let state = states.namespaces[messageId.namespace] {
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] applyMaxReadId peerId: \(messageId.peerId), maxReadId: \(messageId) (before: \(states.namespaces))")
|
||||
}
|
||||
|
||||
switch state {
|
||||
case let .idBased(maxIncomingReadId, maxOutgoingReadId, maxKnownId, count, markedUnread):
|
||||
if maxIncomingReadId < messageId.id || (topMessageId != nil && (messageId.id == topMessageId!.0 || topMessageId!.1) && state.count != 0) || markedUnread {
|
||||
var (deltaCount, holes) = incomingStatsInRange(messageId.namespace, maxIncomingReadId + 1, messageId.id)
|
||||
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] applyMaxReadId after deltaCount: \(deltaCount), holes: \(holes)")
|
||||
}
|
||||
|
||||
if let topMessageId = topMessageId, (messageId.id == topMessageId.0 || topMessageId.1) {
|
||||
if deltaCount != Int(state.count) {
|
||||
deltaCount = Int(state.count)
|
||||
holes = true
|
||||
}
|
||||
}
|
||||
|
||||
self.markReadStatesAsUpdated(messageId.peerId, namespaces: states.namespaces)
|
||||
|
||||
states.namespaces[messageId.namespace] = .idBased(maxIncomingReadId: messageId.id, maxOutgoingReadId: maxOutgoingReadId, maxKnownId: maxKnownId, count: max(0, count - Int32(deltaCount)), markedUnread: false)
|
||||
return (CombinedPeerReadState(states: states.namespaces.map({$0})), holes)
|
||||
}
|
||||
case .indexBased:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return (nil, true)
|
||||
}
|
||||
|
||||
return (nil, false)
|
||||
}
|
||||
|
||||
func applyIncomingMaxReadIndex(_ messageIndex: MessageIndex, topMessageIndex: MessageIndex?, incomingStatsInRange: (MessageIndex, MessageIndex) -> (count: Int, holes: Bool, readMesageIds: [MessageId])) -> (CombinedPeerReadState?, Bool, [MessageId]) {
|
||||
if let states = self.get(messageIndex.id.peerId), let state = states.namespaces[messageIndex.id.namespace] {
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] applyIncomingMaxReadIndex peerId: \(messageIndex.id.peerId), maxReadIndex: \(messageIndex) (before: \(states.namespaces))")
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .idBased:
|
||||
assertionFailure()
|
||||
case let .indexBased(maxIncomingReadIndex, maxOutgoingReadIndex, count, markedUnread):
|
||||
var readPastTopIndex = false
|
||||
if let topMessageIndex = topMessageIndex, messageIndex >= topMessageIndex && count != 0 {
|
||||
readPastTopIndex = true
|
||||
}
|
||||
if maxIncomingReadIndex < messageIndex || markedUnread || readPastTopIndex {
|
||||
let (realDeltaCount, holes, messageIds) = incomingStatsInRange(maxIncomingReadIndex.successor(), messageIndex)
|
||||
var deltaCount = realDeltaCount
|
||||
if readPastTopIndex {
|
||||
deltaCount = max(Int(count), deltaCount)
|
||||
}
|
||||
|
||||
if traceReadStates {
|
||||
print("[ReadStateTable] applyIncomingMaxReadIndex after deltaCount: \(deltaCount), holes: \(holes)")
|
||||
}
|
||||
|
||||
self.markReadStatesAsUpdated(messageIndex.id.peerId, namespaces: states.namespaces)
|
||||
|
||||
states.namespaces[messageIndex.id.namespace] = .indexBased(maxIncomingReadIndex: messageIndex, maxOutgoingReadIndex: maxOutgoingReadIndex, count: max(0, count - Int32(deltaCount)), markedUnread: false)
|
||||
return (CombinedPeerReadState(states: states.namespaces.map({$0})), holes, messageIds)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (nil, true, [])
|
||||
}
|
||||
|
||||
return (nil, false, [])
|
||||
}
|
||||
|
||||
func applyOutgoingMaxReadId(_ messageId: MessageId) -> (CombinedPeerReadState?, Bool) {
|
||||
if let states = self.get(messageId.peerId), let state = states.namespaces[messageId.namespace] {
|
||||
switch state {
|
||||
case let .idBased(maxIncomingReadId, maxOutgoingReadId, maxKnownId, count, markedUnread):
|
||||
if maxOutgoingReadId < messageId.id {
|
||||
self.markReadStatesAsUpdated(messageId.peerId, namespaces: states.namespaces)
|
||||
states.namespaces[messageId.namespace] = .idBased(maxIncomingReadId: maxIncomingReadId, maxOutgoingReadId: messageId.id, maxKnownId: maxKnownId, count: count, markedUnread: markedUnread)
|
||||
return (CombinedPeerReadState(states: states.namespaces.map({$0})), false)
|
||||
}
|
||||
case .indexBased:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return (nil, true)
|
||||
}
|
||||
|
||||
return (nil, false)
|
||||
}
|
||||
|
||||
func applyOutgoingMaxReadIndex(_ messageIndex: MessageIndex, outgoingIndexStatsInRange: (MessageIndex, MessageIndex) -> [MessageId]) -> (CombinedPeerReadState?, Bool, [MessageId]) {
|
||||
if let states = self.get(messageIndex.id.peerId), let state = states.namespaces[messageIndex.id.namespace] {
|
||||
switch state {
|
||||
case .idBased:
|
||||
assertionFailure()
|
||||
break
|
||||
case let .indexBased(maxIncomingReadIndex, maxOutgoingReadIndex, count, markedUnread):
|
||||
if maxOutgoingReadIndex < messageIndex {
|
||||
let messageIds: [MessageId] = outgoingIndexStatsInRange(maxOutgoingReadIndex.successor(), messageIndex)
|
||||
|
||||
self.markReadStatesAsUpdated(messageIndex.id.peerId, namespaces: states.namespaces)
|
||||
states.namespaces[messageIndex.id.namespace] = .indexBased(maxIncomingReadIndex: maxIncomingReadIndex, maxOutgoingReadIndex: messageIndex, count: count, markedUnread: markedUnread)
|
||||
return (CombinedPeerReadState(states: states.namespaces.map({$0})), false, messageIds)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (nil, true, [])
|
||||
}
|
||||
|
||||
return (nil, false, [])
|
||||
}
|
||||
|
||||
func applyInteractiveMaxReadIndex(_ messageIndex: MessageIndex, incomingStatsInRange: (MessageId.Namespace, MessageId.Id, MessageId.Id) -> (count: Int, holes: Bool), incomingIndexStatsInRange: (MessageIndex, MessageIndex) -> (count: Int, holes: Bool, readMesageIds: [MessageId]), topMessageId: (MessageId.Id, Bool)?, topMessageIndexByNamespace: (MessageId.Namespace) -> MessageIndex?) -> (combinedState: CombinedPeerReadState?, ApplyInteractiveMaxReadIdResult, readMesageIds: [MessageId]) {
|
||||
if let states = self.get(messageIndex.id.peerId) {
|
||||
if let state = states.namespaces[messageIndex.id.namespace] {
|
||||
switch state {
|
||||
case .idBased:
|
||||
let (combinedState, holes) = self.applyIncomingMaxReadId(messageIndex.id, incomingStatsInRange: incomingStatsInRange, topMessageId: topMessageId)
|
||||
|
||||
if let combinedState = combinedState {
|
||||
return (combinedState, .Push(thenSync: holes), [])
|
||||
}
|
||||
|
||||
return (combinedState, holes ? .Push(thenSync: true) : .None, [])
|
||||
case .indexBased:
|
||||
let topMessageIndex: MessageIndex? = topMessageIndexByNamespace(messageIndex.id.namespace)
|
||||
let (combinedState, holes, messageIds) = self.applyIncomingMaxReadIndex(messageIndex, topMessageIndex: topMessageIndex, incomingStatsInRange: incomingIndexStatsInRange)
|
||||
|
||||
if let combinedState = combinedState {
|
||||
return (combinedState, .Push(thenSync: holes), messageIds)
|
||||
}
|
||||
|
||||
return (combinedState, holes ? .Push(thenSync: true) : .None, messageIds)
|
||||
}
|
||||
} else {
|
||||
for (namespace, state) in states.namespaces {
|
||||
if let topIndex = topMessageIndexByNamespace(namespace), topIndex <= messageIndex {
|
||||
switch state {
|
||||
case .idBased:
|
||||
let (combinedState, holes) = self.applyIncomingMaxReadId(topIndex.id, incomingStatsInRange: incomingStatsInRange, topMessageId: nil)
|
||||
|
||||
if let combinedState = combinedState {
|
||||
return (combinedState, .Push(thenSync: holes), [])
|
||||
}
|
||||
|
||||
return (combinedState, holes ? .Push(thenSync: true) : .None, [])
|
||||
case .indexBased:
|
||||
let (combinedState, holes, messageIds) = self.applyIncomingMaxReadIndex(topIndex, topMessageIndex: topMessageIndexByNamespace(namespace), incomingStatsInRange: incomingIndexStatsInRange)
|
||||
|
||||
if let combinedState = combinedState {
|
||||
return (combinedState, .Push(thenSync: holes), messageIds)
|
||||
}
|
||||
|
||||
return (combinedState, holes ? .Push(thenSync: true) : .None, messageIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
return (nil, .Push(thenSync: true), [])
|
||||
}
|
||||
} else {
|
||||
return (nil, .Push(thenSync: true), [])
|
||||
}
|
||||
}
|
||||
|
||||
func applyInteractiveMarkUnread(peerId: PeerId, namespace: MessageId.Namespace, value: Bool) -> CombinedPeerReadState? {
|
||||
if let states = self.get(peerId), let state = states.namespaces[namespace] {
|
||||
switch state {
|
||||
case let .idBased(maxIncomingReadId, maxOutgoingReadId, maxKnownId, count, markedUnread):
|
||||
if markedUnread != value {
|
||||
self.markReadStatesAsUpdated(peerId, namespaces: states.namespaces)
|
||||
|
||||
states.namespaces[namespace] = .idBased(maxIncomingReadId: maxIncomingReadId, maxOutgoingReadId: maxOutgoingReadId, maxKnownId: maxKnownId, count: count, markedUnread: value)
|
||||
return CombinedPeerReadState(states: states.namespaces.map({$0}))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case let .indexBased(maxIncomingReadIndex, maxOutgoingReadIndex, count, markedUnread):
|
||||
if markedUnread != value {
|
||||
self.markReadStatesAsUpdated(peerId, namespaces: states.namespaces)
|
||||
|
||||
states.namespaces[namespace] = .indexBased(maxIncomingReadIndex: maxIncomingReadIndex, maxOutgoingReadIndex: maxOutgoingReadIndex, count: count, markedUnread: value)
|
||||
return CombinedPeerReadState(states: states.namespaces.map({$0}))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func transactionUnreadCountDeltas() -> [PeerId: Int32] {
|
||||
var deltas: [PeerId: Int32] = [:]
|
||||
for (id, initialNamespaces) in self.updatedInitialPeerReadStates {
|
||||
var initialCount: Int32 = 0
|
||||
for (_, state) in initialNamespaces {
|
||||
initialCount += state.count
|
||||
}
|
||||
|
||||
var updatedCount: Int32 = 0
|
||||
if let maybeStates = self.cachedPeerReadStates[id] {
|
||||
if let states = maybeStates {
|
||||
for (_, state) in states.namespaces {
|
||||
updatedCount += state.count
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
if initialCount != updatedCount {
|
||||
deltas[id] = updatedCount - initialCount
|
||||
}
|
||||
}
|
||||
return deltas
|
||||
}
|
||||
|
||||
func transactionAlteredInitialPeerCombinedReadStates() -> [PeerId: CombinedPeerReadState] {
|
||||
var result: [PeerId: CombinedPeerReadState] = [:]
|
||||
for (peerId, namespacesAndStates) in self.updatedInitialPeerReadStates {
|
||||
var states: [(MessageId.Namespace, PeerReadState)] = []
|
||||
for (namespace, state) in namespacesAndStates {
|
||||
states.append((namespace, state))
|
||||
}
|
||||
result[peerId] = CombinedPeerReadState(states: states)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedPeerReadStates.removeAll()
|
||||
assert(self.updatedInitialPeerReadStates.isEmpty)
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if !self.updatedInitialPeerReadStates.isEmpty {
|
||||
let sharedBuffer = WriteBuffer()
|
||||
for (id, initialNamespaces) in self.updatedInitialPeerReadStates {
|
||||
if let wrappedStates = self.cachedPeerReadStates[id], let states = wrappedStates {
|
||||
sharedBuffer.reset()
|
||||
var count: Int32 = Int32(states.namespaces.count)
|
||||
sharedBuffer.write(&count, offset: 0, length: 4)
|
||||
for (namespace, state) in states.namespaces {
|
||||
var namespaceId: Int32 = namespace
|
||||
sharedBuffer.write(&namespaceId, offset: 0, length: 4)
|
||||
|
||||
switch state {
|
||||
case .idBased(var maxIncomingReadId, var maxOutgoingReadId, var maxKnownId, var count, let markedUnread):
|
||||
var kind: Int8 = 0
|
||||
sharedBuffer.write(&kind, offset: 0, length: 1)
|
||||
|
||||
sharedBuffer.write(&maxIncomingReadId, offset: 0, length: 4)
|
||||
sharedBuffer.write(&maxOutgoingReadId, offset: 0, length: 4)
|
||||
sharedBuffer.write(&maxKnownId, offset: 0, length: 4)
|
||||
sharedBuffer.write(&count, offset: 0, length: 4)
|
||||
var flags: Int32 = 0
|
||||
if markedUnread {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
sharedBuffer.write(&flags, offset: 0, length: 4)
|
||||
case .indexBased(let maxIncomingReadIndex, let maxOutgoingReadIndex, var count, let markedUnread):
|
||||
var kind: Int8 = 1
|
||||
sharedBuffer.write(&kind, offset: 0, length: 1)
|
||||
|
||||
var maxIncomingReadTimestamp: Int32 = maxIncomingReadIndex.timestamp
|
||||
var maxIncomingReadIdPeerId: Int64 = maxIncomingReadIndex.id.peerId.toInt64()
|
||||
var maxIncomingReadIdNamespace: Int32 = maxIncomingReadIndex.id.namespace
|
||||
var maxIncomingReadIdId: Int32 = maxIncomingReadIndex.id.id
|
||||
|
||||
var maxOutgoingReadTimestamp: Int32 = maxOutgoingReadIndex.timestamp
|
||||
var maxOutgoingReadIdPeerId: Int64 = maxOutgoingReadIndex.id.peerId.toInt64()
|
||||
var maxOutgoingReadIdNamespace: Int32 = maxOutgoingReadIndex.id.namespace
|
||||
var maxOutgoingReadIdId: Int32 = maxOutgoingReadIndex.id.id
|
||||
|
||||
sharedBuffer.write(&maxIncomingReadTimestamp, offset: 0, length: 4)
|
||||
sharedBuffer.write(&maxIncomingReadIdPeerId, offset: 0, length: 8)
|
||||
sharedBuffer.write(&maxIncomingReadIdNamespace, offset: 0, length: 4)
|
||||
sharedBuffer.write(&maxIncomingReadIdId, offset: 0, length: 4)
|
||||
|
||||
sharedBuffer.write(&maxOutgoingReadTimestamp, offset: 0, length: 4)
|
||||
sharedBuffer.write(&maxOutgoingReadIdPeerId, offset: 0, length: 8)
|
||||
sharedBuffer.write(&maxOutgoingReadIdNamespace, offset: 0, length: 4)
|
||||
sharedBuffer.write(&maxOutgoingReadIdId, offset: 0, length: 4)
|
||||
|
||||
sharedBuffer.write(&count, offset: 0, length: 4)
|
||||
|
||||
var flags: Int32 = 0
|
||||
if markedUnread {
|
||||
flags |= 1 << 0
|
||||
}
|
||||
sharedBuffer.write(&flags, offset: 0, length: 4)
|
||||
}
|
||||
}
|
||||
self.valueBox.set(self.table, key: self.key(id), value: sharedBuffer)
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: self.key(id), secure: false)
|
||||
}
|
||||
}
|
||||
self.updatedInitialPeerReadStates.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import Foundation
|
||||
|
||||
public enum PeerReadStateSynchronizationOperation: Equatable {
|
||||
case Push(state: CombinedPeerReadState?, thenSync: Bool)
|
||||
case Validate
|
||||
}
|
||||
|
||||
final class MessageHistorySynchronizeReadStateTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8)
|
||||
|
||||
private func key(peerId: PeerId) -> ValueBoxKey {
|
||||
self.sharedKey.setInt64(0, value: peerId.toInt64())
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private var updatedPeerIds: [PeerId: PeerReadStateSynchronizationOperation?] = [:]
|
||||
|
||||
private func lowerBound() -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8)
|
||||
key.setInt64(0, value: 0)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound() -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8)
|
||||
memset(key.memory, 0xff, key.length)
|
||||
return key
|
||||
}
|
||||
|
||||
func set(_ peerId: PeerId, operation: PeerReadStateSynchronizationOperation?, operations: inout [PeerId: PeerReadStateSynchronizationOperation?]) {
|
||||
self.updatedPeerIds[peerId] = operation
|
||||
operations[peerId] = operation
|
||||
}
|
||||
|
||||
func get(getCombinedPeerReadState: (PeerId) -> CombinedPeerReadState?) -> [PeerId: PeerReadStateSynchronizationOperation] {
|
||||
self.beforeCommit()
|
||||
|
||||
var operations: [PeerId: PeerReadStateSynchronizationOperation] = [:]
|
||||
self.valueBox.range(self.table, start: self.lowerBound(), end: self.upperBound(), values: { key, value in
|
||||
let peerId = PeerId(key.getInt64(0))
|
||||
var operationValue: Int8 = 0
|
||||
value.read(&operationValue, offset: 0, length: 1)
|
||||
|
||||
let operation: PeerReadStateSynchronizationOperation
|
||||
if operationValue == 0 {
|
||||
var syncValue: Int8 = 0
|
||||
value.read(&syncValue, offset: 0, length: 1)
|
||||
operation = .Push(state: getCombinedPeerReadState(peerId), thenSync: syncValue != 0)
|
||||
} else {
|
||||
operation = .Validate
|
||||
}
|
||||
|
||||
operations[peerId] = operation
|
||||
return true
|
||||
}, limit: 0)
|
||||
return operations
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if !self.updatedPeerIds.isEmpty {
|
||||
let key = ValueBoxKey(length: 8)
|
||||
let buffer = WriteBuffer()
|
||||
for (peerId, operation) in self.updatedPeerIds {
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
if let operation = operation {
|
||||
buffer.reset()
|
||||
switch operation {
|
||||
case let .Push(_, thenSync):
|
||||
var operationValue: Int8 = 0
|
||||
buffer.write(&operationValue, offset: 0, length: 1)
|
||||
var syncValue: Int8 = thenSync ? 1 : 0
|
||||
buffer.write(&syncValue, offset: 0, length: 1)
|
||||
case .Validate:
|
||||
var operationValue: Int8 = 1
|
||||
buffer.write(&operationValue, offset: 0, length: 1)
|
||||
}
|
||||
|
||||
self.valueBox.set(self.table, key: key, value: buffer)
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: key, secure: false)
|
||||
}
|
||||
}
|
||||
self.updatedPeerIds.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
2585
submodules/Postbox/Postbox/MessageHistoryTable.swift
Normal file
2585
submodules/Postbox/Postbox/MessageHistoryTable.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,40 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableMessageHistoryTagSummaryView: MutablePostboxView {
|
||||
private let tag: MessageTags
|
||||
private let peerId: PeerId
|
||||
private let namespace: MessageId.Namespace
|
||||
|
||||
fileprivate var count: Int32?
|
||||
|
||||
init(postbox: Postbox, tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace) {
|
||||
self.tag = tag
|
||||
self.peerId = peerId
|
||||
self.namespace = namespace
|
||||
|
||||
self.count = postbox.messageHistoryTagsSummaryTable.get(MessageHistoryTagsSummaryKey(tag: tag, peerId: peerId, namespace: namespace))?.count
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var hasChanges = false
|
||||
|
||||
if let summary = transaction.currentUpdatedMessageTagSummaries[MessageHistoryTagsSummaryKey(tag: self.tag, peerId: self.peerId, namespace: self.namespace)] {
|
||||
self.count = summary.count
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
return hasChanges
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return MessageHistoryTagSummaryView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class MessageHistoryTagSummaryView: PostboxView {
|
||||
public let count: Int32?
|
||||
|
||||
init(_ view: MutableMessageHistoryTagSummaryView) {
|
||||
self.count = view.count
|
||||
}
|
||||
}
|
||||
176
submodules/Postbox/Postbox/MessageHistoryTagsSummaryTable.swift
Normal file
176
submodules/Postbox/Postbox/MessageHistoryTagsSummaryTable.swift
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import Foundation
|
||||
|
||||
public struct MessageHistoryTagNamespaceCountValidityRange: Equatable {
|
||||
public let maxId: MessageId.Id
|
||||
|
||||
public init(maxId: MessageId.Id) {
|
||||
self.maxId = maxId
|
||||
}
|
||||
|
||||
public static func ==(lhs: MessageHistoryTagNamespaceCountValidityRange, rhs: MessageHistoryTagNamespaceCountValidityRange) -> Bool {
|
||||
return lhs.maxId == rhs.maxId
|
||||
}
|
||||
|
||||
public func contains(_ id: MessageId.Id) -> Bool {
|
||||
return id <= self.maxId
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageHistoryTagNamespaceSummary: Equatable, CustomStringConvertible {
|
||||
public let version: Int32
|
||||
public let count: Int32
|
||||
public let range: MessageHistoryTagNamespaceCountValidityRange
|
||||
|
||||
public init(version: Int32, count: Int32, range: MessageHistoryTagNamespaceCountValidityRange) {
|
||||
self.version = version
|
||||
self.count = count
|
||||
self.range = range
|
||||
}
|
||||
|
||||
public static func ==(lhs: MessageHistoryTagNamespaceSummary, rhs: MessageHistoryTagNamespaceSummary) -> Bool {
|
||||
return lhs.version == rhs.version && lhs.count == rhs.count && lhs.range == rhs.range
|
||||
}
|
||||
|
||||
func withAddedCount(_ value: Int32) -> MessageHistoryTagNamespaceSummary {
|
||||
return MessageHistoryTagNamespaceSummary(version: self.version, count: self.count + value, range: self.range)
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "(version: \(self.version), count: \(self.count), range: (maxId: \(self.range.maxId)))"
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageHistoryTagsSummaryKey: Equatable, Hashable {
|
||||
let tag: MessageTags
|
||||
let peerId: PeerId
|
||||
let namespace: MessageId.Namespace
|
||||
}
|
||||
|
||||
private func readSummary(_ value: ReadBuffer) -> MessageHistoryTagNamespaceSummary {
|
||||
var versionValue: Int32 = 0
|
||||
value.read(&versionValue, offset: 0, length: 4)
|
||||
var countValue: Int32 = 0
|
||||
value.read(&countValue, offset: 0, length: 4)
|
||||
var maxIdValue: Int32 = 0
|
||||
value.read(&maxIdValue, offset: 0, length: 4)
|
||||
|
||||
return MessageHistoryTagNamespaceSummary(version: versionValue, count: countValue, range: MessageHistoryTagNamespaceCountValidityRange(maxId: maxIdValue))
|
||||
}
|
||||
|
||||
private func writeSummary(_ summary: MessageHistoryTagNamespaceSummary, to buffer: WriteBuffer) {
|
||||
var versionValue: Int32 = summary.version
|
||||
buffer.write(&versionValue, offset: 0, length: 4)
|
||||
var countValue: Int32 = summary.count
|
||||
buffer.write(&countValue, offset: 0, length: 4)
|
||||
var maxIdValue: Int32 = summary.range.maxId
|
||||
buffer.write(&maxIdValue, offset: 0, length: 4)
|
||||
}
|
||||
|
||||
private struct CachedEntry {
|
||||
let summary: MessageHistoryTagNamespaceSummary?
|
||||
}
|
||||
|
||||
class MessageHistoryTagsSummaryTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let invalidateTable: InvalidatedMessageHistoryTagsSummaryTable
|
||||
|
||||
private var cachedSummaries: [MessageHistoryTagsSummaryKey: CachedEntry] = [:]
|
||||
private var updatedKeys = Set<MessageHistoryTagsSummaryKey>()
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 4 + 8 + 4)
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, invalidateTable: InvalidatedMessageHistoryTagsSummaryTable) {
|
||||
self.invalidateTable = invalidateTable
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(key: MessageHistoryTagsSummaryKey, sharedKey: ValueBoxKey = ValueBoxKey(length: 4 + 8 + 4)) -> ValueBoxKey {
|
||||
sharedKey.setUInt32(0, value: key.tag.rawValue)
|
||||
sharedKey.setInt64(4, value: key.peerId.toInt64())
|
||||
sharedKey.setInt32(4 + 8, value: key.namespace)
|
||||
return sharedKey
|
||||
}
|
||||
|
||||
func get(_ key: MessageHistoryTagsSummaryKey) -> MessageHistoryTagNamespaceSummary? {
|
||||
if let cached = self.cachedSummaries[key] {
|
||||
return cached.summary
|
||||
} else if let value = self.valueBox.get(self.table, key: self.key(key: key, sharedKey: self.sharedKey)) {
|
||||
let entry = readSummary(value)
|
||||
self.cachedSummaries[key] = CachedEntry(summary: entry)
|
||||
return entry
|
||||
} else {
|
||||
self.cachedSummaries[key] = CachedEntry(summary: nil)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func set(_ key: MessageHistoryTagsSummaryKey, summary: MessageHistoryTagNamespaceSummary, updatedSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary]) {
|
||||
if self.get(key) != summary {
|
||||
self.updatedKeys.insert(key)
|
||||
self.cachedSummaries[key] = CachedEntry(summary: summary)
|
||||
updatedSummaries[key] = summary
|
||||
}
|
||||
}
|
||||
|
||||
func addMessage(key: MessageHistoryTagsSummaryKey, id: MessageId.Id, updatedSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation]) {
|
||||
if let current = self.get(key) {
|
||||
if !current.range.contains(id) {
|
||||
self.set(key, summary: current.withAddedCount(1), updatedSummaries: &updatedSummaries)
|
||||
if current.range.maxId == 0 {
|
||||
self.invalidateTable.insert(InvalidatedMessageHistoryTagsSummaryKey(peerId: key.peerId, namespace: key.namespace, tagMask: key.tag), operations: &invalidateSummaries)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.set(key, summary: MessageHistoryTagNamespaceSummary(version: 0, count: 1, range: MessageHistoryTagNamespaceCountValidityRange(maxId: 0)), updatedSummaries: &updatedSummaries)
|
||||
self.invalidateTable.insert(InvalidatedMessageHistoryTagsSummaryKey(peerId: key.peerId, namespace: key.namespace, tagMask: key.tag), operations: &invalidateSummaries)
|
||||
}
|
||||
}
|
||||
|
||||
func removeMessage(key: MessageHistoryTagsSummaryKey, id: MessageId.Id, updatedSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation]) {
|
||||
if let current = self.get(key) {
|
||||
if current.count == 0 {
|
||||
self.invalidateTable.insert(InvalidatedMessageHistoryTagsSummaryKey(peerId: key.peerId, namespace: key.namespace, tagMask: key.tag), operations: &invalidateSummaries)
|
||||
} else {
|
||||
self.set(key, summary: current.withAddedCount(-1), updatedSummaries: &updatedSummaries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replace(key: MessageHistoryTagsSummaryKey, count: Int32, maxId: MessageId.Id, updatedSummaries: inout [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary]) {
|
||||
var version: Int32 = 0
|
||||
if let current = self.get(key) {
|
||||
version = current.version + 1
|
||||
}
|
||||
self.set(key, summary: MessageHistoryTagNamespaceSummary(version: version, count: count, range: MessageHistoryTagNamespaceCountValidityRange(maxId: maxId)), updatedSummaries: &updatedSummaries)
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedSummaries.removeAll()
|
||||
assert(self.updatedKeys.isEmpty)
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if !self.updatedKeys.isEmpty {
|
||||
let buffer = WriteBuffer()
|
||||
for key in self.updatedKeys {
|
||||
if let cached = self.cachedSummaries[key] {
|
||||
if let summary = cached.summary {
|
||||
buffer.reset()
|
||||
writeSummary(summary, to: buffer)
|
||||
self.valueBox.set(self.table, key: self.key(key: key, sharedKey: self.sharedKey), value: buffer)
|
||||
} else {
|
||||
assertionFailure()
|
||||
self.valueBox.remove(self.table, key: self.key(key: key, sharedKey: self.sharedKey), secure: false)
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
self.updatedKeys.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
177
submodules/Postbox/Postbox/MessageHistoryTagsTable.swift
Normal file
177
submodules/Postbox/Postbox/MessageHistoryTagsTable.swift
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import Foundation
|
||||
|
||||
private func extractKey(_ key: ValueBoxKey) -> MessageIndex {
|
||||
return MessageIndex(id: MessageId(peerId: PeerId(key.getInt64(0)), namespace: key.getInt32(8 + 4), id: key.getInt32(8 + 4 + 4 + 4)), timestamp: key.getInt32(8 + 4 + 4))
|
||||
}
|
||||
|
||||
class MessageHistoryTagsTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 8 + 4 + 4 + 4 + 4)
|
||||
|
||||
private let summaryTable: MessageHistoryTagsSummaryTable
|
||||
private let summaryTags: MessageTags
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxTable, seedConfiguration: SeedConfiguration, summaryTable: MessageHistoryTagsSummaryTable) {
|
||||
self.summaryTable = summaryTable
|
||||
self.summaryTags = seedConfiguration.messageTagsWithSummary
|
||||
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(tag: MessageTags, index: MessageIndex, key: ValueBoxKey = ValueBoxKey(length: 8 + 4 + 4 + 4 + 4)) -> ValueBoxKey {
|
||||
key.setInt64(0, value: index.id.peerId.toInt64())
|
||||
key.setUInt32(8, value: tag.rawValue)
|
||||
key.setInt32(8 + 4, value: index.id.namespace)
|
||||
key.setInt32(8 + 4 + 4, value: index.timestamp)
|
||||
key.setInt32(8 + 4 + 4 + 4, value: index.id.id)
|
||||
return key
|
||||
}
|
||||
|
||||
private func lowerBound(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 8 + 4 + 4)
|
||||
key.setInt64(0, value: peerId.toInt64())
|
||||
key.setUInt32(8, value: tag.rawValue)
|
||||
key.setInt32(8 + 4, value: namespace)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace) -> ValueBoxKey {
|
||||
return self.lowerBound(tag: tag, peerId: peerId, namespace: namespace).successor
|
||||
}
|
||||
|
||||
func add(tags: MessageTags, index: MessageIndex, updatedSummaries: inout[MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation]) {
|
||||
for tag in tags {
|
||||
self.valueBox.set(self.table, key: self.key(tag: tag, index: index, key: self.sharedKey), value: MemoryBuffer())
|
||||
if self.summaryTags.contains(tag) {
|
||||
self.summaryTable.addMessage(key: MessageHistoryTagsSummaryKey(tag: tag, peerId: index.id.peerId, namespace: index.id.namespace), id: index.id.id, updatedSummaries: &updatedSummaries, invalidateSummaries: &invalidateSummaries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remove(tags: MessageTags, index: MessageIndex, updatedSummaries: inout[MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], invalidateSummaries: inout [InvalidatedMessageHistoryTagsSummaryEntryOperation]) {
|
||||
for tag in tags {
|
||||
self.valueBox.remove(self.table, key: self.key(tag: tag, index: index, key: self.sharedKey), secure: false)
|
||||
|
||||
if self.summaryTags.contains(tag) {
|
||||
self.summaryTable.removeMessage(key: MessageHistoryTagsSummaryKey(tag: tag, peerId: index.id.peerId, namespace: index.id.namespace), id: index.id.id, updatedSummaries: &updatedSummaries, invalidateSummaries: &invalidateSummaries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func entryLocation(at index: MessageIndex, tag: MessageTags) -> MessageHistoryEntryLocation? {
|
||||
if let _ = self.valueBox.get(self.table, key: self.key(tag: tag, index: index)) {
|
||||
var greaterCount = 0
|
||||
self.valueBox.range(self.table, start: self.key(tag: tag, index: index), end: self.upperBound(tag: tag, peerId: index.id.peerId, namespace: index.id.namespace), keys: { _ in
|
||||
greaterCount += 1
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
var lowerCount = 0
|
||||
self.valueBox.range(self.table, start: self.key(tag: tag, index: index), end: self.lowerBound(tag: tag, peerId: index.id.peerId, namespace: index.id.namespace), keys: { _ in
|
||||
lowerCount += 1
|
||||
return true
|
||||
}, limit: 0)
|
||||
|
||||
return MessageHistoryEntryLocation(index: lowerCount, count: greaterCount + lowerCount + 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func earlierIndices(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace, index: MessageIndex?, includeFrom: Bool, count: Int) -> [MessageIndex] {
|
||||
var indices: [MessageIndex] = []
|
||||
let key: ValueBoxKey
|
||||
if let index = index {
|
||||
if includeFrom {
|
||||
key = self.key(tag: tag, index: index).successor
|
||||
} else {
|
||||
key = self.key(tag: tag, index: index)
|
||||
}
|
||||
} else {
|
||||
key = self.upperBound(tag: tag, peerId: peerId, namespace: namespace)
|
||||
}
|
||||
self.valueBox.range(self.table, start: key, end: self.lowerBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in
|
||||
indices.append(extractKey(key))
|
||||
return true
|
||||
}, limit: count)
|
||||
return indices
|
||||
}
|
||||
|
||||
func laterIndices(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace, index: MessageIndex?, includeFrom: Bool, count: Int) -> [MessageIndex] {
|
||||
var indices: [MessageIndex] = []
|
||||
let key: ValueBoxKey
|
||||
if let index = index {
|
||||
if includeFrom {
|
||||
key = self.key(tag: tag, index: index).predecessor
|
||||
} else {
|
||||
key = self.key(tag: tag, index: index)
|
||||
}
|
||||
} else {
|
||||
key = self.lowerBound(tag: tag, peerId: peerId, namespace: namespace)
|
||||
}
|
||||
self.valueBox.range(self.table, start: key, end: self.upperBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in
|
||||
indices.append(extractKey(key))
|
||||
return true
|
||||
}, limit: count)
|
||||
return indices
|
||||
}
|
||||
|
||||
func getMessageCountInRange(tag: MessageTags, peerId: PeerId, namespace: MessageId.Namespace, lowerBound: MessageIndex, upperBound: MessageIndex) -> Int {
|
||||
precondition(lowerBound.id.namespace == namespace)
|
||||
precondition(upperBound.id.namespace == namespace)
|
||||
var lowerBoundKey = self.key(tag: tag, index: lowerBound)
|
||||
if lowerBound.timestamp > 1 {
|
||||
lowerBoundKey = lowerBoundKey.predecessor
|
||||
}
|
||||
var upperBoundKey = self.key(tag: tag, index: upperBound)
|
||||
if upperBound.timestamp < Int32.max - 1 {
|
||||
upperBoundKey = upperBoundKey.successor
|
||||
}
|
||||
return Int(self.valueBox.count(self.table, start: lowerBoundKey, end: upperBoundKey))
|
||||
}
|
||||
|
||||
func findRandomIndex(peerId: PeerId, namespace: MessageId.Namespace, tag: MessageTags, ignoreIds: ([MessageId], Set<MessageId>), isMessage: (MessageIndex) -> Bool) -> MessageIndex? {
|
||||
var indices: [MessageIndex] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(tag: tag, peerId: peerId, namespace: namespace), end: self.upperBound(tag: tag, peerId: peerId, namespace: namespace), keys: { key in
|
||||
indices.append(extractKey(key))
|
||||
return true
|
||||
}, limit: 0)
|
||||
var checkedIndices = Set<Int>()
|
||||
while checkedIndices.count < indices.count {
|
||||
let i = Int(arc4random_uniform(UInt32(indices.count)))
|
||||
if checkedIndices.contains(i) {
|
||||
continue
|
||||
}
|
||||
checkedIndices.insert(i)
|
||||
let index = indices[i]
|
||||
if isMessage(index) && !ignoreIds.1.contains(index.id) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
checkedIndices.removeAll()
|
||||
let lastId = ignoreIds.0.last
|
||||
while checkedIndices.count < indices.count {
|
||||
let i = Int(arc4random_uniform(UInt32(indices.count)))
|
||||
if checkedIndices.contains(i) {
|
||||
continue
|
||||
}
|
||||
checkedIndices.insert(i)
|
||||
let index = indices[i]
|
||||
if isMessage(index) && lastId != index.id {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func debugGetAllIndices() -> [MessageIndex] {
|
||||
var indices: [MessageIndex] = []
|
||||
self.valueBox.scan(self.table, values: { key, value in
|
||||
indices.append(extractKey(key))
|
||||
return true
|
||||
})
|
||||
return indices
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import Foundation
|
||||
|
||||
private func collectionId(_ peerId: PeerId) -> String {
|
||||
return "p\(UInt64(bitPattern: peerId.toInt64()))"
|
||||
}
|
||||
|
||||
private func itemId(_ messageId: MessageId) -> String {
|
||||
return "p\(UInt64(bitPattern: messageId.peerId.toInt64()))a\(UInt32(bitPattern: messageId.namespace))b\(UInt32(bitPattern: messageId.id))"
|
||||
}
|
||||
|
||||
private func messageTags(_ tags: MessageTags) -> String {
|
||||
var result = ""
|
||||
for tag in tags {
|
||||
if !result.isEmpty {
|
||||
result += " "
|
||||
}
|
||||
result += "t\(tag.rawValue)"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func parseMessageId(_ value: String) -> MessageId? {
|
||||
if !value.hasPrefix("p") {
|
||||
return nil
|
||||
}
|
||||
guard let aRange = value.range(of: "a") else {
|
||||
return nil
|
||||
}
|
||||
guard let bRange = value.range(of: "b") else {
|
||||
return nil
|
||||
}
|
||||
let pString = value[value.index(value.startIndex, offsetBy: 1) ..< aRange.lowerBound]
|
||||
let nString = value[aRange.upperBound ..< bRange.lowerBound]
|
||||
let iString = value[bRange.upperBound ..< value.endIndex]
|
||||
|
||||
guard let pValue = UInt64(pString) else {
|
||||
return nil
|
||||
}
|
||||
guard let nValue = UInt32(nString) else {
|
||||
return nil
|
||||
}
|
||||
guard let iValue = UInt32(iString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return MessageId(peerId: PeerId(Int64(bitPattern: pValue)), namespace: Int32(bitPattern: nValue), id: Int32(bitPattern: iValue))
|
||||
}
|
||||
|
||||
private let alphanumerics = CharacterSet.alphanumerics
|
||||
|
||||
final class MessageHistoryTextIndexTable {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxFullTextTable {
|
||||
return ValueBoxFullTextTable(id: id)
|
||||
}
|
||||
|
||||
private let valueBox: ValueBox
|
||||
private let table: ValueBoxFullTextTable
|
||||
|
||||
init(valueBox: ValueBox, table: ValueBoxFullTextTable) {
|
||||
self.valueBox = valueBox
|
||||
self.table = table
|
||||
}
|
||||
|
||||
func add(messageId: MessageId, text: String, tags: MessageTags) {
|
||||
self.valueBox.fullTextSet(self.table, collectionId: collectionId(messageId.peerId), itemId: itemId(messageId), contents: text, tags: messageTags(tags))
|
||||
}
|
||||
|
||||
func remove(messageId: MessageId) {
|
||||
self.valueBox.fullTextRemove(self.table, itemId: itemId(messageId))
|
||||
}
|
||||
|
||||
func search(peerId: PeerId?, text: String, tags: MessageTags?) -> [MessageId] {
|
||||
var escapedText = String(text.map({ c in
|
||||
var codeUnits: [UnicodeScalar] = []
|
||||
for codeUnit in String(c).unicodeScalars {
|
||||
codeUnits.append(codeUnit)
|
||||
}
|
||||
for codeUnit in codeUnits {
|
||||
if !alphanumerics.contains(codeUnit) {
|
||||
return " "
|
||||
}
|
||||
}
|
||||
return c
|
||||
}))
|
||||
if !escapedText.isEmpty {
|
||||
escapedText += "*"
|
||||
}
|
||||
var result: [MessageId] = []
|
||||
self.valueBox.fullTextMatch(self.table, collectionId: peerId.flatMap { collectionId($0) }, query: escapedText, tags: tags.flatMap(messageTags), values: { _, itemId in
|
||||
if let messageId = parseMessageId(itemId) {
|
||||
result.append(messageId)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
return true
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
57
submodules/Postbox/Postbox/MessageHistoryUnsentTable.swift
Normal file
57
submodules/Postbox/Postbox/MessageHistoryUnsentTable.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import Foundation
|
||||
|
||||
enum IntermediateMessageHistoryUnsentOperation {
|
||||
case Insert(MessageId)
|
||||
case Remove(MessageId)
|
||||
}
|
||||
|
||||
final class MessageHistoryUnsentTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
private let sharedKey = ValueBoxKey(length: 4 + 4 + 8)
|
||||
|
||||
private func key(_ id: MessageId) -> ValueBoxKey {
|
||||
self.sharedKey.setInt32(0, value: id.namespace)
|
||||
self.sharedKey.setInt32(4, value: id.id)
|
||||
self.sharedKey.setInt64(4 + 4, value: id.peerId.toInt64())
|
||||
|
||||
return self.sharedKey
|
||||
}
|
||||
|
||||
private func lowerBound() -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 1)
|
||||
key.setInt8(0, value: 0)
|
||||
return key
|
||||
}
|
||||
|
||||
private func upperBound() -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4 + 4 + 8)
|
||||
memset(key.memory, 0xff, key.length)
|
||||
return key
|
||||
}
|
||||
|
||||
func add(_ id: MessageId, operations: inout [IntermediateMessageHistoryUnsentOperation]) {
|
||||
self.valueBox.set(self.table, key: self.key(id), value: MemoryBuffer())
|
||||
operations.append(.Insert(id))
|
||||
}
|
||||
|
||||
func remove(_ id: MessageId, operations: inout [IntermediateMessageHistoryUnsentOperation]) {
|
||||
self.valueBox.remove(self.table, key: self.key(id), secure: false)
|
||||
operations.append(.Remove(id))
|
||||
}
|
||||
|
||||
func get() -> [MessageId] {
|
||||
var ids: [MessageId] = []
|
||||
self.valueBox.range(self.table, start: self.lowerBound(), end: self.upperBound(), keys: { key in
|
||||
ids.append(MessageId(peerId: PeerId(key.getInt64(4 + 4)), namespace: key.getInt32(0), id: key.getInt32(4)))
|
||||
return true
|
||||
}, limit: 0)
|
||||
return ids
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
|
||||
}
|
||||
}
|
||||
1013
submodules/Postbox/Postbox/MessageHistoryView.swift
Normal file
1013
submodules/Postbox/Postbox/MessageHistoryView.swift
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,9 @@
|
|||
import Foundation
|
||||
|
||||
public struct MutableMessageHistoryEntryAttributes: Equatable {
|
||||
public var authorIsContact: Bool
|
||||
|
||||
public init(authorIsContact: Bool) {
|
||||
self.authorIsContact = authorIsContact
|
||||
}
|
||||
}
|
||||
1316
submodules/Postbox/Postbox/MessageHistoryViewState.swift
Normal file
1316
submodules/Postbox/Postbox/MessageHistoryViewState.swift
Normal file
File diff suppressed because it is too large
Load diff
295
submodules/Postbox/Postbox/MessageMediaTable.swift
Normal file
295
submodules/Postbox/Postbox/MessageMediaTable.swift
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
import Foundation
|
||||
|
||||
private enum MediaEntryType: Int8 {
|
||||
case Direct
|
||||
case MessageReference
|
||||
}
|
||||
|
||||
enum InsertMediaResult {
|
||||
case Reference
|
||||
case Embed(Media)
|
||||
}
|
||||
|
||||
enum RemoveMediaResult {
|
||||
case Reference
|
||||
case Embedded(MessageIndex)
|
||||
}
|
||||
|
||||
enum DebugMediaEntry {
|
||||
case Direct(Media, Int)
|
||||
case MessageReference(MessageIndex)
|
||||
}
|
||||
|
||||
final class MessageMediaTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
func key(_ id: MediaId, key: ValueBoxKey = ValueBoxKey(length: 4 + 8)) -> ValueBoxKey {
|
||||
key.setInt32(0, value: id.namespace)
|
||||
key.setInt64(4, value: id.id)
|
||||
return key
|
||||
}
|
||||
|
||||
func get(_ id: MediaId, embedded: (MessageIndex, MediaId) -> Media?) -> (MessageIndex?, Media)? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(id)) {
|
||||
var type: Int8 = 0
|
||||
value.read(&type, offset: 0, length: 1)
|
||||
if type == MediaEntryType.Direct.rawValue {
|
||||
var dataLength: Int32 = 0
|
||||
value.read(&dataLength, offset: 0, length: 4)
|
||||
if let media = PostboxDecoder(buffer: MemoryBuffer(memory: value.memory + value.offset, capacity: Int(dataLength), length: Int(dataLength), freeWhenDone: false)).decodeRootObject() as? Media {
|
||||
return (nil, media)
|
||||
}
|
||||
} else if type == MediaEntryType.MessageReference.rawValue {
|
||||
var idPeerId: Int64 = 0
|
||||
var idNamespace: Int32 = 0
|
||||
var idId: Int32 = 0
|
||||
var idTimestamp: Int32 = 0
|
||||
value.read(&idPeerId, offset: 0, length: 8)
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
value.read(&idTimestamp, offset: 0, length: 4)
|
||||
|
||||
let referencedMessageIndex = MessageIndex(id: MessageId(peerId: PeerId(idPeerId), namespace: idNamespace, id: idId), timestamp: idTimestamp)
|
||||
|
||||
if let result = embedded(referencedMessageIndex, id) {
|
||||
return (referencedMessageIndex, result)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func set(_ media: Media, index: MessageIndex, messageHistoryTable: MessageHistoryTable, sharedWriteBuffer: WriteBuffer = WriteBuffer(), sharedEncoder: PostboxEncoder = PostboxEncoder()) -> InsertMediaResult {
|
||||
if let id = media.id {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(id)) {
|
||||
var type: Int8 = 0
|
||||
value.read(&type, offset: 0, length: 1)
|
||||
if type == MediaEntryType.Direct.rawValue {
|
||||
var dataLength: Int32 = 0
|
||||
value.read(&dataLength, offset: 0, length: 4)
|
||||
value.skip(Int(dataLength))
|
||||
|
||||
sharedWriteBuffer.reset()
|
||||
sharedWriteBuffer.write(value.memory, offset: 0, length: value.offset)
|
||||
|
||||
var messageReferenceCount: Int32 = 0
|
||||
value.read(&messageReferenceCount, offset: 0, length: 4)
|
||||
messageReferenceCount += 1
|
||||
sharedWriteBuffer.write(&messageReferenceCount, offset: 0, length: 4)
|
||||
|
||||
withExtendedLifetime(sharedWriteBuffer, {
|
||||
self.valueBox.set(self.table, key: self.key(id), value: sharedWriteBuffer.readBufferNoCopy())
|
||||
})
|
||||
|
||||
return .Reference
|
||||
} else if type == MediaEntryType.MessageReference.rawValue {
|
||||
var idPeerId: Int64 = 0
|
||||
var idNamespace: Int32 = 0
|
||||
var idId: Int32 = 0
|
||||
var idTimestamp: Int32 = 0
|
||||
value.read(&idPeerId, offset: 0, length: 8)
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
value.read(&idTimestamp, offset: 0, length: 4)
|
||||
|
||||
let referencedMessageIndex = MessageIndex(id: MessageId(peerId: PeerId(idPeerId), namespace: idNamespace, id: idId), timestamp: idTimestamp)
|
||||
if referencedMessageIndex == index {
|
||||
return .Embed(media)
|
||||
}
|
||||
|
||||
if let media = messageHistoryTable.unembedMedia(referencedMessageIndex, id: id) {
|
||||
sharedWriteBuffer.reset()
|
||||
var directType: Int8 = MediaEntryType.Direct.rawValue
|
||||
sharedWriteBuffer.write(&directType, offset: 0, length: 1)
|
||||
|
||||
sharedEncoder.reset()
|
||||
sharedEncoder.encodeRootObject(media)
|
||||
let mediaBuffer = sharedEncoder.memoryBuffer()
|
||||
var mediaBufferLength = Int32(mediaBuffer.length)
|
||||
sharedWriteBuffer.write(&mediaBufferLength, offset: 0, length: 4)
|
||||
sharedWriteBuffer.write(mediaBuffer.memory, offset: 0, length: mediaBuffer.length)
|
||||
|
||||
var messageReferenceCount: Int32 = 2
|
||||
sharedWriteBuffer.write(&messageReferenceCount, offset: 0, length: 4)
|
||||
|
||||
withExtendedLifetime(sharedWriteBuffer, {
|
||||
self.valueBox.set(self.table, key: self.key(id), value: sharedWriteBuffer.readBufferNoCopy())
|
||||
})
|
||||
}
|
||||
|
||||
return .Reference
|
||||
} else {
|
||||
return .Embed(media)
|
||||
}
|
||||
} else {
|
||||
sharedWriteBuffer.reset()
|
||||
var type: Int8 = MediaEntryType.MessageReference.rawValue
|
||||
sharedWriteBuffer.write(&type, offset: 0, length: 1)
|
||||
var idPeerId: Int64 = index.id.peerId.toInt64()
|
||||
var idNamespace: Int32 = index.id.namespace
|
||||
var idId: Int32 = index.id.id
|
||||
var idTimestamp: Int32 = index.timestamp
|
||||
sharedWriteBuffer.write(&idPeerId, offset: 0, length: 8)
|
||||
sharedWriteBuffer.write(&idNamespace, offset: 0, length: 4)
|
||||
sharedWriteBuffer.write(&idId, offset: 0, length: 4)
|
||||
sharedWriteBuffer.write(&idTimestamp, offset: 0, length: 4)
|
||||
|
||||
withExtendedLifetime(sharedWriteBuffer, {
|
||||
self.valueBox.set(self.table, key: self.key(id), value: sharedWriteBuffer.readBufferNoCopy())
|
||||
})
|
||||
|
||||
return .Embed(media)
|
||||
}
|
||||
} else {
|
||||
return .Embed(media)
|
||||
}
|
||||
}
|
||||
|
||||
func removeReference(_ id: MediaId, sharedWriteBuffer: WriteBuffer = WriteBuffer()) -> RemoveMediaResult {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(id)) {
|
||||
var type: Int8 = 0
|
||||
value.read(&type, offset: 0, length: 1)
|
||||
if type == MediaEntryType.Direct.rawValue {
|
||||
var dataLength: Int32 = 0
|
||||
value.read(&dataLength, offset: 0, length: 4)
|
||||
value.skip(Int(dataLength))
|
||||
|
||||
sharedWriteBuffer.reset()
|
||||
sharedWriteBuffer.write(value.memory, offset: 0, length: value.offset)
|
||||
|
||||
var messageReferenceCount: Int32 = 0
|
||||
value.read(&messageReferenceCount, offset: 0, length: 4)
|
||||
messageReferenceCount -= 1
|
||||
sharedWriteBuffer.write(&messageReferenceCount, offset: 0, length: 4)
|
||||
|
||||
if messageReferenceCount <= 0 {
|
||||
self.valueBox.remove(self.table, key: self.key(id), secure: false)
|
||||
} else {
|
||||
withExtendedLifetime(sharedWriteBuffer, {
|
||||
self.valueBox.set(self.table, key: self.key(id), value: sharedWriteBuffer.readBufferNoCopy())
|
||||
})
|
||||
}
|
||||
|
||||
return .Reference
|
||||
} else if type == MediaEntryType.MessageReference.rawValue {
|
||||
var idPeerId: Int64 = 0
|
||||
var idNamespace: Int32 = 0
|
||||
var idId: Int32 = 0
|
||||
var idTimestamp: Int32 = 0
|
||||
value.read(&idPeerId, offset: 0, length: 8)
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
value.read(&idTimestamp, offset: 0, length: 4)
|
||||
|
||||
let referencedMessageIndex = MessageIndex(id: MessageId(peerId: PeerId(idPeerId), namespace: idNamespace, id: idId), timestamp: idTimestamp)
|
||||
|
||||
self.valueBox.remove(self.table, key: self.key(id), secure: false)
|
||||
|
||||
return .Embedded(referencedMessageIndex)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
return .Reference
|
||||
}
|
||||
|
||||
func removeEmbeddedMedia(_ media: Media) {
|
||||
if let id = media.id {
|
||||
self.valueBox.remove(self.table, key: self.key(id), secure: false)
|
||||
}
|
||||
}
|
||||
|
||||
func update(_ id: MediaId, media: Media, messageHistoryTable: MessageHistoryTable, operationsByPeerId: inout [PeerId: [MessageHistoryOperation]], sharedWriteBuffer: WriteBuffer = WriteBuffer(), sharedEncoder: PostboxEncoder = PostboxEncoder()) {
|
||||
if let updatedId = media.id {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(id)) {
|
||||
var type: Int8 = 0
|
||||
value.read(&type, offset: 0, length: 1)
|
||||
if type == MediaEntryType.Direct.rawValue {
|
||||
var dataLength: Int32 = 0
|
||||
value.read(&dataLength, offset: 0, length: 4)
|
||||
value.skip(Int(dataLength))
|
||||
|
||||
var messageReferenceCount: Int32 = 0
|
||||
value.read(&messageReferenceCount, offset: 0, length: 4)
|
||||
|
||||
sharedWriteBuffer.reset()
|
||||
var directType: Int8 = MediaEntryType.Direct.rawValue
|
||||
sharedWriteBuffer.write(&directType, offset: 0, length: 1)
|
||||
|
||||
sharedEncoder.reset()
|
||||
sharedEncoder.encodeRootObject(media)
|
||||
let mediaBuffer = sharedEncoder.memoryBuffer()
|
||||
var mediaBufferLength = Int32(mediaBuffer.length)
|
||||
sharedWriteBuffer.write(&mediaBufferLength, offset: 0, length: 4)
|
||||
sharedWriteBuffer.write(mediaBuffer.memory, offset: 0, length: mediaBuffer.length)
|
||||
|
||||
sharedWriteBuffer.write(&messageReferenceCount, offset: 0, length: 4)
|
||||
|
||||
if id != updatedId {
|
||||
self.valueBox.remove(self.table, key: self.key(id), secure: false)
|
||||
}
|
||||
withExtendedLifetime(sharedWriteBuffer, {
|
||||
self.valueBox.set(self.table, key: self.key(updatedId), value: sharedWriteBuffer.readBufferNoCopy())
|
||||
})
|
||||
} else if type == MediaEntryType.MessageReference.rawValue {
|
||||
var idPeerId: Int64 = 0
|
||||
var idNamespace: Int32 = 0
|
||||
var idId: Int32 = 0
|
||||
var idTimestamp: Int32 = 0
|
||||
value.read(&idPeerId, offset: 0, length: 8)
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
value.read(&idTimestamp, offset: 0, length: 4)
|
||||
|
||||
let referencedMessageIndex = MessageIndex(id: MessageId(peerId: PeerId(idPeerId), namespace: idNamespace, id: idId), timestamp: idTimestamp)
|
||||
messageHistoryTable.updateEmbeddedMedia(referencedMessageIndex, mediaId: id, media: media, operationsByPeerId: &operationsByPeerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func debugList() -> [DebugMediaEntry] {
|
||||
var entries: [DebugMediaEntry] = []
|
||||
|
||||
let upperBoundKey = ValueBoxKey(length: 8 + 4)
|
||||
memset(upperBoundKey.memory, 0xff, 8 + 4)
|
||||
self.valueBox.range(self.table, start: ValueBoxKey(length: 0), end: upperBoundKey, values: { key, value in
|
||||
var type: Int8 = 0
|
||||
value.read(&type, offset: 0, length: 1)
|
||||
if type == MediaEntryType.Direct.rawValue {
|
||||
var dataLength: Int32 = 0
|
||||
value.read(&dataLength, offset: 0, length: 4)
|
||||
if let media = PostboxDecoder(buffer: MemoryBuffer(memory: value.memory + value.offset, capacity: Int(dataLength), length: Int(dataLength), freeWhenDone: false)).decodeRootObject() as? Media {
|
||||
|
||||
value.skip(Int(dataLength))
|
||||
|
||||
var messageReferenceCount: Int32 = 0
|
||||
value.read(&messageReferenceCount, offset: 0, length: 4)
|
||||
|
||||
entries.append(.Direct(media, Int(messageReferenceCount)))
|
||||
}
|
||||
} else if type == MediaEntryType.MessageReference.rawValue {
|
||||
var idPeerId: Int64 = 0
|
||||
var idNamespace: Int32 = 0
|
||||
var idId: Int32 = 0
|
||||
var idTimestamp: Int32 = 0
|
||||
value.read(&idPeerId, offset: 0, length: 8)
|
||||
value.read(&idNamespace, offset: 0, length: 4)
|
||||
value.read(&idId, offset: 0, length: 4)
|
||||
value.read(&idTimestamp, offset: 0, length: 4)
|
||||
|
||||
let referencedMessageIndex = MessageIndex(id: MessageId(peerId: PeerId(idPeerId), namespace: idNamespace, id: idId), timestamp: idTimestamp)
|
||||
|
||||
entries.append(.MessageReference(referencedMessageIndex))
|
||||
}
|
||||
|
||||
return true
|
||||
}, limit: 1000)
|
||||
|
||||
return entries
|
||||
}
|
||||
}
|
||||
142
submodules/Postbox/Postbox/MessageOfInterestHolesView.swift
Normal file
142
submodules/Postbox/Postbox/MessageOfInterestHolesView.swift
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import Foundation
|
||||
|
||||
public struct HolesViewMedia: Comparable {
|
||||
public let media: Media
|
||||
public let peer: Peer
|
||||
public let authorIsContact: Bool
|
||||
public let index: MessageIndex
|
||||
|
||||
public static func ==(lhs: HolesViewMedia, rhs: HolesViewMedia) -> Bool {
|
||||
return lhs.index == rhs.index && (lhs.media === rhs.media || lhs.media.isEqual(to: rhs.media)) && lhs.peer.isEqual(rhs.peer) && lhs.authorIsContact == rhs.authorIsContact
|
||||
}
|
||||
|
||||
public static func <(lhs: HolesViewMedia, rhs: HolesViewMedia) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
}
|
||||
|
||||
public struct MessageOfInterestHole: Hashable, Equatable {
|
||||
public let hole: MessageHistoryViewHole
|
||||
public let direction: MessageHistoryViewRelativeHoleDirection
|
||||
}
|
||||
|
||||
public enum MessageOfInterestViewLocation: Hashable {
|
||||
case peer(PeerId)
|
||||
}
|
||||
|
||||
final class MutableMessageOfInterestHolesView: MutablePostboxView {
|
||||
private let location: MessageOfInterestViewLocation
|
||||
private let count: Int
|
||||
private var anchor: HistoryViewInputAnchor
|
||||
private var wrappedView: MutableMessageHistoryView
|
||||
|
||||
fileprivate var closestHole: MessageOfInterestHole?
|
||||
fileprivate var closestLaterMedia: [HolesViewMedia] = []
|
||||
|
||||
init(postbox: Postbox, location: MessageOfInterestViewLocation, namespace: MessageId.Namespace, count: Int) {
|
||||
self.location = location
|
||||
self.count = count
|
||||
|
||||
var peerId: PeerId
|
||||
switch self.location {
|
||||
case let .peer(id):
|
||||
peerId = id
|
||||
}
|
||||
var anchor: HistoryViewInputAnchor = .upperBound
|
||||
if let combinedState = postbox.readStateTable.getCombinedState(peerId), let state = combinedState.states.first, state.1.count != 0 {
|
||||
switch state.1 {
|
||||
case let .idBased(maxIncomingReadId, _, _, _, _):
|
||||
anchor = .message(MessageId(peerId: peerId, namespace: state.0, id: maxIncomingReadId))
|
||||
case let .indexBased(maxIncomingReadIndex, _, _, _):
|
||||
anchor = .index(maxIncomingReadIndex)
|
||||
}
|
||||
}
|
||||
self.anchor = anchor
|
||||
self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], peerIds: .single(peerId), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0})
|
||||
let _ = self.updateFromView()
|
||||
}
|
||||
|
||||
private func updateFromView() -> Bool {
|
||||
let closestHole: MessageOfInterestHole?
|
||||
if let (hole, direction) = self.wrappedView.firstHole() {
|
||||
closestHole = MessageOfInterestHole(hole: hole, direction: direction)
|
||||
} else {
|
||||
closestHole = nil
|
||||
}
|
||||
|
||||
var closestLaterMedia: [HolesViewMedia] = []
|
||||
switch self.wrappedView.sampledState {
|
||||
case .loading:
|
||||
break
|
||||
case let .loaded(sample):
|
||||
switch sample.anchor {
|
||||
case .index:
|
||||
let anchorIndex = binaryIndexOrLower(sample.entries, sample.anchor)
|
||||
loop: for i in max(0, anchorIndex) ..< sample.entries.count {
|
||||
let message = sample.entries[i].message
|
||||
if !message.media.isEmpty, let peer = message.peers[message.id.peerId] {
|
||||
for media in message.media {
|
||||
closestLaterMedia.append(HolesViewMedia(media: media, peer: peer, authorIsContact: sample.entries[i].attributes.authorIsContact, index: message.index))
|
||||
}
|
||||
}
|
||||
if closestLaterMedia.count >= 3 {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
case .lowerBound, .upperBound:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if self.closestHole != closestHole || self.closestLaterMedia != closestLaterMedia {
|
||||
self.closestHole = closestHole
|
||||
self.closestLaterMedia = closestLaterMedia
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var peerId: PeerId
|
||||
switch self.location {
|
||||
case let .peer(id):
|
||||
peerId = id
|
||||
}
|
||||
var anchor: HistoryViewInputAnchor = self.anchor
|
||||
if transaction.alteredInitialPeerCombinedReadStates[peerId] != nil {
|
||||
if let combinedState = postbox.readStateTable.getCombinedState(peerId), let state = combinedState.states.first, state.1.count != 0 {
|
||||
switch state.1 {
|
||||
case let .idBased(maxIncomingReadId, _, _, _, _):
|
||||
anchor = .message(MessageId(peerId: peerId, namespace: state.0, id: maxIncomingReadId))
|
||||
case let .indexBased(maxIncomingReadIndex, _, _, _):
|
||||
anchor = .index(maxIncomingReadIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.anchor != anchor {
|
||||
self.anchor = anchor
|
||||
self.wrappedView = MutableMessageHistoryView(postbox: postbox, orderStatistics: [], peerIds: .single(peerId), anchor: self.anchor, combinedReadStates: nil, transientReadStates: nil, tag: nil, count: self.count, topTaggedMessages: [:], additionalDatas: [], getMessageCountInRange: { _, _ in return 0})
|
||||
return self.updateFromView()
|
||||
} else if self.wrappedView.replay(postbox: postbox, transaction: transaction) {
|
||||
return self.updateFromView()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return MessageOfInterestHolesView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class MessageOfInterestHolesView: PostboxView {
|
||||
public let closestHole: MessageOfInterestHole?
|
||||
public let closestLaterMedia: [HolesViewMedia]
|
||||
|
||||
init(_ view: MutableMessageOfInterestHolesView) {
|
||||
self.closestHole = view.closestHole
|
||||
self.closestLaterMedia = view.closestLaterMedia
|
||||
}
|
||||
}
|
||||
55
submodules/Postbox/Postbox/MessageView.swift
Normal file
55
submodules/Postbox/Postbox/MessageView.swift
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableMessageView {
|
||||
let messageId: MessageId
|
||||
var stableId: UInt32?
|
||||
var message: Message?
|
||||
|
||||
init(messageId: MessageId, message: Message?) {
|
||||
self.messageId = messageId
|
||||
self.message = message
|
||||
self.stableId = message?.stableId
|
||||
}
|
||||
|
||||
func replay(_ operations: [MessageHistoryOperation], updatedMedia: [MediaId: Media?], renderIntermediateMessage: (IntermediateMessage) -> Message) -> Bool {
|
||||
var updated = false
|
||||
for operation in operations {
|
||||
switch operation {
|
||||
case let .Remove(indices):
|
||||
if let message = self.message {
|
||||
let messageIndex = message.index
|
||||
for (index, _) in indices {
|
||||
if index == messageIndex {
|
||||
self.message = nil
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .InsertMessage(message):
|
||||
if message.id == self.messageId || message.stableId == self.stableId {
|
||||
self.message = renderIntermediateMessage(message)
|
||||
self.stableId = message.stableId
|
||||
updated = true
|
||||
}
|
||||
case .UpdateEmbeddedMedia:
|
||||
break
|
||||
case .UpdateTimestamp:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
public final class MessageView {
|
||||
public let messageId: MessageId
|
||||
public let message: Message?
|
||||
|
||||
init(_ view: MutableMessageView) {
|
||||
self.messageId = view.messageId
|
||||
self.message = view.message
|
||||
}
|
||||
}
|
||||
66
submodules/Postbox/Postbox/MessagesView.swift
Normal file
66
submodules/Postbox/Postbox/MessagesView.swift
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableMessagesView: MutablePostboxView {
|
||||
fileprivate let ids: Set<MessageId>
|
||||
private let peerIds: Set<PeerId>
|
||||
fileprivate var messages: [MessageId: Message] = [:]
|
||||
|
||||
init(postbox: Postbox, ids: Set<MessageId>) {
|
||||
self.ids = ids
|
||||
self.peerIds = Set(ids.map { $0.peerId })
|
||||
for id in ids {
|
||||
if let message = postbox.getMessage(id) {
|
||||
self.messages[message.id] = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var updatedIds = Set<MessageId>()
|
||||
for peerId in self.peerIds {
|
||||
if let operations = transaction.currentOperationsByPeerId[peerId] {
|
||||
for operation in operations {
|
||||
switch operation {
|
||||
case let .InsertMessage(message):
|
||||
if self.ids.contains(message.id) {
|
||||
updatedIds.insert(message.id)
|
||||
}
|
||||
case let .Remove(indices):
|
||||
for index in indices {
|
||||
if self.ids.contains(index.0.id) {
|
||||
updatedIds.insert(index.0.id)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !updatedIds.isEmpty {
|
||||
for id in updatedIds {
|
||||
if let message = postbox.getMessage(id) {
|
||||
self.messages[message.id] = message
|
||||
} else {
|
||||
self.messages.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return MessagesView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class MessagesView: PostboxView {
|
||||
public var messages: [MessageId: Message] = [:]
|
||||
|
||||
init(_ view: MutableMessagesView) {
|
||||
self.messages = view.messages
|
||||
}
|
||||
}
|
||||
144
submodules/Postbox/Postbox/MetadataTable.swift
Normal file
144
submodules/Postbox/Postbox/MetadataTable.swift
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import Foundation
|
||||
|
||||
private enum MetadataKey: Int32 {
|
||||
case UserVersion = 1
|
||||
case State = 2
|
||||
case TransactionStateVersion = 3
|
||||
case MasterClientId = 4
|
||||
case AccessChallenge = 5
|
||||
case RemoteContactCount = 6
|
||||
}
|
||||
|
||||
final class MetadataTable: Table {
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
|
||||
}
|
||||
|
||||
private var cachedState: PostboxCoding?
|
||||
private var cachedRemoteContactCount: Int32?
|
||||
|
||||
private let sharedBuffer = WriteBuffer()
|
||||
|
||||
override init(valueBox: ValueBox, table: ValueBoxTable) {
|
||||
super.init(valueBox: valueBox, table: table)
|
||||
}
|
||||
|
||||
private func key(_ key: MetadataKey) -> ValueBoxKey {
|
||||
let valueBoxKey = ValueBoxKey(length: 8)
|
||||
valueBoxKey.setInt64(0, value: Int64(key.rawValue))
|
||||
return valueBoxKey
|
||||
}
|
||||
|
||||
func userVersion() -> Int32? {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.UserVersion)) {
|
||||
var version: Int32 = 0
|
||||
value.read(&version, offset: 0, length: 4)
|
||||
return version
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setUserVersion(_ version: Int32) {
|
||||
sharedBuffer.reset()
|
||||
let buffer = sharedBuffer
|
||||
var varVersion: Int32 = version
|
||||
buffer.write(&varVersion, offset: 0, length: 4)
|
||||
self.valueBox.set(self.table, key: self.key(.UserVersion), value: buffer)
|
||||
}
|
||||
|
||||
func state() -> PostboxCoding? {
|
||||
if let cachedState = self.cachedState {
|
||||
return cachedState
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.State)) {
|
||||
if let state = PostboxDecoder(buffer: value).decodeRootObject() {
|
||||
self.cachedState = state
|
||||
return state
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setState(_ state: PostboxCoding) {
|
||||
self.cachedState = state
|
||||
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(state)
|
||||
withExtendedLifetime(encoder, {
|
||||
self.valueBox.set(self.table, key: self.key(.State), value: encoder.readBufferNoCopy())
|
||||
})
|
||||
}
|
||||
|
||||
func transactionStateVersion() -> Int64 {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.TransactionStateVersion)) {
|
||||
var version: Int64 = 0
|
||||
value.read(&version, offset: 0, length: 8)
|
||||
return version
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func incrementTransactionStateVersion() -> Int64 {
|
||||
var version = self.transactionStateVersion() + 1
|
||||
sharedBuffer.reset()
|
||||
let buffer = sharedBuffer
|
||||
buffer.write(&version, offset: 0, length: 8)
|
||||
self.valueBox.set(self.table, key: self.key(.TransactionStateVersion), value: buffer)
|
||||
return version
|
||||
}
|
||||
|
||||
func masterClientId() -> Int64 {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.MasterClientId)) {
|
||||
var clientId: Int64 = 0
|
||||
value.read(&clientId, offset: 0, length: 8)
|
||||
return clientId
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func setMasterClientId(_ id: Int64) {
|
||||
sharedBuffer.reset()
|
||||
let buffer = sharedBuffer
|
||||
var clientId = id
|
||||
buffer.write(&clientId, offset: 0, length: 8)
|
||||
self.valueBox.set(self.table, key: self.key(.MasterClientId), value: buffer)
|
||||
}
|
||||
|
||||
func accessChallengeData() -> PostboxAccessChallengeData {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.AccessChallenge)) {
|
||||
return PostboxAccessChallengeData(decoder: PostboxDecoder(buffer: value))
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func setRemoteContactCount(_ count: Int32) {
|
||||
self.cachedRemoteContactCount = count
|
||||
var mutableCount: Int32 = count
|
||||
self.valueBox.set(self.table, key: self.key(.RemoteContactCount), value: MemoryBuffer(memory: &mutableCount, capacity: 4, length: 4, freeWhenDone: false))
|
||||
}
|
||||
|
||||
func getRemoteContactCount() -> Int32 {
|
||||
if let cachedRemoteContactCount = self.cachedRemoteContactCount {
|
||||
return cachedRemoteContactCount
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: self.key(.RemoteContactCount)) {
|
||||
var count: Int32 = 0
|
||||
value.read(&count, offset: 0, length: 4)
|
||||
self.cachedRemoteContactCount = count
|
||||
return count
|
||||
} else {
|
||||
self.cachedRemoteContactCount = 0
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
self.cachedState = nil
|
||||
self.cachedRemoteContactCount = nil
|
||||
}
|
||||
}
|
||||
52
submodules/Postbox/Postbox/MultiplePeersView.swift
Normal file
52
submodules/Postbox/Postbox/MultiplePeersView.swift
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableMultiplePeersView {
|
||||
let peerIds: Set<PeerId>
|
||||
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
var presences: [PeerId: PeerPresence] = [:]
|
||||
|
||||
init(peerIds: [PeerId], getPeer: (PeerId) -> Peer?, getPeerPresence: (PeerId) -> PeerPresence?) {
|
||||
self.peerIds = Set(peerIds)
|
||||
|
||||
for peerId in self.peerIds {
|
||||
if let peer = getPeer(peerId) {
|
||||
self.peers[peerId] = peer
|
||||
}
|
||||
if let presence = getPeerPresence(peerId) {
|
||||
self.presences[peerId] = presence
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replay(updatedPeers: [PeerId: Peer], updatedPeerPresences: [PeerId: PeerPresence]) -> Bool {
|
||||
if updatedPeers.isEmpty && updatedPeerPresences.isEmpty {
|
||||
return false
|
||||
}
|
||||
|
||||
var updated = false
|
||||
|
||||
for peerId in self.peerIds {
|
||||
if let peer = updatedPeers[peerId] {
|
||||
self.peers[peerId] = peer
|
||||
updated = true
|
||||
}
|
||||
if let presence = updatedPeerPresences[peerId] {
|
||||
self.presences[peerId] = presence
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
public final class MultiplePeersView {
|
||||
public let peers: [PeerId: Peer]
|
||||
public let presences: [PeerId: PeerPresence]
|
||||
|
||||
init(_ view: MutableMultiplePeersView) {
|
||||
self.peers = view.peers
|
||||
self.presences = view.presences
|
||||
}
|
||||
}
|
||||
12
submodules/Postbox/Postbox/MurMurHash32.h
Normal file
12
submodules/Postbox/Postbox/MurMurHash32.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#ifndef Postbox_MurMurHash32_h
|
||||
#define Postbox_MurMurHash32_h
|
||||
|
||||
#import <stdint.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
int32_t murMurHash32(void *bytes, int length);
|
||||
int32_t murMurHash32Data(NSData *data);
|
||||
int32_t murMurHashString32(const char *s);
|
||||
NSString *postboxTransformedString(CFStringRef string, bool replaceWithTransliteratedVersion, bool appendTransliteratedVersion);
|
||||
|
||||
#endif
|
||||
120
submodules/Postbox/Postbox/MurMurHash32.m
Normal file
120
submodules/Postbox/Postbox/MurMurHash32.m
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#import "MurMurHash32.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define FORCE_INLINE __attribute__((always_inline))
|
||||
|
||||
static inline uint32_t rotl32 ( uint32_t x, int8_t r )
|
||||
{
|
||||
return (x << r) | (x >> (32 - r));
|
||||
}
|
||||
|
||||
#define ROTL32(x,y) rotl32(x,y)
|
||||
|
||||
static FORCE_INLINE uint32_t getblock ( const uint32_t * p, int i )
|
||||
{
|
||||
return p[i];
|
||||
}
|
||||
|
||||
static FORCE_INLINE uint32_t fmix ( uint32_t h )
|
||||
{
|
||||
h ^= h >> 16;
|
||||
h *= 0x85ebca6b;
|
||||
h ^= h >> 13;
|
||||
h *= 0xc2b2ae35;
|
||||
h ^= h >> 16;
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
static void murMurHash32Impl(const void *key, int len, uint32_t seed, void *out)
|
||||
{
|
||||
const uint8_t * data = (const uint8_t*)key;
|
||||
const int nblocks = len / 4;
|
||||
|
||||
uint32_t h1 = seed;
|
||||
|
||||
const uint32_t c1 = 0xcc9e2d51;
|
||||
const uint32_t c2 = 0x1b873593;
|
||||
|
||||
//----------
|
||||
// body
|
||||
|
||||
const uint32_t * blocks = (const uint32_t *)(data + nblocks*4);
|
||||
|
||||
for(int i = -nblocks; i; i++)
|
||||
{
|
||||
uint32_t k1 = getblock(blocks,i);
|
||||
|
||||
k1 *= c1;
|
||||
k1 = ROTL32(k1,15);
|
||||
k1 *= c2;
|
||||
|
||||
h1 ^= k1;
|
||||
h1 = ROTL32(h1,13);
|
||||
h1 = h1*5+0xe6546b64;
|
||||
}
|
||||
|
||||
//----------
|
||||
// tail
|
||||
|
||||
const uint8_t * tail = (const uint8_t*)(data + nblocks*4);
|
||||
|
||||
uint32_t k1 = 0;
|
||||
|
||||
switch(len & 3)
|
||||
{
|
||||
case 3: k1 ^= tail[2] << 16;
|
||||
case 2: k1 ^= tail[1] << 8;
|
||||
case 1: k1 ^= tail[0];
|
||||
k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1;
|
||||
};
|
||||
|
||||
//----------
|
||||
// finalization
|
||||
|
||||
h1 ^= len;
|
||||
|
||||
h1 = fmix(h1);
|
||||
|
||||
*(uint32_t*)out = h1;
|
||||
}
|
||||
|
||||
int32_t murMurHash32(void *bytes, int length)
|
||||
{
|
||||
int32_t result = 0;
|
||||
murMurHash32Impl(bytes, length, -137723950, &result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int32_t murMurHash32Data(NSData *data) {
|
||||
return murMurHash32((void *)data.bytes, (int)data.length);
|
||||
}
|
||||
|
||||
int32_t murMurHashString32(const char *s)
|
||||
{
|
||||
int32_t result = 0;
|
||||
murMurHash32Impl(s, (int)strlen(s), -137723950, &result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
NSString *postboxTransformedString(CFStringRef string, bool replaceWithTransliteratedVersion, bool appendTransliteratedVersion) {
|
||||
NSMutableString *mutableString = [[NSMutableString alloc] initWithString:(__bridge NSString * _Nonnull)(string)];
|
||||
CFStringTransform((CFMutableStringRef)mutableString, NULL, kCFStringTransformStripCombiningMarks, false);
|
||||
|
||||
if (replaceWithTransliteratedVersion || appendTransliteratedVersion) {
|
||||
NSMutableString *transliteratedString = [[NSMutableString alloc] initWithString:mutableString];
|
||||
CFStringTransform((CFMutableStringRef)transliteratedString, NULL, kCFStringTransformToLatin, false);
|
||||
if (replaceWithTransliteratedVersion) {
|
||||
return transliteratedString;
|
||||
} else {
|
||||
[mutableString appendString:@" "];
|
||||
[mutableString appendString:transliteratedString];
|
||||
}
|
||||
}
|
||||
|
||||
return mutableString;
|
||||
}
|
||||
33
submodules/Postbox/Postbox/MutableBasicPeerView.swift
Normal file
33
submodules/Postbox/Postbox/MutableBasicPeerView.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableBasicPeerView: MutablePostboxView {
|
||||
private let peerId: PeerId
|
||||
fileprivate var peer: Peer?
|
||||
|
||||
init(postbox: Postbox, peerId: PeerId) {
|
||||
self.peerId = peerId
|
||||
self.peer = postbox.peerTable.get(peerId)
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
if let peer = transaction.currentUpdatedPeers[self.peerId] {
|
||||
self.peer = peer
|
||||
updated = true
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return BasicPeerView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class BasicPeerView: PostboxView {
|
||||
public let peer: Peer?
|
||||
|
||||
init(_ view: MutableBasicPeerView) {
|
||||
self.peer = view.peer
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import Foundation
|
||||
|
||||
final class MutablePeerChatInclusionView: MutablePostboxView {
|
||||
private let peerId: PeerId
|
||||
fileprivate var inclusion: Bool
|
||||
|
||||
init(postbox: Postbox, peerId: PeerId) {
|
||||
self.peerId = peerId
|
||||
|
||||
self.inclusion = postbox.chatListIndexTable.get(peerId: self.peerId).includedIndex(peerId: peerId) != nil
|
||||
}
|
||||
|
||||
func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool {
|
||||
var updated = false
|
||||
if transaction.currentUpdatedChatListInclusions[self.peerId] != nil {
|
||||
let inclusion = postbox.chatListIndexTable.get(peerId: self.peerId).includedIndex(peerId: self.peerId) != nil
|
||||
if self.inclusion != inclusion {
|
||||
self.inclusion = inclusion
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func immutableView() -> PostboxView {
|
||||
return PeerChatInclusionView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class PeerChatInclusionView: PostboxView {
|
||||
public let inclusion: Bool
|
||||
|
||||
init(_ view: MutablePeerChatInclusionView) {
|
||||
self.inclusion = view.inclusion
|
||||
}
|
||||
}
|
||||
31
submodules/Postbox/Postbox/NoticeEntryView.swift
Normal file
31
submodules/Postbox/Postbox/NoticeEntryView.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import Foundation
|
||||
|
||||
final class MutableNoticeEntryView {
|
||||
private let key: NoticeEntryKey
|
||||
fileprivate var value: NoticeEntry?
|
||||
|
||||
init(accountManagerImpl: AccountManagerImpl, key: NoticeEntryKey) {
|
||||
self.key = key
|
||||
self.value = accountManagerImpl.noticeTable.get(key: key)
|
||||
}
|
||||
|
||||
func replay(accountManagerImpl: AccountManagerImpl, updatedKeys: Set<NoticeEntryKey>) -> Bool {
|
||||
if updatedKeys.contains(self.key) {
|
||||
self.value = accountManagerImpl.noticeTable.get(key: self.key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func immutableView() -> NoticeEntryView {
|
||||
return NoticeEntryView(self)
|
||||
}
|
||||
}
|
||||
|
||||
public final class NoticeEntryView {
|
||||
public let value: NoticeEntry?
|
||||
|
||||
init(_ view: MutableNoticeEntryView) {
|
||||
self.value = view.value
|
||||
}
|
||||
}
|
||||
108
submodules/Postbox/Postbox/NoticeTable.swift
Normal file
108
submodules/Postbox/Postbox/NoticeTable.swift
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import Foundation
|
||||
|
||||
public struct NoticeEntryKey: Hashable {
|
||||
public let namespace: ValueBoxKey
|
||||
public let key: ValueBoxKey
|
||||
|
||||
fileprivate let combinedKey: ValueBoxKey
|
||||
|
||||
public init(namespace: ValueBoxKey, key: ValueBoxKey) {
|
||||
self.namespace = namespace
|
||||
self.key = key
|
||||
|
||||
let combinedKey = ValueBoxKey(length: namespace.length + key.length)
|
||||
memcpy(combinedKey.memory, namespace.memory, namespace.length)
|
||||
memcpy(combinedKey.memory.advanced(by: namespace.length), key.memory, key.length)
|
||||
self.combinedKey = combinedKey
|
||||
}
|
||||
|
||||
public static func ==(lhs: NoticeEntryKey, rhs: NoticeEntryKey) -> Bool {
|
||||
return lhs.combinedKey == rhs.combinedKey
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.combinedKey.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
private struct CachedEntry {
|
||||
let entry: NoticeEntry?
|
||||
}
|
||||
|
||||
public protocol NoticeEntry: PostboxCoding {
|
||||
func isEqual(to: NoticeEntry) -> Bool
|
||||
}
|
||||
|
||||
final class NoticeTable: Table {
|
||||
private var cachedEntries: [NoticeEntryKey: CachedEntry] = [:]
|
||||
private var updatedEntryKeys = Set<NoticeEntryKey>()
|
||||
|
||||
static func tableSpec(_ id: Int32) -> ValueBoxTable {
|
||||
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
func getAll() -> [ValueBoxKey: NoticeEntry] {
|
||||
var result: [ValueBoxKey: NoticeEntry] = [:]
|
||||
self.valueBox.scan(self.table, values: { key, value in
|
||||
if let object = PostboxDecoder(buffer: value).decodeRootObject() as? NoticeEntry {
|
||||
result[key] = object
|
||||
}
|
||||
return true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func get(key: NoticeEntryKey) -> NoticeEntry? {
|
||||
if let cached = self.cachedEntries[key] {
|
||||
return cached.entry
|
||||
} else {
|
||||
if let value = self.valueBox.get(self.table, key: key.combinedKey), let object = PostboxDecoder(buffer: value).decodeRootObject() as? NoticeEntry {
|
||||
self.cachedEntries[key] = CachedEntry(entry: object)
|
||||
return object
|
||||
} else {
|
||||
self.cachedEntries[key] = CachedEntry(entry: nil)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func set(key: NoticeEntryKey, value: NoticeEntry?) {
|
||||
self.cachedEntries[key] = CachedEntry(entry: value)
|
||||
updatedEntryKeys.insert(key)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
var keys: [ValueBoxKey] = []
|
||||
self.valueBox.scan(self.table, keys: { key in
|
||||
keys.append(key)
|
||||
return true
|
||||
})
|
||||
for key in keys {
|
||||
self.valueBox.remove(self.table, key: key, secure: false)
|
||||
}
|
||||
self.updatedEntryKeys.formUnion(cachedEntries.keys)
|
||||
self.cachedEntries.removeAll()
|
||||
}
|
||||
|
||||
override func clearMemoryCache() {
|
||||
assert(self.updatedEntryKeys.isEmpty)
|
||||
}
|
||||
|
||||
override func beforeCommit() {
|
||||
if !self.updatedEntryKeys.isEmpty {
|
||||
for key in self.updatedEntryKeys {
|
||||
if let value = self.cachedEntries[key]?.entry {
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(value)
|
||||
withExtendedLifetime(encoder, {
|
||||
self.valueBox.set(self.table, key: key.combinedKey, value: encoder.readBufferNoCopy())
|
||||
})
|
||||
} else {
|
||||
self.valueBox.remove(self.table, key: key.combinedKey, secure: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.updatedEntryKeys.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue