Skip to content

Commit 08fd253

Browse files
Attachments WaitForInit (#80)
* added attachment helpers changelog. Added waitForInit * add comment * set xcode version * update iOS target * revert xcode version
1 parent 11def98 commit 08fd253

File tree

4 files changed

+125
-2
lines changed

4 files changed

+125
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
* Update core extension to 0.4.6 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.6))
66
* Add `getCrudTransactions()`, returning an async sequence of transactions.
77
* Compatibility with Swift 6.2 and XCode 26.
8+
* Update minimum MacOS target to v12
9+
* Update minimum iOS target to v15
10+
* [Attachment Helpers] Added automatic verification or records' `local_uri` values on `AttachmentQueue` initialization.
11+
initialization can be awaited with `AttachmentQueue.waitForInit()`. `AttachmentQueue.startSync()` also performs this verification.
12+
`waitForInit()` is only recommended if `startSync` is not called directly after creating the queue.
813

914
## 1.5.1
1015

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ if let corePath = localCoreExtension {
5252
let package = Package(
5353
name: packageName,
5454
platforms: [
55-
.iOS(.v13),
56-
.macOS(.v10_15),
55+
.iOS(.v15),
56+
.macOS(.v12),
5757
.watchOS(.v9)
5858
],
5959
products: [

Sources/PowerSync/attachments/AttachmentQueue.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ public protocol AttachmentQueueProtocol: Sendable {
1010
var localStorage: any LocalStorageAdapter { get }
1111
var downloadAttachments: Bool { get }
1212

13+
/// Waits for automatically triggered initialization.
14+
/// This ensures all attachment records have been verified before use.
15+
/// The `startSync` method also performs this verification. This call is not
16+
/// needed if `startSync` is called after creating the Attachment Queue.
17+
func waitForInit() async throws
18+
1319
/// Starts the attachment sync process
1420
func startSync() async throws
1521

@@ -287,6 +293,9 @@ public actor AttachmentQueue: AttachmentQueueProtocol {
287293

288294
private let _getLocalUri: @Sendable (_ filename: String) async -> String
289295

296+
private let initializedSubject = PassthroughSubject<Result<Void, Error>, Never>()
297+
private var initializationResult: Result<Void, Error>?
298+
290299
/// Initializes the attachment queue
291300
/// - Parameters match the stored properties
292301
public init(
@@ -337,13 +346,44 @@ public actor AttachmentQueue: AttachmentQueueProtocol {
337346
syncThrottle: self.syncThrottleDuration
338347
)
339348

349+
// Storing a reference to this task is non-trivial since we capture
350+
// Self. Swift 6 Strict concurrency checking will complain about a nonisolated initializer
340351
Task {
341352
do {
342353
try await attachmentsService.withContext { context in
343354
try await self.verifyAttachments(context: context)
344355
}
356+
await self.setInitializedResult(.success(()))
345357
} catch {
346358
self.logger.error("Error verifying attachments: \(error.localizedDescription)", tag: logTag)
359+
await self.setInitializedResult(.failure(error))
360+
}
361+
}
362+
}
363+
364+
/// Actor isolated method to set the initialization result
365+
private func setInitializedResult(_ result: Result<Void, Error>) {
366+
initializationResult = result
367+
initializedSubject.send(result)
368+
}
369+
370+
public func waitForInit() async throws {
371+
if let isInitialized = initializationResult {
372+
switch isInitialized {
373+
case .success:
374+
return
375+
case let .failure(error):
376+
throw error
377+
}
378+
}
379+
380+
// Wait for the result asynchronously
381+
for try await result in initializedSubject.values {
382+
switch result {
383+
case .success:
384+
return
385+
case let .failure(error):
386+
throw error
347387
}
348388
}
349389
}

Tests/PowerSyncTests/AttachmentTests.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,84 @@ final class AttachmentTests: XCTestCase {
173173
try await queue.clearQueue()
174174
try await queue.close()
175175
}
176+
177+
func testAttachmentInitVerification() async throws {
178+
actor MockRemoteStorage: RemoteStorageAdapter {
179+
func uploadFile(
180+
fileData _: Data,
181+
attachment _: Attachment
182+
) async throws {}
183+
184+
func downloadFile(attachment _: Attachment) async throws -> Data {
185+
return Data([1, 2, 3])
186+
}
187+
188+
func deleteFile(attachment _: Attachment) async throws {}
189+
}
190+
191+
// Create an attachments record which has an invalid local_uri
192+
let attachmentsDirectory = getAttachmentDirectory()
193+
194+
try FileManager.default.createDirectory(
195+
at: URL(fileURLWithPath: attachmentsDirectory),
196+
withIntermediateDirectories: true,
197+
attributes: nil
198+
)
199+
200+
let filename = "test.jpeg"
201+
202+
try Data("1".utf8).write(
203+
to: URL(fileURLWithPath: attachmentsDirectory).appendingPathComponent(filename)
204+
)
205+
try await database.execute(
206+
sql: """
207+
INSERT OR REPLACE INTO
208+
attachments (id, timestamp, filename, local_uri, media_type, size, state, has_synced, meta_data)
209+
VALUES
210+
(uuid(), ?, ?, ?, ?, ?, ?, ?, ?)
211+
""",
212+
parameters: [
213+
Date().ISO8601Format(),
214+
filename,
215+
// This is a broken local_uri
216+
URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("not_attachments/test.jpeg").path,
217+
"application/jpeg",
218+
1,
219+
AttachmentState.synced.rawValue,
220+
1,
221+
""
222+
]
223+
)
224+
225+
let mockedRemote = MockRemoteStorage()
226+
227+
let queue = AttachmentQueue(
228+
db: database,
229+
remoteStorage: mockedRemote,
230+
attachmentsDirectory: attachmentsDirectory,
231+
watchAttachments: { [database = database!] in try database.watch(options: WatchOptions(
232+
sql: "SELECT photo_id FROM users WHERE photo_id IS NOT NULL",
233+
mapper: { cursor in try WatchedAttachmentItem(
234+
id: cursor.getString(name: "photo_id"),
235+
fileExtension: "jpg"
236+
) }
237+
)) }
238+
)
239+
240+
try await queue.waitForInit()
241+
242+
// the attachment should have been corrected in the init
243+
let attachments = try await queue.attachmentsService.withContext { context in
244+
try await context.getAttachments()
245+
}
246+
247+
guard let firstAttachment = attachments.first else {
248+
XCTFail("Could not find the attachment record")
249+
return
250+
}
251+
252+
XCTAssert(firstAttachment.localUri == URL(fileURLWithPath: attachmentsDirectory).appendingPathComponent(filename).path)
253+
}
176254
}
177255

178256
public enum WaitForMatchError: Error {

0 commit comments

Comments
 (0)