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
@@ -176,7 +176,8 @@ final class OfflineLibraryServiceTests: XCTestCase {
let syncService = RemoteLibrarySyncService(
repository: DefaultRemoteLibraryRepository(
apiClient: OfflineLibraryMockAPIClient(audioAssetData: sampleMp3Data(seed: track.assetId)),
store: remoteLibraryStore
store: remoteLibraryStore,
syncCursorStore: InMemoryRemoteLibrarySyncCursorStore()
),
downloadStateStore: downloadStateStore,
audioFileStore: audioFileStore,
@@ -226,8 +227,10 @@ final class OfflineLibraryServiceTests: XCTestCase {
let remoteLibraryStore = InMemoryRemoteLibraryStore()
let audioData = sampleMp3Data(seed: track.assetId)
let apiClient = OfflineLibraryMockAPIClient(
remoteLibraryResponse: RemoteLibraryResponseDTO(
tracks: [makeRemoteTrackDTO(from: track)]
bootstrapResponse: SyncBootstrapResponse(
nextCursor: SyncCursor(value: "1"),
tracks: [track],
serverTime: "2026-05-30T08:00:00.000Z"
),
audioAssetData: audioData,
artworkDataByArtworkID: [
@@ -240,7 +243,8 @@ final class OfflineLibraryServiceTests: XCTestCase {
let syncService = RemoteLibrarySyncService(
repository: DefaultRemoteLibraryRepository(
apiClient: apiClient,
store: remoteLibraryStore
store: remoteLibraryStore,
syncCursorStore: InMemoryRemoteLibrarySyncCursorStore()
),
downloadStateStore: downloadStateStore,
audioFileStore: audioFileStore,
@@ -284,8 +288,10 @@ final class OfflineLibraryServiceTests: XCTestCase {
makeRemoteTrack(trackId: "track-2", assetId: "asset-2", title: "Track 2", artworkId: "artwork-2"),
]
let apiClient = OfflineLibraryMockAPIClient(
remoteLibraryResponse: RemoteLibraryResponseDTO(
tracks: tracks.map { makeRemoteTrackDTO(from: $0) }
bootstrapResponse: SyncBootstrapResponse(
nextCursor: SyncCursor(value: "1"),
tracks: tracks,
serverTime: "2026-05-30T08:00:00.000Z"
),
audioAssetDataByAssetID: [
"asset-1": sampleMp3Data(seed: "asset-1"),
@@ -304,7 +310,8 @@ final class OfflineLibraryServiceTests: XCTestCase {
let firstRepository = DefaultRemoteLibraryRepository(
apiClient: apiClient,
store: try FileRemoteLibraryStore(fileURL: remoteLibraryFileURL)
store: try FileRemoteLibraryStore(fileURL: remoteLibraryFileURL),
syncCursorStore: InMemoryRemoteLibrarySyncCursorStore()
)
let firstDownloadStateStore = try FileRemoteTrackDownloadStateStore(fileURL: downloadStateFileURL)
let firstAudioStore = try FileOfflineAudioFileStore(baseDirectoryURL: audioDirectory)
@@ -329,7 +336,8 @@ final class OfflineLibraryServiceTests: XCTestCase {
let relaunchedRepository = DefaultRemoteLibraryRepository(
apiClient: apiClient,
store: try FileRemoteLibraryStore(fileURL: remoteLibraryFileURL)
store: try FileRemoteLibraryStore(fileURL: remoteLibraryFileURL),
syncCursorStore: InMemoryRemoteLibrarySyncCursorStore(cursor: SyncCursor(value: "1"))
)
let relaunchedDownloadStateStore = try FileRemoteTrackDownloadStateStore(fileURL: downloadStateFileURL)
let relaunchedAudioStore = try FileOfflineAudioFileStore(baseDirectoryURL: audioDirectory)
@@ -404,18 +412,22 @@ private actor InMemoryRemoteLibraryRepository: RemoteLibraryRepository {
}
private struct OfflineLibraryMockAPIClient: VelodyAPIClient {
let remoteLibraryResponse: RemoteLibraryResponseDTO?
let bootstrapResponse: SyncBootstrapResponse
let audioAssetData: Data?
let audioAssetDataByAssetID: [String: Data]
let artworkDataByArtworkID: [String: Data]
init(
remoteLibraryResponse: RemoteLibraryResponseDTO? = nil,
bootstrapResponse: SyncBootstrapResponse = SyncBootstrapResponse(
nextCursor: SyncCursor(value: "0"),
tracks: [],
serverTime: "2026-05-30T08:00:00.000Z"
),
audioAssetData: Data? = nil,
audioAssetDataByAssetID: [String: Data] = [:],
artworkDataByArtworkID: [String: Data] = [:]
) {
self.remoteLibraryResponse = remoteLibraryResponse
self.bootstrapResponse = bootstrapResponse
self.audioAssetData = audioAssetData
self.audioAssetDataByAssetID = audioAssetDataByAssetID
self.artworkDataByArtworkID = artworkDataByArtworkID
@@ -444,11 +456,17 @@ private struct OfflineLibraryMockAPIClient: VelodyAPIClient {
}
func fetchSyncBootstrap() async throws -> SyncBootstrapResponse {
SyncBootstrapResponse(
nextCursor: SyncCursor(value: "0"),
tracks: [],
bootstrapResponse
}
func fetchSyncChanges(
cursor: SyncCursor
) async throws -> SyncChangesResponse {
SyncChangesResponse(
nextCursor: cursor,
hasMore: false,
requiresBootstrap: false,
events: [],
deletedTrackIds: [],
serverTime: "2026-05-30T08:00:00.000Z"
)
}
@@ -457,7 +475,9 @@ private struct OfflineLibraryMockAPIClient: VelodyAPIClient {
deviceId: String
) async throws -> RemoteLibraryResponseDTO {
_ = deviceId
return remoteLibraryResponse ?? RemoteLibraryResponseDTO(tracks: [])
return RemoteLibraryResponseDTO(
tracks: bootstrapResponse.tracks.map { makeRemoteTrackDTO(from: $0) }
)
}
func downloadAudioAsset(