Add 'submodules/Postbox/' from commit '534443c710'

git-subtree-dir: submodules/Postbox
git-subtree-mainline: 373769682e
git-subtree-split: 534443c710
This commit is contained in:
Peter 2019-06-11 18:56:39 +01:00
commit 4459dc5b47
218 changed files with 281352 additions and 0 deletions

25
submodules/Postbox/.gitignore vendored Normal file
View 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
View file

@ -0,0 +1,3 @@
[submodule "submodules/lmdb"]
path = submodules/lmdb
url = https://github.com/LMDB/lmdb.git

44
submodules/Postbox/BUCK Normal file
View 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'
],
)

View 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
}
}

View 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
}
}
}

View 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)
}
}

View 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())
})
}
}

View 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))
}
}

View file

@ -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)
}
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View file

@ -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
}
}
}

View 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
}
}

View file

@ -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?)
}

View 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
}

View 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
}
}

View 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
}

View 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()
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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(&currentTotalRootUnreadState, 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(&currentTotalRootUnreadState, 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(&currentTotalRootUnreadState, 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: &currentUpdatedGroupSummarySynchronizeOperations)
} 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)
}
}

View 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
})
}
}

View 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
}
}

View file

@ -0,0 +1,6 @@
import Foundation
public enum ChatLocation: Equatable {
case peer(PeerId)
//case group(PeerGroupId)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
SWIFT_INCLUDE_PATHS = $(SRCROOT)/Postbox
MODULEMAP_PRIVATE_FILE = $(SRCROOT)/Postbox/module.private.modulemap

View 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
}
}

View 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
}
}

View 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
}
}
}

View 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

View 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);
}

View 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
}
}
}

View file

@ -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
}
})
}
}

View 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
}
}

View 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
}
}

View 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)
}
}

View 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
}
}

View 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()
}
}
}

View 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)
}
}

View 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>

View file

@ -0,0 +1,7 @@
import Foundation
public struct InitialMessageHistoryData {
public let peer: Peer?
public let chatInterfaceState: PeerChatInterfaceState?
public let associatedMessages: [MessageId: Message]
}

View 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)
}
}

View file

@ -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()
}
}
}

View file

@ -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
}
}

View file

@ -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() {
}
}

View 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

View 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

View 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
}
}

View 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()
}
}

View 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() {
}
}

View 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])
}

View 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
}
}

View 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() {
}
}

View 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
}
}

View 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
}
}

View 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() {
}
}

View 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
}
}

View 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)
}
}

View file

@ -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
}
}

View 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
}
}

View 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)
}
}

View 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)
}
}

View 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
}
}

View 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
}
}
}

View 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)
}
}
}

View 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
}
}

View 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
}
}
}

View 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
}
}

View file

@ -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)
}
}

View 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
}
}
}

View 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
}
}

View 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
}
}

View 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(&timestamp, 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(&timestamp, 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()
}
}

View 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
}
}
}

View 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])
}

View 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()
}
}
}

View file

@ -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()
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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
}
}

View 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()
}
}
}

View 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
}
}

View file

@ -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
}
}

View 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() {
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
import Foundation
public struct MutableMessageHistoryEntryAttributes: Equatable {
public var authorIsContact: Bool
public init(authorIsContact: Bool) {
self.authorIsContact = authorIsContact
}
}

File diff suppressed because it is too large Load diff

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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

View 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;
}

View 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
}
}

View file

@ -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
}
}

View 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
}
}

View 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