Add websockets

sync2
Laurent 12 months ago
parent 23a34838e5
commit e625e39eb2
  1. 10
      LeStorage.xcodeproj/project.pbxproj
  2. 52
      LeStorage/StoreCenter.swift
  3. 24
      LeStorage/Utils/URLManager.swift
  4. 112
      LeStorage/WebSocketManager.swift
  5. 2
      README.md

@ -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 = "<group>"; };
C4D4779E2CB92FD80077713D /* SyncedStorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncedStorable.swift; sourceTree = "<group>"; };
C4D477A02CB9586A0077713D /* StoredCollection+Sync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredCollection+Sync.swift"; sourceTree = "<group>"; };
C4FAE6992CEB84B300790446 /* WebSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketManager.swift; sourceTree = "<group>"; };
C4FAE69B2CEB8E9500790446 /* URLManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLManager.swift; sourceTree = "<group>"; };
C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCenter.swift; sourceTree = "<group>"; };
C4FC2E302C353E7B0021F3BF /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -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 */,

@ -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()

@ -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)/"
}
}

@ -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()
}
}

@ -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

Loading…
Cancel
Save