fix issue where updated items needs to copy properties instead of the instance being replaced

sync2
Laurent 1 year ago
parent 56a2f6e618
commit f926a1fcbe
  1. 8
      LeStorage.xcodeproj/project.pbxproj
  2. 6
      LeStorage/ApiCallCollection.swift
  3. 12
      LeStorage/Codables/ApiCall.swift
  4. 4
      LeStorage/Codables/DataLog.swift
  5. 12
      LeStorage/Codables/FailedAPICall.swift
  6. 25
      LeStorage/Codables/GetSyncData.swift
  7. 7
      LeStorage/Codables/Log.swift
  8. 126
      LeStorage/Codables/SyncResponse.swift
  9. 84
      LeStorage/Services.swift
  10. 5
      LeStorage/Storable.swift
  11. 6
      LeStorage/Store.swift
  12. 59
      LeStorage/StoreCenter.swift
  13. 22
      LeStorage/StoredCollection.swift
  14. 2
      LeStorage/Utils/Errors.swift

@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400D7222CC2AF560092237C /* GetSyncData.swift */; };
C400D7252CC2B5CF0092237C /* SyncResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400D7242CC2B5CF0092237C /* SyncResponse.swift */; };
C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */ = {isa = PBXBuildFile; fileRef = C425D4382B6D24E1002A7B48 /* LeStorage.docc */; };
C425D4452B6D24E1002A7B48 /* LeStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = C425D4372B6D24E1002A7B48 /* LeStorage.h */; settings = {ATTRIBUTES = (Public, ); }; };
C425D4582B6D2519002A7B48 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4572B6D2519002A7B48 /* Store.swift */; };
@ -49,6 +51,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
C400D7222CC2AF560092237C /* GetSyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSyncData.swift; sourceTree = "<group>"; };
C400D7242CC2B5CF0092237C /* SyncResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncResponse.swift; sourceTree = "<group>"; };
C425D4342B6D24E1002A7B48 /* LeStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LeStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C425D4372B6D24E1002A7B48 /* LeStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LeStorage.h; sourceTree = "<group>"; };
C425D4382B6D24E1002A7B48 /* LeStorage.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = LeStorage.docc; sourceTree = "<group>"; };
@ -177,6 +181,8 @@
C45D35902C0A1DB5000F379F /* FailedAPICall.swift */,
C4FC2E302C353E7B0021F3BF /* Log.swift */,
C4A47D9A2B7CFFC500ADC637 /* Settings.swift */,
C400D7222CC2AF560092237C /* GetSyncData.swift */,
C400D7242CC2B5CF0092237C /* SyncResponse.swift */,
);
path = Codables;
sourceTree = "<group>";
@ -305,11 +311,13 @@
C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */,
C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */,
C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */,
C400D7252CC2B5CF0092237C /* SyncResponse.swift in Sources */,
C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */,
C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */,
C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */,
C4D477972CB66EEA0077713D /* Date+Extensions.swift in Sources */,
C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */,
C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */,
C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */,
C4A47D9C2B7CFFE000ADC637 /* Settings.swift in Sources */,
C4FC2E292C2B2EC30021F3BF /* StoreCenter.swift in Sources */,

@ -190,8 +190,14 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
case .delete:
let _: Empty = try await self._executeApiCall(apiCall)
case .get:
if T.self == GetSyncData.self {
let _: Empty = try await self._executeApiCall(apiCall)
} else {
let _: [T] = try await self._executeApiCall(apiCall)
}
// process GET
// what if it is a sync GET
}
} catch {
// Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:")
Logger.error(error)

@ -27,11 +27,11 @@ class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
/// The HTTP method of the call: post...
var method: HTTPMethod
/// The id of the underlying data
var dataId: String
/// The content of the call
var body: String
var body: String?
/// The id of the underlying data stored in the body
var dataId: String?
/// The number of times the call has been executed
var attemptsCount: Int = 0
@ -45,4 +45,8 @@ class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
self.body = body
}
func copy(from other: any Storable) {
fatalError("should not happen")
}
}

@ -30,4 +30,8 @@ class DataLog: ModelObject, Storable {
self.operation = operation
}
func copy(from other: any Storable) {
fatalError("should not happen")
}
}

@ -41,4 +41,16 @@ class FailedAPICall: SyncedModelObject, SyncedStorable {
self.authentication = authentication
}
func copy(from other: any Storable) {
guard let fac = other as? FailedAPICall else { return }
self.date = fac.date
self.callId = fac.callId
self.type = fac.type
self.apiCall = fac.apiCall
self.error = fac.error
self.authentication = fac.authentication
}
}

@ -0,0 +1,25 @@
//
// SyncData.swift
// LeStorage
//
// Created by Laurent Morvillier on 18/10/2024.
//
import Foundation
class GetSyncData: ModelObject, SyncedStorable {
static func filterByStoreIdentifier() -> Bool { return false }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
var lastUpdate: Date = Date()
static func resourceName() -> String {
return "data"
}
func copy(from other: any Storable) {
guard let getSyncData = other as? GetSyncData else { return }
self.lastUpdate = getSyncData.lastUpdate
}
}

@ -23,4 +23,11 @@ class Log: SyncedModelObject, SyncedStorable {
self.message = message
}
func copy(from other: any Storable) {
guard let log = other as? Log else { return }
self.date = log.date
self.message = log.message
}
}

@ -0,0 +1,126 @@
//
// SyncResponse.swift
// LeStorage
//
// Created by Laurent Morvillier on 18/10/2024.
//
import Foundation
struct SyncResponse: Codable {
let updates: [String: [Codable]]
let deletions: [String: [Int]]
let date: String?
enum CodingKeys: String, CodingKey {
case updates, deletions, date
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
deletions = try container.decode([String: [Int]].self, forKey: .deletions)
date = try container.decodeIfPresent(String.self, forKey: .date)
let updatesContainer = try container.nestedContainer(
keyedBy: DynamicCodingKeys.self, forKey: .updates)
var updatesDict = [String: [AnyCodable]]()
for key in updatesContainer.allKeys {
let swiftClass = try SyncResponse._classFromClassName(key.stringValue)
let decodedArray = try updatesContainer.decode([AnyCodable].self, forKey: key)
let typedArray = decodedArray.compactMap { $0.value as? AnyCodable }
updatesDict[key.stringValue] = typedArray
}
updates = updatesDict
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(deletions, forKey: .deletions)
try container.encodeIfPresent(date, forKey: .date)
var updatesContainer = container.nestedContainer(
keyedBy: DynamicCodingKeys.self, forKey: .updates)
for (key, value) in updates {
let encodableArray = value.map { AnyCodable($0) }
try updatesContainer.encode(
encodableArray, forKey: DynamicCodingKeys(stringValue: key)!)
}
}
struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
fileprivate static func _classFromClassName(_ className: String) throws -> Codable.Type {
let fullClassName = "PadelClub.\(className)"
let modelClass: AnyClass? = NSClassFromString(fullClassName)
if let type = modelClass as? Codable.Type {
return type
} else {
throw LeStorageError.cantFindClassFromName(name: className)
}
}
}
struct AnyCodable: Codable {
let value: Any
init(_ value: Any) {
self.value = value
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let intValue = try? container.decode(Int.self) {
value = intValue
} else if let doubleValue = try? container.decode(Double.self) {
value = doubleValue
} else if let boolValue = try? container.decode(Bool.self) {
value = boolValue
} else if let stringValue = try? container.decode(String.self) {
value = stringValue
} else if let arrayValue = try? container.decode([AnyCodable].self) {
value = arrayValue.map { $0.value }
} else if let dictionaryValue = try? container.decode([String: AnyCodable].self) {
value = dictionaryValue.mapValues { $0.value }
} else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: "AnyCodable value cannot be decoded")
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
case let intValue as Int:
try container.encode(intValue)
case let doubleValue as Double:
try container.encode(doubleValue)
case let boolValue as Bool:
try container.encode(boolValue)
case let stringValue as String:
try container.encode(stringValue)
case let arrayValue as [Any]:
try container.encode(arrayValue.map { AnyCodable($0) })
case let dictionaryValue as [String: Any]:
try container.encode(dictionaryValue.mapValues { AnyCodable($0) })
default:
throw EncodingError.invalidValue(
value,
EncodingError.Context(
codingPath: container.codingPath,
debugDescription: "AnyCodable value cannot be encoded"))
}
}
}

@ -87,9 +87,9 @@ public class Services {
_ request: URLRequest, apiCall: ApiCall<T>
) async throws -> V {
let debugURL = request.url?.absoluteString ?? ""
print("Run \(request.httpMethod ?? "") \(debugURL)")
// print("Run \(request.httpMethod ?? "") \(debugURL)")
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request)
print("response = \(String(data: task.0, encoding: .utf8) ?? "")")
print("\(apiCall.method.rawValue) \(String(describing: T.self)) => \(String(data: task.0, encoding: .utf8) ?? "")")
if let response = task.1 as? HTTPURLResponse {
let statusCode = response.statusCode
@ -97,6 +97,11 @@ public class Services {
switch statusCode {
case 200..<300: // success
try await StoreCenter.main.deleteApiCallById(type: T.self, id: apiCall.id)
if T.self == GetSyncData.self {
StoreCenter.main.synchronizeContent(task.0, decoder: self.jsonDecoder)
}
default: // error
Logger.log(
"Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")")
@ -120,8 +125,13 @@ public class Services {
Logger.w(message)
}
if !(V.self is Empty?.Type) {
return try jsonDecoder.decode(V.self, from: task.0)
return try self._decode(data: task.0)
}
fileprivate func _decode<V: Decodable>(data: Data) throws -> V {
if !(V.self is Empty?.Type || V.self is Empty.Type) {
return try jsonDecoder.decode(V.self, from: data)
} else {
return try jsonDecoder.decode(V.self, from: "{}".data(using: .utf8)!)
}
@ -133,9 +143,9 @@ public class Services {
/// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure
fileprivate func _runRequest<V: Decodable>(_ request: URLRequest) async throws -> V {
let debugURL = request.url?.absoluteString ?? ""
print("Run \(request.httpMethod ?? "") \(debugURL)")
// print("Run \(request.httpMethod ?? "") \(debugURL)")
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request)
print("response = \(String(data: task.0, encoding: .utf8) ?? "")")
print("\(request.httpMethod ?? "") \(debugURL) => \(String(data: task.0, encoding: .utf8) ?? "")")
if let response = task.1 as? HTTPURLResponse {
let statusCode = response.statusCode
@ -158,7 +168,7 @@ public class Services {
StoreCenter.main.log(message: message)
Logger.w(message)
}
return try jsonDecoder.decode(V.self, from: task.0)
return try self._decode(data: task.0)
}
/// Returns if the token is required for a request
@ -190,31 +200,31 @@ public class Services {
/// Returns a POST request for the resource
/// - Parameters:
/// - type: the type of the request resource
fileprivate func _postRequest<T: SyncedStorable>(type: T.Type) throws -> URLRequest {
let requiresToken = self._isTokenRequired(type: T.self, method: .post)
return try self._baseRequest(
servicePath: T.path(), method: .post, requiresToken: requiresToken)
}
/// Returns a PUT request for the resource
/// - Parameters:
/// - type: the type of the request resource
fileprivate func _putRequest<T: SyncedStorable>(type: T.Type, id: String) throws -> URLRequest {
let requiresToken = self._isTokenRequired(type: T.self, method: .put)
return try self._baseRequest(
servicePath: T.path(id: id), method: .put, requiresToken: requiresToken)
}
/// Returns a DELETE request for the resource
/// - Parameters:
/// - type: the type of the request resource
fileprivate func _deleteRequest<T: SyncedStorable>(type: T.Type, id: String) throws
-> URLRequest
{
let requiresToken = self._isTokenRequired(type: T.self, method: .delete)
return try self._baseRequest(
servicePath: T.path(id: id), method: .delete, requiresToken: requiresToken)
}
// fileprivate func _postRequest<T: SyncedStorable>(type: T.Type) throws -> URLRequest {
// let requiresToken = self._isTokenRequired(type: T.self, method: .post)
// return try self._baseRequest(
// servicePath: T.path(), method: .post, requiresToken: requiresToken)
// }
//
// /// Returns a PUT request for the resource
// /// - Parameters:
// /// - type: the type of the request resource
// fileprivate func _putRequest<T: SyncedStorable>(type: T.Type, id: String) throws -> URLRequest {
// let requiresToken = self._isTokenRequired(type: T.self, method: .put)
// return try self._baseRequest(
// servicePath: T.path(id: id), method: .put, requiresToken: requiresToken)
// }
//
// /// Returns a DELETE request for the resource
// /// - Parameters:
// /// - type: the type of the request resource
// fileprivate func _deleteRequest<T: SyncedStorable>(type: T.Type, id: String) throws
// -> URLRequest
// {
// let requiresToken = self._isTokenRequired(type: T.self, method: .delete)
// return try self._baseRequest(
// servicePath: T.path(id: id), method: .delete, requiresToken: requiresToken)
// }
/// Returns the base URLRequest for a ServiceConf instance
/// - Parameters:
@ -286,7 +296,7 @@ public class Services {
guard let url = URL(string: urlString) else {
throw ServiceError.urlCreationError(url: urlString)
}
guard let bodyData = apiCall.body.data(using: .utf8) else {
guard let body = apiCall.body, let bodyData = body.data(using: .utf8) else {
throw ServiceError.cantDecodeData(content: apiCall.body)
}
@ -352,9 +362,9 @@ public class Services {
/// - request: The synchronization request
fileprivate func _runGetSyncLogRequest(_ request: URLRequest) async throws {
let debugURL = request.url?.absoluteString ?? ""
print("Run \(request.httpMethod ?? "") \(debugURL)")
// print("Run \(request.httpMethod ?? "") \(debugURL)")
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request)
print("response = \(String(data: task.0, encoding: .utf8) ?? "")")
print("\(request.httpMethod ?? "") \(debugURL) => \(String(data: task.0, encoding: .utf8) ?? "")")
if let response = task.1 as? HTTPURLResponse {
let statusCode = response.statusCode
@ -422,7 +432,7 @@ public class Services {
/// Executes an ApiCall
func runApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V {
let request = try self._syncRequest(from: apiCall)
print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)")
// print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)")
return try await self._runRequest(request, apiCall: apiCall)
}
@ -433,7 +443,7 @@ public class Services {
let url = try self._url(from: apiCall)
var request = URLRequest(url: url)
request.httpMethod = apiCall.method.rawValue
request.httpBody = apiCall.body.data(using: .utf8)
request.httpBody = apiCall.body?.data(using: .utf8)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if self._isTokenRequired(type: T.self, method: apiCall.method) {

@ -28,10 +28,13 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol {
/// so when we do that on the server, we also need to do it locally
func deleteDependencies()
static var relationshipNames: [String] { get }
// static var relationshipNames: [String] { get }
/// A method called after the instance has been deleted from its StoredCollection
func hasBeenDeleted()
func copy(from other: any Storable)
}
extension Storable {

@ -147,7 +147,7 @@ open class Store {
/// Loads all collection with the data from the server
public func loadCollectionsFromServer() {
for collection in self._StoredCollections() {
for collection in self._syncedCollections() {
Task {
try? await collection.loadDataFromServerIfAllowed()
}
@ -156,7 +156,7 @@ open class Store {
/// Loads all synchronized collection with server data if they don't already have a local file
public func loadCollectionsFromServerIfNoFile() {
for collection in self._StoredCollections() {
for collection in self._syncedCollections() {
Task {
do {
try await collection.loadCollectionsFromServerIfNoFile()
@ -167,7 +167,7 @@ open class Store {
}
}
fileprivate func _StoredCollections() -> [any SomeSyncedCollection] {
fileprivate func _syncedCollections() -> [any SomeSyncedCollection] {
return self._collections.values.compactMap { $0 as? any SomeSyncedCollection }
}

@ -32,7 +32,8 @@ public class StoreCenter {
public var forceNoSynchronization: Bool = false
/// A store for the Settings object
fileprivate var _settingsStorage: MicroStorage<Settings> = MicroStorage(fileName: "settings.json")
fileprivate var _settingsStorage: MicroStorage<Settings> = MicroStorage(
fileName: "settings.json")
/// The services performing the API calls
fileprivate var _services: Services?
@ -174,8 +175,8 @@ public class StoreCenter {
do {
return try keychainStore.getValue()
} catch {
let deviceId: String = UIDevice.current.identifierForVendor?.uuidString ??
UUID().uuidString
let deviceId: String =
UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
do {
try keychainStore.add(value: deviceId)
} catch {
@ -275,7 +276,9 @@ public class StoreCenter {
}
/// Executes an ApiCall
fileprivate func _executeApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V {
fileprivate func _executeApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>)
async throws -> V
{
return try await self.service().runApiCall(apiCall)
}
@ -288,7 +291,8 @@ public class StoreCenter {
/// Returns whether the collection can synchronize
fileprivate func _canSynchronise() -> Bool {
return !self.forceNoSynchronization && self.collectionsCanSynchronize && self.userIsAllowed()
return !self.forceNoSynchronization && self.collectionsCanSynchronize
&& self.userIsAllowed()
}
/// Transmit the insertion request to the ApiCall collection
@ -328,6 +332,10 @@ public class StoreCenter {
// MARK: - Synchronization
fileprivate func _createSyncApiCallCollection() {
self.loadApiCallCollection(type: GetSyncData.self)
}
public func initialSynchronization() {
self._settingsStorage.update { settings in
settings.lastSynchronization = Date()
@ -343,7 +351,8 @@ public class StoreCenter {
func synchronizeContent(_ data: Data, decoder: JSONDecoder) {
do {
guard let json = try JSONSerialization.jsonObject(with: data, options: [])
guard
let json = try JSONSerialization.jsonObject(with: data, options: [])
as? [String: Any]
else {
Logger.w("data unrecognized")
@ -370,7 +379,6 @@ public class StoreCenter {
if let dateString: String = json["date"] as? String,
let syncDate = Date.iso8601Formatter.date(from: dateString) {
self._settingsStorage.update { settings in
settings.lastSynchronization = syncDate
}
@ -393,7 +401,8 @@ public class StoreCenter {
for updateItem in updateArray {
do {
let jsonData = try JSONSerialization.data(withJSONObject: updateItem, options: [])
let jsonData = try JSONSerialization.data(
withJSONObject: updateItem, options: [])
let decodedObject = try decoder.decode(type, from: jsonData)
let storeId: String? = decodedObject.getStoreId()
@ -418,11 +427,13 @@ public class StoreCenter {
if let object = updateItem["data"] {
do {
let jsonData = try JSONSerialization.data(withJSONObject: object, options: [])
let jsonData = try JSONSerialization.data(
withJSONObject: object, options: [])
let decodedObject = try decoder.decode(type, from: jsonData)
let storeId = updateItem["storeId"] as? String
StoreCenter.main.synchronizationDelete(instance: decodedObject, storeId: storeId)
StoreCenter.main.synchronizationDelete(
instance: decodedObject, storeId: storeId)
} catch {
Logger.error(error)
}
@ -452,13 +463,15 @@ public class StoreCenter {
}
fileprivate func _hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool {
return self._dataLogs.contains(where: { $0.dataId == instance.stringId && $0.operation == .delete })
return self._dataLogs.contains(where: {
$0.dataId == instance.stringId && $0.operation == .delete
})
}
func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?) {
let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance)
if !hasAlreadyBeenDeleted {
DispatchQueue.main.async {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self._store(id: storeId)?.addOrUpdateIfNewer(instance)
}
}
@ -485,7 +498,8 @@ public class StoreCenter {
}
fileprivate func _addDataLog<T: Storable>(_ instance: T, method: HTTPMethod) {
let dataLog = DataLog(dataId: instance.stringId, modelName: String(describing: T.self), operation: method)
let dataLog = DataLog(
dataId: instance.stringId, modelName: String(describing: T.self), operation: method)
self._dataLogs.addOrUpdate(instance: dataLog)
}
@ -530,7 +544,9 @@ public class StoreCenter {
/// If configured for, logs and send to the server a failed API call
/// Logs a failed API call that has failed at least 5 times
func logFailedAPICall(_ apiCallId: String, request: URLRequest, collectionName: String, error: String) {
func logFailedAPICall(
_ apiCallId: String, request: URLRequest, collectionName: String, error: String
) {
guard let failedAPICallsCollection = self._failedAPICallsCollection,
let collection = self._apiCallCollections[collectionName],
@ -542,12 +558,16 @@ public class StoreCenter {
Task {
if let apiCall = await collection.findCallById(apiCallId) {
if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) && apiCall.attemptsCount > 6 {
if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId })
&& apiCall.attemptsCount > 6
{
do {
let authValue = request.allHTTPHeaderFields?["Authorization"]
let string = try apiCall.jsonString()
let failedAPICall = FailedAPICall(callId: apiCall.id, type: collectionName, apiCall: string, error: error, authentication: authValue)
let failedAPICall = FailedAPICall(
callId: apiCall.id, type: collectionName, apiCall: string, error: error,
authentication: authValue)
DispatchQueue.main.async {
failedAPICallsCollection.addOrUpdate(instance: failedAPICall)
@ -567,12 +587,15 @@ public class StoreCenter {
guard let failedAPICallsCollection = self._failedAPICallsCollection,
let body: Data = request.httpBody,
let bodyString = String(data: body, encoding: .utf8),
let url = request.url?.absoluteString else {
let url = request.url?.absoluteString
else {
return
}
let authValue = request.allHTTPHeaderFields?["Authorization"]
let failedAPICall = FailedAPICall(callId: request.hashValue.formatted(), type: url, apiCall: bodyString, error: error, authentication: authValue)
let failedAPICall = FailedAPICall(
callId: request.hashValue.formatted(), type: url, apiCall: bodyString, error: error,
authentication: authValue)
failedAPICallsCollection.addOrUpdate(instance: failedAPICall)
}

@ -33,11 +33,14 @@ protocol SomeSyncedCollection: SomeCollection {
}
extension Notification.Name {
public static let CollectionDidLoad: Notification.Name = Notification.Name.init("notification.collectionDidLoad")
public static let CollectionDidChange: Notification.Name = Notification.Name.init("notification.collectionDidChange")
public static let CollectionDidLoad: Notification.Name = Notification.Name.init(
"notification.collectionDidLoad")
public static let CollectionDidChange: Notification.Name = Notification.Name.init(
"notification.collectionDidChange")
}
public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollection, CollectionHolder {
public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollection, CollectionHolder
{
/// Doesn't write the collection in a file
fileprivate(set) var inMemory: Bool = false
@ -58,7 +61,8 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
self._scheduleWrite()
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name.CollectionDidChange, object: self)
NotificationCenter.default.post(
name: NSNotification.Name.CollectionDidChange, object: self)
}
self._hasChanged = false
}
@ -158,7 +162,8 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
func setAsLoaded() {
self.hasLoaded = true
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name.CollectionDidLoad, object: self)
NotificationCenter.default.post(
name: NSNotification.Name.CollectionDidLoad, object: self)
}
}
@ -170,7 +175,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
/// Updates the whole index with the items array
fileprivate func _updateIndexIfNecessary() {
if let _ = self._indexes {
if self._indexes != nil {
self._indexes = self.items.dictionary { $0.id }
}
}
@ -276,7 +281,10 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
}
func updateItem(_ instance: T, index: Int) {
self.items[index] = instance
// var existingItem = self.items[index]
// existingItem.hasBeenDeleted()
// self._copy(instance, into: &existingItem) // we need to keep the instance alive for screen to refresh
self.items[index].copy(from: instance)
instance.store = self.store
self._indexes?[instance.id] = instance
}

@ -26,7 +26,7 @@ public enum ServiceError: Error {
case missingUserName
case missingUserId
case responseError(response: String)
case cantDecodeData(content: String)
case cantDecodeData(content: String?)
}
public enum UUIDError: Error {

Loading…
Cancel
Save