Summary
In @powersync/common, AttachmentService exposes a single shared Mutex (via withContext) that is acquired by both the foreground AttachmentQueue.saveFile path and the background SyncingService.processAttachments loop. processAttachments keeps that mutex held across the per-attachment remoteStorage.uploadFile / downloadFile calls. As a result, a slow or stalled network request from the background sync loop blocks any foreground saveFile for the full duration of the upload.
Where
packages/common/src/client/attachments/AttachmentService.ts — mutex = new Mutex(), withContext() wraps the entire callback in runExclusive.
packages/common/src/client/attachments/SyncingService.ts — processAttachments() calls service.withContext(async (context) => { for (...) await this.uploadAttachment(att, context); ... }). The uploadAttachment body awaits this.remoteStorage.uploadFile(blob, attachment), which is user-provided fetch-based I/O.
packages/common/src/client/attachments/AttachmentQueue.ts — saveFile() ultimately also goes through service.withContext(...) to insert the new attachment row, so it queues behind the in-flight upload.
Observed behaviour
A reproducible hang in a Tauri 2 / WebKitGTK 2.52 desktop app on Linux:
- User registers a new person with a photo. UI calls
queue.saveFile(...).
- At the same time,
processAttachments is mid-iteration on a previously queued attachment whose upload has stalled (flaky network, sleeping mobile NAT, etc.).
- WebKitGTK's
fetch has an implicit ~96 s timeout (Linux TCP keepalive defaults are 2 h, far longer); the stalled upload sits inside the mutex until WebKit finally aborts it.
- During that window the foreground
saveFile is pending — the user sees a silent infinite spinner; no error is thrown.
- Once the upload eventually errors out and the mutex releases, the foreground
saveFile proceeds and the file lands on disk normally.
Reproduction
On Linux, applying degraded network with tc netem:
tc qdisc add dev <iface> root netem loss random 30% 25% \
delay 800ms 400ms distribution normal reorder 25% 50% rate 100kbit
then triggering a foreground saveFile while a previous attachment is mid-upload reliably produces ~96 s delays between [attachments] arrayBuffer ready and [attachments] Photo saved. The delay matches the WebKit implicit fetch timeout almost exactly. Removing the netem rule (or running on a stable network) makes the symptom disappear.
Hypothesis
The mutex correctly protects the in-memory AttachmentContext cache and the atomic state transitions in the attachments table, but its scope in processAttachments is wider than it needs to be — the per-attachment network call has no shared state to protect. Holding the lock across the network I/O means any foreground writer is gated on the slowest in-flight upload.
Environment
@powersync/common 1.51.0 (also observed on 1.53.1)
@powersync/tauri-plugin 0.0.4
- Tauri 2.x, WebKitGTK 2.52 on Linux
- Supabase Storage as
RemoteStorageAdapter
Happy to provide additional logs / a minimal repro if useful.
Summary
In
@powersync/common,AttachmentServiceexposes a single sharedMutex(viawithContext) that is acquired by both the foregroundAttachmentQueue.saveFilepath and the backgroundSyncingService.processAttachmentsloop.processAttachmentskeeps that mutex held across the per-attachmentremoteStorage.uploadFile/downloadFilecalls. As a result, a slow or stalled network request from the background sync loop blocks any foregroundsaveFilefor the full duration of the upload.Where
packages/common/src/client/attachments/AttachmentService.ts—mutex = new Mutex(),withContext()wraps the entire callback inrunExclusive.packages/common/src/client/attachments/SyncingService.ts—processAttachments()callsservice.withContext(async (context) => { for (...) await this.uploadAttachment(att, context); ... }). TheuploadAttachmentbody awaitsthis.remoteStorage.uploadFile(blob, attachment), which is user-providedfetch-based I/O.packages/common/src/client/attachments/AttachmentQueue.ts—saveFile()ultimately also goes throughservice.withContext(...)to insert the new attachment row, so it queues behind the in-flight upload.Observed behaviour
A reproducible hang in a Tauri 2 / WebKitGTK 2.52 desktop app on Linux:
queue.saveFile(...).processAttachmentsis mid-iteration on a previously queued attachment whose upload has stalled (flaky network, sleeping mobile NAT, etc.).fetchhas an implicit ~96 s timeout (Linux TCP keepalive defaults are 2 h, far longer); the stalled upload sits inside the mutex until WebKit finally aborts it.saveFileis pending — the user sees a silent infinite spinner; no error is thrown.saveFileproceeds and the file lands on disk normally.Reproduction
On Linux, applying degraded network with
tc netem:then triggering a foreground
saveFilewhile a previous attachment is mid-upload reliably produces ~96 s delays between[attachments] arrayBuffer readyand[attachments] Photo saved. The delay matches the WebKit implicit fetch timeout almost exactly. Removing the netem rule (or running on a stable network) makes the symptom disappear.Hypothesis
The mutex correctly protects the in-memory
AttachmentContextcache and the atomic state transitions in theattachmentstable, but its scope inprocessAttachmentsis wider than it needs to be — the per-attachment network call has no shared state to protect. Holding the lock across the network I/O means any foreground writer is gated on the slowest in-flight upload.Environment
@powersync/common1.51.0 (also observed on 1.53.1)@powersync/tauri-plugin0.0.4RemoteStorageAdapterHappy to provide additional logs / a minimal repro if useful.