Created
March 29, 2022 18:51
-
-
Save ts95/445c939d8e9b5414f43c163df1a6b64a to your computer and use it in GitHub Desktop.
Append-only database + Text editor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
protocol AppendOnlyDatabaseProtocol { | |
var count: Int { get } | |
mutating func append(_ other: Data) | |
subscript(index: Data.Index) -> UInt8 { get } | |
subscript(bounds: Range<Data.Index>) -> Data { get } | |
} | |
extension Data: AppendOnlyDatabaseProtocol {} | |
class Ref<T> { | |
var value: T | |
init(initialValue: T) { | |
value = initialValue | |
} | |
} | |
class TextEditor { | |
private let databaseRef: Ref<AppendOnlyDatabaseProtocol> | |
private(set) var stringBuffer = "" | |
init(databaseRef: Ref<AppendOnlyDatabaseProtocol>) { | |
self.databaseRef = databaseRef | |
replayMutations() | |
} | |
func append(string: String) { | |
commit(mutation: .append(Data(string.utf8))) | |
stringBuffer += string | |
} | |
func remove(range: Range<String.Index>) { | |
commit(mutation: .remove(range: range)) | |
stringBuffer.removeSubrange(range) | |
} | |
func clear() { | |
remove(range: (stringBuffer.startIndex..<stringBuffer.endIndex)) | |
} | |
func replayMutations() { | |
stringBuffer = "" | |
var seeker = 0 | |
while seeker < databaseRef.value.count { | |
switch ReadOperation(byte: databaseRef.value[seeker]) { | |
case .append: | |
seeker += 1 | |
let countSize = MemoryLayout<Int>.size | |
let data = databaseRef.value[seeker..<seeker+countSize] as NSData | |
let count = data.bytes.assumingMemoryBound(to: Int.self).pointee.littleEndian | |
seeker += countSize | |
let stringData = databaseRef.value[seeker..<seeker+count] | |
let string = String(decoding: stringData, as: UTF8.self) | |
stringBuffer += string | |
seeker += stringData.count | |
case .remove: | |
seeker += 1 | |
let indexSize = MemoryLayout<String.Index>.size | |
var data = databaseRef.value[seeker..<seeker+indexSize] as NSData | |
let lowerBound = data.bytes.assumingMemoryBound(to: String.Index.self).pointee | |
seeker += indexSize | |
data = databaseRef.value[seeker..<seeker+indexSize] as NSData | |
let upperBound = data.bytes.assumingMemoryBound(to: String.Index.self).pointee | |
stringBuffer.removeSubrange(lowerBound..<upperBound) | |
seeker += indexSize | |
default: | |
break | |
} | |
} | |
} | |
private func commit(mutation: WriteMutation) { | |
databaseRef.value.append(mutation.data) | |
} | |
enum WriteMutation { | |
case append(Data) | |
case remove(range: Range<String.Index>) | |
var operation: UInt8 { | |
switch self { | |
case .append: | |
return 0xF8 | |
case .remove: | |
return 0xF9 | |
} | |
} | |
var data: Data { | |
switch self { | |
case .append(let data): | |
return Data([operation]) + dataFrom(int: data.count) + data | |
case .remove(let range): | |
return Data([operation]) + dataFrom(stringIndex: range.lowerBound) + dataFrom(stringIndex: range.upperBound) | |
} | |
} | |
private func dataFrom(int: Int) -> Data { | |
Data(withUnsafeBytes(of: int.littleEndian, Array.init)) | |
} | |
private func dataFrom(stringIndex: String.Index) -> Data { | |
Data(withUnsafeBytes(of: stringIndex, Array.init)) | |
} | |
} | |
enum ReadOperation { | |
case append | |
case remove | |
init?(byte: UInt8) { | |
switch byte { | |
case 0xF8: | |
self = .append | |
case 0xF9: | |
self = .remove | |
default: | |
return nil | |
} | |
} | |
} | |
} | |
let databaseRef = Ref<AppendOnlyDatabaseProtocol>(initialValue: Data()) | |
let textEditor = TextEditor(databaseRef: databaseRef) | |
textEditor.append(string: "Hello world!") | |
textEditor.remove(range: textEditor.stringBuffer.range(of: "world!")!) | |
textEditor.append(string: "Toni!") | |
let secondTextEditor = TextEditor(databaseRef: databaseRef) | |
secondTextEditor.append(string: " Nice to meet you!") | |
secondTextEditor.clear() | |
secondTextEditor.append(string: "😛") | |
print(textEditor.stringBuffer) | |
textEditor.replayMutations() | |
print(textEditor.stringBuffer) | |
print("Size of database:", databaseRef.value.count) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment