From e625e39eb2efab7e19ba5c9186f852ff20bada50 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 18 Nov 2024 17:24:15 +0100 Subject: [PATCH] Add websockets --- LeStorage.xcodeproj/project.pbxproj | 10 ++- LeStorage/StoreCenter.swift | 52 ++++++++++--- LeStorage/Utils/URLManager.swift | 24 ++++++ LeStorage/WebSocketManager.swift | 112 ++++++++++++++++++++++++++++ README.md | 2 +- 5 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 LeStorage/Utils/URLManager.swift create mode 100644 LeStorage/WebSocketManager.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 77d7966..7646a7c 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ C4D4779D2CB923720077713D /* DataLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D4779C2CB923720077713D /* DataLog.swift */; }; C4D4779F2CB92FD80077713D /* SyncedStorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D4779E2CB92FD80077713D /* SyncedStorable.swift */; }; C4D477A12CB9586A0077713D /* StoredCollection+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D477A02CB9586A0077713D /* StoredCollection+Sync.swift */; }; + C4FAE69A2CEB84B300790446 /* WebSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FAE6992CEB84B300790446 /* WebSocketManager.swift */; }; + C4FAE69C2CEB8E9500790446 /* URLManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FAE69B2CEB8E9500790446 /* URLManager.swift */; }; C4FC2E292C2B2EC30021F3BF /* StoreCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */; }; C4FC2E312C353E7B0021F3BF /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FC2E302C353E7B0021F3BF /* Log.swift */; }; /* End PBXBuildFile section */ @@ -85,6 +87,8 @@ C4D4779C2CB923720077713D /* DataLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLog.swift; sourceTree = ""; }; C4D4779E2CB92FD80077713D /* SyncedStorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncedStorable.swift; sourceTree = ""; }; C4D477A02CB9586A0077713D /* StoredCollection+Sync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredCollection+Sync.swift"; sourceTree = ""; }; + C4FAE6992CEB84B300790446 /* WebSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketManager.swift; sourceTree = ""; }; + C4FAE69B2CEB8E9500790446 /* URLManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLManager.swift; sourceTree = ""; }; C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCenter.swift; sourceTree = ""; }; C4FC2E302C353E7B0021F3BF /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -148,6 +152,7 @@ C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */, C4D477A02CB9586A0077713D /* StoredCollection+Sync.swift */, C456EFE12BE52379007388E2 /* StoredSingleton.swift */, + C4FAE6992CEB84B300790446 /* WebSocketManager.swift */, C4A47D932B7CF7C500ADC637 /* MicroStorage.swift */, C4A47D822B7665BC00ADC637 /* Wip */, C4A47D582B6D352900ADC637 /* Utils */, @@ -158,15 +163,16 @@ C4A47D582B6D352900ADC637 /* Utils */ = { isa = PBXGroup; children = ( - C467AAE22CD2466400D76CD2 /* Formatter.swift */, C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */, C4A47D6A2B71244100ADC637 /* Collection+Extension.swift */, C4D477962CB66EEA0077713D /* Date+Extensions.swift */, C4A47DAE2B85FD3800ADC637 /* Errors.swift */, C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */, C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */, + C467AAE22CD2466400D76CD2 /* Formatter.swift */, C4A47D832B7B97F000ADC637 /* KeychainStore.swift */, C4A47D522B6D2C5F00ADC637 /* Logger.swift */, + C4FAE69B2CEB8E9500790446 /* URLManager.swift */, ); path = Utils; sourceTree = ""; @@ -313,6 +319,7 @@ C4FC2E312C353E7B0021F3BF /* Log.swift in Sources */, C4D477A12CB9586A0077713D /* StoredCollection+Sync.swift in Sources */, C4A47D512B6D2C4E00ADC637 /* Codable+Extensions.swift in Sources */, + C4FAE69A2CEB84B300790446 /* WebSocketManager.swift in Sources */, C4D4779F2CB92FD80077713D /* SyncedStorable.swift in Sources */, C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */, C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */, @@ -332,6 +339,7 @@ C4A47D812B7665AD00ADC637 /* Migration.swift in Sources */, C4A47D9B2B7CFFDA00ADC637 /* ApiCall.swift in Sources */, C4A47D942B7CF7C500ADC637 /* MicroStorage.swift in Sources */, + C4FAE69C2CEB8E9500790446 /* URLManager.swift in Sources */, C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */, C425D4582B6D2519002A7B48 /* Store.swift in Sources */, C4D4779D2CB923720077713D /* DataLog.swift in Sources */, diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 6bac901..0974b22 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -17,14 +17,14 @@ public class StoreCenter { fileprivate var _stores: [String: Store] = [:] /// The URL of the django API - public var synchronizationApiURL: String? { - didSet { - if let url = synchronizationApiURL { - self._services = Services(url: url) - } - } - } - +// public var synchronizationApiURL: String? { +// didSet { +// if let url = synchronizationApiURL { +// self._services = Services(url: url) +// } +// } +// } + /// Indicates to Stored Collection if they can synchronize public var collectionsCanSynchronize: Bool = true @@ -38,6 +38,9 @@ public class StoreCenter { /// The services performing the API calls fileprivate var _services: Services? + /// The WebSocketManager that manages realtime synchronization + fileprivate var _webSocketManager: WebSocketManager? + /// The dictionary of registered StoredCollections fileprivate var _apiCallCollections: [String: any SomeCallCollection] = [:] @@ -56,6 +59,9 @@ public class StoreCenter { /// A list of username that cannot synchronize with the server fileprivate var _blackListedUserName: [String] = [] + /// The URL manager + fileprivate var _urlManager: URLManager? = nil + init() { // self._syncGetRequests = ApiCallCollection() @@ -69,6 +75,31 @@ public class StoreCenter { } } + public func configureURLs(httpScheme: String, domain: String) { + let urlManager: URLManager = URLManager(httpScheme: httpScheme, domain: domain) + self._urlManager = urlManager + self._services = Services(url: urlManager.api) + Logger.log("Sync URL: \(urlManager.api)") + + if self.userId != nil { + self._configureWebSocket() + } + } + + fileprivate func _configureWebSocket() { + guard let userId = self.userId else { + Logger.w("Tried to configure websocket but userId is nil") + return + } + guard let urlManager = self._urlManager else { + Logger.w("Tried to configure websocket no URL has been defined") + return + } + let url = urlManager.websocket(userId: userId) + self._webSocketManager = WebSocketManager(urlString: url) + Logger.log("websocket configured: \(url)") + } + /// Returns the service instance public func service() throws -> Services { if let service = self._services { @@ -135,6 +166,7 @@ public class StoreCenter { self._settingsStorage.update { settings in settings.userId = user.id settings.username = user.username + self._configureWebSocket() } } @@ -164,6 +196,8 @@ public class StoreCenter { settings.username = nil settings.userId = nil settings.lastSynchronization = nil + + self._webSocketManager = nil } } @@ -367,7 +401,7 @@ public class StoreCenter { Store.main.loadCollectionsFromServer() } - public func synchronizeLastUpdates() async throws { + func synchronizeLastUpdates() async throws { if let lastSync = self._settingsStorage.item.lastSynchronization { let getSyncData = GetSyncData() diff --git a/LeStorage/Utils/URLManager.swift b/LeStorage/Utils/URLManager.swift new file mode 100644 index 0000000..f1e0ff7 --- /dev/null +++ b/LeStorage/Utils/URLManager.swift @@ -0,0 +1,24 @@ +// +// URLManager.swift +// LeStorage +// +// Created by Laurent Morvillier on 18/11/2024. +// + +import Foundation + +struct URLManager { + + var httpScheme: String + var domain: String + private let apiPath: String = "roads" + + var api: String { + return "\(self.httpScheme)\(self.domain)/\(self.apiPath)/" + } + + func websocket(userId: String) -> String { + return "ws://\(self.domain)/ws/user/\(userId)/" + } + +} diff --git a/LeStorage/WebSocketManager.swift b/LeStorage/WebSocketManager.swift new file mode 100644 index 0000000..219d4d6 --- /dev/null +++ b/LeStorage/WebSocketManager.swift @@ -0,0 +1,112 @@ +// +// WebSocketManager.swift +// WebSocketTest +// +// Created by Laurent Morvillier on 30/08/2024. +// + +import Foundation +import SwiftUI +import Combine + +class WebSocketManager: ObservableObject { + + private var webSocketTask: URLSessionWebSocketTask? +// @Published var messages: [String] = [] + private var timer: Timer? + + @Published var status: String = "status" + + init(urlString: String) { + setupWebSocket(urlString: urlString) + } + + deinit { + disconnect() + } + + private func setupWebSocket(urlString: String) { +// guard let url = URL(string: "ws://127.0.0.1:8000/ws/user/test/") else { + guard let url = URL(string: urlString) else { + print("Invalid URL") + return + } + + let session = URLSession(configuration: .default) + webSocketTask = session.webSocketTask(with: url) + webSocketTask?.resume() + + receiveMessage() + + // Setup a ping timer to keep the connection alive + timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in + self.ping() + } + } + + private func receiveMessage() { + webSocketTask?.receive { result in + switch result { + case .failure(let error): + self.changeStatus(error.localizedDescription) + print("Error in receiving message: \(error)") + + self.webSocketTask?.resume() + case .success(let message): + switch message { + case .string(let text): + print("Received text: \(text)") + Task { + do { + try await StoreCenter.main.synchronizeLastUpdates() + } catch { + Logger.error(error) + } + } + + DispatchQueue.main.async { +// self.messages.append(text) + } + case .data(let data): + print("Received binary message: \(data)") + @unknown default: + print("received other = \(message)") + } + self.changeStatus("success") + + self.receiveMessage() + } + } + } + + func changeStatus(_ status: String) { + DispatchQueue.main.async { + self.status = status + } + } + + func send(_ message: String) { + webSocketTask?.send(.string(message)) { error in + if let error = error { + print("Error in sending message: \(error)") + self.changeStatus("send failed: \(error.localizedDescription)") + } + } + } + + private func ping() { + webSocketTask?.sendPing { error in + if let error = error { + print("Error in sending ping: \(error)") + self.changeStatus("ping failed: \(error.localizedDescription)") + } + } + } + + func disconnect() { + self.changeStatus("disconnected") + webSocketTask?.cancel(with: .goingAway, reason: nil) + timer?.invalidate() + } + +} diff --git a/README.md b/README.md index a1c7b0c..04ad5b6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You can store collections inside separate folders by creating other stores: # Sync - When registering your collection, you can choose to have it synchronized. To do that: - - Set `StoreCenter.main.synchronizationApiURL` + - Call `StoreCenter.main.configureURLs` - Pass `synchronized: true` when registering the collection - For each of your `ModelObject`, make sure that `resourceName()` returns the resource path of the endpoint, for example "cars" - Synchronization is expected to be done with a rest_framework API on a django server