Skip to content

Instantly share code, notes, and snippets.

@ts95
Created March 29, 2022 18:51
Show Gist options
  • Save ts95/445c939d8e9b5414f43c163df1a6b64a to your computer and use it in GitHub Desktop.
Save ts95/445c939d8e9b5414f43c163df1a6b64a to your computer and use it in GitHub Desktop.
Append-only database + Text editor
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