Skip to content

Commit

Permalink
feat: Add PersistenceLog (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
juyan authored Nov 26, 2023
2 parents 871639a + 6ebc672 commit 4bd7930
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 5 deletions.
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func createWithFallback() -> ObjectStore {
}
```

swift-filestore does not require developers to create new struct/classes for your data model. For example, to use JSON serialization, just have your existing model conform to `JSONDataRepresentable`.
`swift-filestore` does not require developers to create new struct/classes for your data model. For example, to use JSON serialization, just have your existing model conform to `JSONDataRepresentable`.

```swift

Expand All @@ -44,7 +44,8 @@ try await objectStore.write(key: model.id, namespace: "MyModels", object: model)
```

## Object Change Stream
swift-filestore offers an object change subscription API via Swift Concurrency.

`swift-filestore` offers an object change subscription API via Swift Concurrency.

```swift
for try await model in await objectStore.observe(key: id, namespace: "MyModels", objectType: MyModel.self) {
Expand All @@ -53,7 +54,8 @@ for try await model in await objectStore.observe(key: id, namespace: "MyModels",
```

## Custom serialization/deserialization
If you are looking for non-json serializations, you may define your custom serialization/deserialization protocol as below:

If you are looking for non-json serializations, you can define your custom serialization/deserialization protocol as below:

```swift

Expand All @@ -75,3 +77,27 @@ struct MyModel: BinaryDataRepresentable {
let value: String
}
```

## PersistenceLog

`swift-filestore` offers an immutable logging component named `PersistenceLog`. It allows developer to store records on the disk and flush them at the right time. It can be used as an alternative to in-memory logging, which may risk data loss because app can be terminated at any time by user or the system.


Below code demonstrates how to use `PersistenceLog` to store and send in-app analytic events:
```swift
//data model for the analytics log
struct AnalyticsEvent: Codable, JSONDataRepresentable {
let name: String
let metaData: String
}

//initialization
let log = try PersistenceLogImpl<AnalyticsEvent>(name: "analytics-log")

//When new event is triggered
try await log.append(event1)

//When it's time to flush and sent to remote server
let events = try await log.flush()
try await networkClient.sendAnalytics(events)
```
86 changes: 86 additions & 0 deletions Sources/SwiftFileStore/PersistenceLog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// File.swift
//
//
// Created by Jun Yan on 11/25/23.
//

import Foundation

/// A persistent log which supports append and flush operation.
/// This can be used as a persistent logging queue as an alternative to in-memory queue to prevent data losses if app is killed.
public protocol PersistenceLog {

associatedtype Element: DataRepresentable

func append(element: Element) async throws

func flush() async throws -> [Element]
}


public actor PersistenceLogImpl<ElementType>: PersistenceLog where ElementType: DataRepresentable {

public typealias Element = ElementType

let fileURL: URL

private let name: String
private let dirURL: URL
private let fileHandle: FileHandle

public init(name: String) throws {
let applicationSupportDir = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let rootDir = applicationSupportDir.appendingPathComponent("persistence-log", isDirectory: true)
try FileManager.default.createDirIfNotExist(url: rootDir)
try self.init(name: name, rootDir: rootDir)
}

init(name: String, rootDir: URL) throws {
self.name = name
self.dirURL = rootDir
let fileURL = rootDir.appendingPathComponent(name, isDirectory: false)
let success = FileManager.default.createFileIfNotExist(url: fileURL)
if !success {
throw "failed to create file at \(fileURL.absoluteString)"
}
self.fileHandle = try FileHandle(forUpdating: fileURL)
self.fileURL = fileURL
}

public func append(element: ElementType) async throws {
try fileHandle.seekToEnd()
let data = try element.serialize()
let dataSize = UInt32(data.count)
let bytes: Data = withUnsafeBytes(of: dataSize) { Data($0) } + data
try fileHandle.write(contentsOf: bytes)
}

public func flush() async throws -> [ElementType] {
try fileHandle.seek(toOffset: 0)
let fileData = try fileHandle.readToEnd()
try fileHandle.truncate(atOffset: 0)
return try fileData?.deserializeToArray() ?? []
}
}

extension Data {

func deserializeToArray<ElementType>() throws -> [ElementType] where ElementType: DataRepresentable {
var result: [ElementType] = []
var idx = 0
let uint32Size = MemoryLayout<UInt32>.size
while idx < count {
let sizeData = subdata(in: idx ..< idx + uint32Size)
let size = sizeData.withUnsafeBytes { (rawPtr: UnsafeRawBufferPointer) in
rawPtr.load(as: UInt32.self)
}
idx += uint32Size
let elementData = subdata(in: idx ..< idx + Int(size))
idx += Int(size)
let element = try ElementType.from(data: elementData)
result.append(element)
}
return result
}
}
1 change: 0 additions & 1 deletion Tests/SwiftFileStoreTests/FileObjectStoreTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Combine
import XCTest
@testable import SwiftFileStore

Expand Down
1 change: 0 additions & 1 deletion Tests/SwiftFileStoreTests/MemoryObjectStoreTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Combine
import XCTest
@testable import SwiftFileStore

Expand Down
28 changes: 28 additions & 0 deletions Tests/SwiftFileStoreTests/PersistenceLogTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// File.swift
//
//
// Created by Jun Yan on 11/25/23.
//

import XCTest
@testable import SwiftFileStore

final class PersistenceLogTests: XCTestCase {

var log: (any TestObjectLog)!

func test_append_flush() async throws {
log = try! PersistenceLogImpl<TestObject>(name: "test-queue")
let object1 = TestObject(value: 1)
let object2 = TestObject(value: 2)
try await log.append(element: object1)
try await log.append(element: object2)
let result = try await log.flush()
XCTAssertEqual(result, [object1, object2])
}
}

protocol TestObjectLog: PersistenceLog where Element == TestObject {}

extension PersistenceLogImpl<TestObject>: TestObjectLog {}

0 comments on commit 4bd7930

Please sign in to comment.