-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Working on v2 * Switching to NSCache based architecture * Update RemoteObjectDelegate * Fixed Binding * Fixed sync * Adding allKeys in Store * Trying to fix initial pull for RemoteObjects * Experimenting with future ID fetching * Big improvement using a separate map for requests and actual row ids * Updated tests * Made ids public for easier API * Upgrading RemoteRepresentable to be Hashable * Fix store request * Trying to fix network leak * [WIP] Fixing network leak * Making cache recoverable * [WIP] Fixing leaks * Still trying to fix leaks... * Fixing cache keys function * Still working on preventing leaks * Improved safety of RemoteObject * Actually... wasn't a good idea * Introducing new kind of Binding * Better unwrap * Trying to figure out model that would make binding more safe * Might have found something * Still trying to fix the issue * Fixing weird swift binding bug * Fixing for remote objects * Going back to normal binding * Changed the needPull system * Fixed view update cascade * Working on reactivity with live elements * Improved safety * Making the request push method available * Fixed push * Added control over schedule delay * Added delete method * Fix delete * Going with a different, less safe approach, but that could help fix issues * Working on the docs * Added display * Added remote popping mechanism * Now saving to disk automatically * It's now possible to query the store * Better revalidating * Changed project architecture * Improved DataCache * Fixed MainActor * Fix multi object display * Fixing Realtime * Fixed heartbeat * Improved overall system * fix: build
- Loading branch information
Showing
56 changed files
with
3,292 additions
and
3,471 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// | ||
// DataCache.swift | ||
// | ||
// | ||
// Created by Arthur Guiot on 08/04/2023. | ||
// | ||
|
||
import Foundation | ||
import Combine | ||
|
||
public protocol DataCache<Object>: ObservableObject { | ||
associatedtype Object: RemoteData | ||
|
||
var cacheDirectoryName: String { get } | ||
var objectWillChange: ObservableObjectPublisher { get } | ||
|
||
func fetch(using request: URLRequest) async throws -> Data | ||
|
||
func urlRequests(for request: RemoteRequest<Object.ID>) -> [URLRequest] | ||
} | ||
|
||
extension DataCache { | ||
public var cacheDirectory: URL { | ||
let fileManager = FileManager.default | ||
let urls = fileManager.urls(for: .cachesDirectory, in: .userDomainMask) | ||
let directoryURL = urls[0].appendingPathComponent(cacheDirectoryName) | ||
if !fileManager.fileExists(atPath: directoryURL.path) { | ||
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) | ||
} | ||
return directoryURL | ||
} | ||
|
||
public var logger: Logger { | ||
return Logger() | ||
} | ||
|
||
private func cachedData(for request: URLRequest) -> Data? { | ||
guard let url = request.url, let fileName = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .alphanumerics) else { | ||
return nil | ||
} | ||
let fileURL = cacheDirectory.appendingPathComponent(fileName) | ||
return try? Data(contentsOf: fileURL) | ||
} | ||
|
||
private func storeCachedData(_ data: Data, for request: URLRequest) { | ||
guard let url = request.url, let fileName = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .alphanumerics) else { | ||
return | ||
} | ||
let fileURL = cacheDirectory.appendingPathComponent(fileName) | ||
do { | ||
try data.write(to: fileURL) | ||
} catch { | ||
logger.log(error) | ||
} | ||
} | ||
|
||
public func pull(for request: RemoteRequest<Object.ID>) -> [Object.ObjectData] { | ||
let urlRequests = urlRequests(for: request) | ||
var objects: [Object.ObjectData] = [] | ||
|
||
for req in urlRequests { | ||
if let data = cachedData(for: req) { | ||
let object = Object.object(from: data) | ||
objects.append(object) | ||
} else { | ||
Task.detached { | ||
do { | ||
let fetchedData = try await self.fetch(using: req) | ||
self.storeCachedData(fetchedData, for: req) | ||
await MainActor.run { | ||
self.objectWillChange.send() | ||
} | ||
} catch { | ||
self.logger.log(error) | ||
} | ||
} | ||
} | ||
} | ||
|
||
return objects | ||
} | ||
|
||
public func pullSingle(for id: Object.ID) -> Object.ObjectData? { | ||
return pull(for: .id(id)).first | ||
} | ||
|
||
public func removeAllData() { | ||
try? FileManager.default.removeItem(at: cacheDirectory) | ||
objectWillChange.send() | ||
} | ||
} | ||
|
||
public enum DataCacheError: Error { | ||
case urlRequestCreationFailed | ||
} | ||
|
||
public extension DataCache { | ||
func fetch(using request: URLRequest) async throws -> Data { | ||
let (data, response) = try await URLSession.shared.data(for: request) | ||
guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { | ||
throw DataCacheError.urlRequestCreationFailed | ||
} | ||
return data | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Arthur Guiot on 09/04/2023. | ||
// | ||
|
||
import Foundation | ||
|
||
public protocol RemoteData<ObjectData>: Codable, Hashable, Identifiable where ID: Codable & Hashable { | ||
associatedtype ObjectData | ||
|
||
static func object(from: Data) -> ObjectData | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// | ||
// RemoteBinding.swift | ||
// | ||
// | ||
// Created by Arthur Guiot on 24/03/2023. | ||
// | ||
|
||
import SwiftUI | ||
|
||
@MainActor | ||
@dynamicMemberLookup | ||
public struct RemoteBinding<Delegate> where Delegate: RemoteObjectDelegate { | ||
var id: Delegate.Element.ID | ||
var request: RemoteRequest<Delegate.Element.ID> | ||
|
||
public subscript<T>(dynamicMember keyPath: WritableKeyPath<Delegate.Element, T>) -> Binding<T> { | ||
return Binding<T> { | ||
return Delegate.shared.store.object(id: id)![keyPath: keyPath] | ||
} set: { newValue, transaction in | ||
try? Delegate.shared.store.update(id: id, with: request) { (object: inout Delegate.Element) in | ||
object[keyPath: keyPath] = newValue | ||
} | ||
} | ||
} | ||
} | ||
|
||
public extension Binding { | ||
func unwrap<T>(defaultValue: T) -> Binding<T>! where Value == Optional<T> { | ||
Binding<T>(get: { self.wrappedValue ?? defaultValue }, set: { self.wrappedValue = $0 }) | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// | ||
// RemoteObject.swift | ||
// | ||
// | ||
// Created by Arthur Guiot on 21/03/2023. | ||
// | ||
|
||
import SwiftUI | ||
|
||
@propertyWrapper | ||
public struct RemoteObject<Delegate> : DynamicProperty where Delegate: RemoteObjectDelegate { | ||
|
||
@ObservedObject var store: Store<Delegate> | ||
|
||
var waitForRequest = false | ||
var request: RemoteRequest<Delegate.Element.ID>! | ||
|
||
public init(request: RemoteRequest<Delegate.Element.ID>) { | ||
self.store = Delegate.shared.store | ||
self.request = request | ||
self.store.realtimeController.subscribe(to: request) | ||
// Revalidate | ||
self.revalidate() | ||
} | ||
|
||
public init(waitForRequest: Bool) { | ||
self.store = Delegate.shared.store | ||
self.waitForRequest = true | ||
} | ||
|
||
public mutating func updateRequest(request: RemoteRequest<Delegate.Element.ID>) { | ||
if !waitForRequest { | ||
self.store.realtimeController.unsubscribe(to: self.request) | ||
self.store.realtimeController.subscribe(to: request) | ||
} | ||
self.waitForRequest = false | ||
self.request = request | ||
// Revalidate | ||
self.revalidate() | ||
} | ||
|
||
public func revalidate() { | ||
self.store.revalidate(request: request) | ||
} | ||
|
||
public var wrappedValue: Delegate.Element? { | ||
get { | ||
return store.objects(request: request).first | ||
} | ||
nonmutating set { | ||
guard let newValue = newValue else { return } | ||
do { | ||
try store.save(newValue, with: request) | ||
} catch { | ||
print("### Save to \(Delegate.Element.self) Store Error: \(error)") | ||
} | ||
} | ||
} | ||
|
||
public var projectedValue: Binding<Delegate.Element>? { | ||
guard self.wrappedValue != nil else { return nil } | ||
return .init(get: { self.wrappedValue! }, set: { self.wrappedValue = $0 }) | ||
} | ||
} |
Oops, something went wrong.