From c82f3296b505cda247768ea9229640e6c678ed55 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 16 Sep 2025 11:52:50 +0200 Subject: [PATCH 1/5] added attachment helpers changelog. Added waitForInit --- CHANGELOG.md | 8 ++ Package.swift | 2 +- .../attachments/AttachmentQueue.swift | 38 +++++++++ Tests/PowerSyncTests/AttachmentTests.swift | 78 +++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 062cc31..5772aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.6.0 (unreleased) + +* Update minimum MacOS target to v12 +* [Attachment Helpers] Added automatic verification or records' `local_uri` values on `AttachmentQueue` initialization. +initialization can be awaited with `AttachmentQueue.waitForInit()`. `AttachmentQueue.startSync()` also performs this verification. +`waitForInit()` is only recommended if `startSync` is not called directly after creating the queue. + + ## 1.5.1 * Update core extension to 0.4.5 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.5)) diff --git a/Package.swift b/Package.swift index aac1063..ed380de 100644 --- a/Package.swift +++ b/Package.swift @@ -53,7 +53,7 @@ let package = Package( name: packageName, platforms: [ .iOS(.v13), - .macOS(.v10_15), + .macOS(.v12), .watchOS(.v9) ], products: [ diff --git a/Sources/PowerSync/attachments/AttachmentQueue.swift b/Sources/PowerSync/attachments/AttachmentQueue.swift index 2ab32c2..2dc76ff 100644 --- a/Sources/PowerSync/attachments/AttachmentQueue.swift +++ b/Sources/PowerSync/attachments/AttachmentQueue.swift @@ -10,6 +10,12 @@ public protocol AttachmentQueueProtocol: Sendable { var localStorage: any LocalStorageAdapter { get } var downloadAttachments: Bool { get } + /// Waits for automatically triggered initialization. + /// This ensures all attachment records have been verified before use. + /// The `startSync` method also performs this verification. This call is not + /// needed if `startSync` is called after creating the Attachment Queue. + func waitForInit() async throws + /// Starts the attachment sync process func startSync() async throws @@ -287,6 +293,9 @@ public actor AttachmentQueue: AttachmentQueueProtocol { private let _getLocalUri: @Sendable (_ filename: String) async -> String + private let initializedSubject = PassthroughSubject, Never>() + private var initializationResult: Result? + /// Initializes the attachment queue /// - Parameters match the stored properties public init( @@ -342,8 +351,37 @@ public actor AttachmentQueue: AttachmentQueueProtocol { try await attachmentsService.withContext { context in try await self.verifyAttachments(context: context) } + await self.setInitializedResult(.success(())) } catch { self.logger.error("Error verifying attachments: \(error.localizedDescription)", tag: logTag) + await self.setInitializedResult(.failure(error)) + } + } + } + + /// Actor isolated method to set the initialization result + private func setInitializedResult(_ result: Result) { + initializationResult = result + initializedSubject.send(result) + } + + public func waitForInit() async throws { + if let isInitialized = initializationResult { + switch isInitialized { + case .success: + return + case let .failure(error): + throw error + } + } + + // Wait for the result asynchronously + for try await result in initializedSubject.values { + switch result { + case .success: + return + case let .failure(error): + throw error } } } diff --git a/Tests/PowerSyncTests/AttachmentTests.swift b/Tests/PowerSyncTests/AttachmentTests.swift index 46cb114..9aa40fb 100644 --- a/Tests/PowerSyncTests/AttachmentTests.swift +++ b/Tests/PowerSyncTests/AttachmentTests.swift @@ -173,6 +173,84 @@ final class AttachmentTests: XCTestCase { try await queue.clearQueue() try await queue.close() } + + func testAttachmentInitVerification() async throws { + actor MockRemoteStorage: RemoteStorageAdapter { + func uploadFile( + fileData _: Data, + attachment _: Attachment + ) async throws {} + + func downloadFile(attachment _: Attachment) async throws -> Data { + return Data([1, 2, 3]) + } + + func deleteFile(attachment _: Attachment) async throws {} + } + + // Create an attachments record which has an invalid local_uri + let attachmentsDirectory = getAttachmentDirectory() + + try FileManager.default.createDirectory( + at: URL(fileURLWithPath: attachmentsDirectory), + withIntermediateDirectories: true, + attributes: nil + ) + + let filename = "test.jpeg" + + try Data("1".utf8).write( + to: URL(fileURLWithPath: attachmentsDirectory).appendingPathComponent(filename) + ) + try await database.execute( + sql: """ + INSERT OR REPLACE INTO + attachments (id, timestamp, filename, local_uri, media_type, size, state, has_synced, meta_data) + VALUES + (uuid(), ?, ?, ?, ?, ?, ?, ?, ?) + """, + parameters: [ + Date().ISO8601Format(), + filename, + // This is a broken local_uri + URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("not_attachments/test.jpeg").path, + "application/jpeg", + 1, + AttachmentState.synced.rawValue, + 1, + "" + ] + ) + + let mockedRemote = MockRemoteStorage() + + let queue = AttachmentQueue( + db: database, + remoteStorage: mockedRemote, + attachmentsDirectory: attachmentsDirectory, + watchAttachments: { [database = database!] in try database.watch(options: WatchOptions( + sql: "SELECT photo_id FROM users WHERE photo_id IS NOT NULL", + mapper: { cursor in try WatchedAttachmentItem( + id: cursor.getString(name: "photo_id"), + fileExtension: "jpg" + ) } + )) } + ) + + try await queue.waitForInit() + + // the attachment should have been corrected in the init + let attachments = try await queue.attachmentsService.withContext { context in + try await context.getAttachments() + } + + guard let firstAttachment = attachments.first else { + XCTFail("Could not find the attachment record") + return + } + + XCTAssert(firstAttachment.localUri == URL(fileURLWithPath: attachmentsDirectory).appendingPathComponent(filename).path) + } } public enum WaitForMatchError: Error { From dd0e29ab0bd32a65fc0dba3779537d7fc43dcdba Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 16 Sep 2025 11:56:38 +0200 Subject: [PATCH 2/5] add comment --- Sources/PowerSync/attachments/AttachmentQueue.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/PowerSync/attachments/AttachmentQueue.swift b/Sources/PowerSync/attachments/AttachmentQueue.swift index 2dc76ff..3435db0 100644 --- a/Sources/PowerSync/attachments/AttachmentQueue.swift +++ b/Sources/PowerSync/attachments/AttachmentQueue.swift @@ -346,6 +346,8 @@ public actor AttachmentQueue: AttachmentQueueProtocol { syncThrottle: self.syncThrottleDuration ) + // Storing a reference to this task is non-trivial since we capture + // Self. Swift 6 Strict concurrency checking will complain about a nonisolated initializer Task { do { try await attachmentsService.withContext { context in From 026e74d2f43ac3cfc2864167d918f7b6fbbca2f5 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 16 Sep 2025 14:13:43 +0200 Subject: [PATCH 3/5] set xcode version --- .github/workflows/build_and_test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index f13117d..f9753df 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -13,7 +13,8 @@ jobs: - name: Set up XCode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: latest-stable + # TODO: Update to latest-stable once GH installs iOS 26 simulators + xcode-version: "^16.4.0" - name: Build and Test run: | xcodebuild test -scheme PowerSync-Package -destination "platform=iOS Simulator,name=iPhone 16" From ac1e8a5b6cdbeb30d90110ad42ea08eb4fdfff98 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 16 Sep 2025 14:19:08 +0200 Subject: [PATCH 4/5] update iOS target --- CHANGELOG.md | 1 + Package.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5772aff..e53ec45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.6.0 (unreleased) * Update minimum MacOS target to v12 +* Update minimum iOS target to v15 * [Attachment Helpers] Added automatic verification or records' `local_uri` values on `AttachmentQueue` initialization. initialization can be awaited with `AttachmentQueue.waitForInit()`. `AttachmentQueue.startSync()` also performs this verification. `waitForInit()` is only recommended if `startSync` is not called directly after creating the queue. diff --git a/Package.swift b/Package.swift index ed380de..d298fd4 100644 --- a/Package.swift +++ b/Package.swift @@ -52,7 +52,7 @@ if let corePath = localCoreExtension { let package = Package( name: packageName, platforms: [ - .iOS(.v13), + .iOS(.v15), .macOS(.v12), .watchOS(.v9) ], From 625ae1cd7332123d1b9df05816e815a7743648ef Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 19 Sep 2025 10:15:30 +0200 Subject: [PATCH 5/5] revert xcode version --- .github/workflows/build_and_test.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index f9753df..f13117d 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -13,8 +13,7 @@ jobs: - name: Set up XCode uses: maxim-lobanov/setup-xcode@v1 with: - # TODO: Update to latest-stable once GH installs iOS 26 simulators - xcode-version: "^16.4.0" + xcode-version: latest-stable - name: Build and Test run: | xcodebuild test -scheme PowerSync-Package -destination "platform=iOS Simulator,name=iPhone 16"