Skip to content

Commit

Permalink
V2 (#4)
Browse files Browse the repository at this point in the history
* 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
arguiot authored Nov 14, 2024
1 parent dfcd44f commit 1c011a1
Show file tree
Hide file tree
Showing 56 changed files with 3,292 additions and 3,471 deletions.
17 changes: 0 additions & 17 deletions .github/workflows/swift.yml

This file was deleted.

25 changes: 0 additions & 25 deletions Package.resolved

This file was deleted.

14 changes: 8 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "Gravity",
platforms: [
.macOS(.v10_15),
.macOS(.v11),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6)
Expand All @@ -20,15 +20,17 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(name: "Starscream", url: "https://github.com/daltoniam/Starscream", from: "3.1.1"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Gravity",
dependencies: [
"Starscream"
])
name: "Gravity",
dependencies: []
),
.testTarget(
name: "GravityTests",
dependencies: ["Gravity"]
)
]
)
105 changes: 105 additions & 0 deletions Sources/Gravity/DataCache/DataCache.swift
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
}
}
14 changes: 14 additions & 0 deletions Sources/Gravity/DataCache/RemoteData.swift
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
}
31 changes: 31 additions & 0 deletions Sources/Gravity/RemoteBinding.swift
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 })
}
}
64 changes: 64 additions & 0 deletions Sources/Gravity/RemoteObject.swift
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 })
}
}
Loading

0 comments on commit 1c011a1

Please sign in to comment.