Implement incremental sync and offline recovery
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user