Implement incremental sync and offline recovery

This commit is contained in:
diyaa
2026-06-15 22:31:23 +02:00
parent fa7727d572
commit 295c6c1d9b
25 changed files with 2293 additions and 494 deletions
@@ -280,96 +280,157 @@ public struct SyncCursor: Codable, Hashable, Sendable {
}
public struct SyncEvent: Codable, Hashable, Sendable {
public var cursor: SyncCursor
public var entityType: String
public var entityId: String
public var action: String
public var eventId: String
public var track: RemoteTrack?
public var deletedTrackId: String?
public var createdAt: String
public init(
cursor: SyncCursor,
entityType: String,
entityId: String,
action: String,
eventId: String
track: RemoteTrack? = nil,
deletedTrackId: String? = nil,
createdAt: String
) {
self.cursor = cursor
self.entityType = entityType
self.entityId = entityId
self.action = action
self.eventId = eventId
self.track = track
self.deletedTrackId = deletedTrackId
self.createdAt = createdAt
}
private enum CodingKeys: String, CodingKey {
case cursor
case entityType
case entityId
case action
case track
case deletedTrackId
case createdAt
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
cursor = SyncCursor(
value: try container.decode(String.self, forKey: .cursor)
)
entityType = try container.decode(String.self, forKey: .entityType)
entityId = try container.decode(String.self, forKey: .entityId)
action = try container.decode(String.self, forKey: .action)
track = try container.decodeIfPresent(RemoteTrack.self, forKey: .track)
deletedTrackId = try container.decodeIfPresent(String.self, forKey: .deletedTrackId)
createdAt = try container.decode(String.self, forKey: .createdAt)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(cursor.value, forKey: .cursor)
try container.encode(entityType, forKey: .entityType)
try container.encode(entityId, forKey: .entityId)
try container.encode(action, forKey: .action)
try container.encodeIfPresent(track, forKey: .track)
try container.encodeIfPresent(deletedTrackId, forKey: .deletedTrackId)
try container.encode(createdAt, forKey: .createdAt)
}
}
public struct SyncBootstrapResponse: Codable, Hashable, Sendable {
public var nextCursor: SyncCursor
public var tracks: [LibraryTrack]
public var events: [SyncEvent]
public var deletedTrackIds: [String]
public var tracks: [RemoteTrack]
public var serverTime: String
public init(
nextCursor: SyncCursor,
tracks: [LibraryTrack],
events: [SyncEvent],
deletedTrackIds: [String],
tracks: [RemoteTrack],
serverTime: String
) {
self.nextCursor = nextCursor
self.tracks = tracks
self.events = events
self.deletedTrackIds = deletedTrackIds
self.serverTime = serverTime
}
private enum CodingKeys: String, CodingKey {
case nextCursor
case tracks
case events
case deletedTrackIds
case serverTime
}
private struct WireTrack: Codable {
var id: String?
var title: String?
var artist: String?
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let wireTracks = try container.decode([WireTrack].self, forKey: .tracks)
nextCursor = SyncCursor(
value: try container.decode(String.self, forKey: .nextCursor)
)
tracks = wireTracks.map { track in
LibraryTrack(
id: track.id ?? UUID().uuidString,
title: track.title ?? "Unknown Title",
artist: track.artist ?? "Unknown Artist",
album: nil,
durationSeconds: nil,
localFilePath: "",
sha256: nil
)
}
events = try container.decode([SyncEvent].self, forKey: .events)
deletedTrackIds = try container.decode([String].self, forKey: .deletedTrackIds)
tracks = try container.decode([RemoteTrack].self, forKey: .tracks)
serverTime = try container.decode(String.self, forKey: .serverTime)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let wireTracks = tracks.map { track in
WireTrack(
id: track.id,
title: track.title,
artist: track.artist
)
}
try container.encode(nextCursor.value, forKey: .nextCursor)
try container.encode(wireTracks, forKey: .tracks)
try container.encode(tracks, forKey: .tracks)
try container.encode(serverTime, forKey: .serverTime)
}
}
public struct SyncChangesResponse: Codable, Hashable, Sendable {
public var nextCursor: SyncCursor
public var hasMore: Bool
public var requiresBootstrap: Bool
public var reason: String?
public var events: [SyncEvent]
public var serverTime: String
public init(
nextCursor: SyncCursor,
hasMore: Bool,
requiresBootstrap: Bool,
reason: String? = nil,
events: [SyncEvent],
serverTime: String
) {
self.nextCursor = nextCursor
self.hasMore = hasMore
self.requiresBootstrap = requiresBootstrap
self.reason = reason
self.events = events
self.serverTime = serverTime
}
private enum CodingKeys: String, CodingKey {
case nextCursor
case hasMore
case requiresBootstrap
case reason
case events
case serverTime
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
nextCursor = SyncCursor(
value: try container.decode(String.self, forKey: .nextCursor)
)
hasMore = try container.decode(Bool.self, forKey: .hasMore)
requiresBootstrap = try container.decode(Bool.self, forKey: .requiresBootstrap)
reason = try container.decodeIfPresent(String.self, forKey: .reason)
events = try container.decode([SyncEvent].self, forKey: .events)
serverTime = try container.decode(String.self, forKey: .serverTime)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(nextCursor.value, forKey: .nextCursor)
try container.encode(hasMore, forKey: .hasMore)
try container.encode(requiresBootstrap, forKey: .requiresBootstrap)
try container.encodeIfPresent(reason, forKey: .reason)
try container.encode(events, forKey: .events)
try container.encode(deletedTrackIds, forKey: .deletedTrackIds)
try container.encode(serverTime, forKey: .serverTime)
}
}