Skip to content

feat(attachments): streaming attachment transport + saveFileFromUri - #1039

Open
khawarizmus wants to merge 14 commits into
mainfrom
attachment-transport
Open

feat(attachments): streaming attachment transport + saveFileFromUri#1039
khawarizmus wants to merge 14 commits into
mainfrom
attachment-transport

Conversation

@khawarizmus

@khawarizmus khawarizmus commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses the streaming request in discussion #968. The AttachmentQueue currently round-trips every file body through an ArrayBuffer in JS memory (localStorage.readFileremoteStorage.uploadFile, and the reverse on download), which causes memory pressure / OOM for large files on lower-end devices.

This models the remote side (transfer and delete) as a single adapter so implementations can stream file-URI to the network natively, never materializing the file in the JS heap.

What's included

  • AttachmentTransportAdapter (common) — owns all remote operations: upload/download/delete. Supply this to fully replace remoteStorage.
  • AttachmentQueue options are now a discriminated union — provide exactly one of remoteStorage or transportAdapter. Supplying both, or neither, is a compile-time type error (a runtime guard also covers plain-JS callers). When only remoteStorage is given, an internal buffered transport is composed from it automatically, so existing setups are unchanged.
  • saveFileFromUri — registers a file already on disk and queues it for upload without reading it into memory. Only available when the queue is configured with a StreamingLocalStorageAdapter (enforced in the type system).
  • StreamingLocalStorageAdapter — a subinterface of LocalStorageAdapter adding buffer-free moveFile; implemented by the Node, Expo, and RN-FS adapters.
  • ExpoFileSystemTransportAdapter (attachments-storage-react-native) — native transport using Expo's uploadAsync/downloadAsync (+ a deleteFile callback) for buffer-free transfer.
  • ReactNativeFSTransportAdapter (attachments-storage-react-native) — native transport using @dr.pogodin/react-native-fs's uploadFiles/downloadFile (+ a deleteFile callback).

Backward compatibility

Default behavior is unchanged. When no transportAdapter is supplied, a buffered transport is composed internally from localStorage/remoteStorage, and RemoteStorageAdapter is unchanged. Existing { localStorage, remoteStorage } setups still compile and behave identically.

One intended behavior change for the RemoteStorageAdapter: downloadFile now receives the record with its destination localUri populated (previously null for fresh downloads).

Generated with Claude, tested and reviewed manually.

@changeset-bot

changeset-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d5ae825

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@powersync/common Minor
@powersync/attachments-storage-react-native Minor
@powersync/node Minor
@powersync/react-native Patch
@powersync/tanstack-react-query Patch
@powersync/web Patch
@powersync/diagnostics-app Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@khawarizmus
khawarizmus marked this pull request as draft July 14, 2026 15:54
@khawarizmus
khawarizmus force-pushed the attachment-transport branch from 509d298 to bf2d8cc Compare July 14, 2026 23:21
@khawarizmus
khawarizmus marked this pull request as ready for review July 16, 2026 06:20

@simolus3 simolus3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the changes and the BufferedAttachmentTransport fallback are clean, but we can probably restructure the public interface a bit to get rid of runtime errors.

* Describes the HTTP request used to upload a file's bytes to remote storage.
* Typically points at a presigned URL.
*/
export interface ExpoUploadRequest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add an optional dependency on expo and then use expo types here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not import the types from Expo. The dependency is already present.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What errors did you get? We might need to tweak some typescript configs but copying the types doesn't sound right.

Comment thread packages/common/src/attachments/BufferedAttachmentTransport.ts
Comment thread packages/common/src/attachments/AttachmentQueue.ts Outdated
Comment thread packages/common/src/attachments/AttachmentQueue.ts Outdated
this.logger = logger ?? db.logger;
this.attachmentService = new AttachmentService(db, this.logger, tableName, archivedCacheLimit);

if (!transportAdapter && !remoteStorage) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also throw when both are set (or better yet make it a type error), because the remote implementation wouldn't be used in that case which sounds unintentional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encoded this in the type now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also remove this check then? I don't really see the point in runtime checks for users who get the types wrong.

Comment thread packages/common/src/attachments/AttachmentQueue.ts Outdated
Comment thread packages/common/src/attachments/AttachmentQueue.ts Outdated
khawarizmus and others added 8 commits July 22, 2026 21:03
Model attachment byte-transfer as a single operation via AttachmentTransportAdapter,
with a default BufferedAttachmentTransport that composes the existing local/remote
adapters. Add AttachmentQueue.saveFileFromUri and an optional
LocalStorageAdapter.moveFile for buffer-free registration of files already on disk,
a native Expo ExpoFileSystemTransportAdapter, and moveFile in the Node and React
Native local adapters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tag LocatedAttachmentRecord as @Alpha, use a qualified @link to
AttachmentQueue.saveFile, and update etc/common.api.md for the new
attachment transport API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pter

Native streaming transport for @dr.pogodin/react-native-fs (raw binary PUT via
uploadFiles + downloadFile), mirroring ExpoFileSystemTransportAdapter, so non-Expo
apps can transfer large attachments without buffering them in JS memory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pter

Enhance AttachmentTransportAdapter to support file deletion operations.
This includes updates to ExpoFileSystemTransportAdapter and ReactNativeFSTransportAdapter
to implement the new deleteFile method. Additionally, adjustments were made to
AttachmentQueue and BufferedAttachmentTransport to integrate the delete functionality
into the attachment synchronization process.
…vulnerability audit

This commit updates the websocket-driver dependency to version 0.7.5 in both pnpm-lock.yaml and pnpm-workspace.yaml to address security advisories. Additionally, the README.md for attachments-storage-react-native has been enhanced to clarify the functionality of local storage and streaming transport adapters, including usage examples and API details.

# Conflicts:
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
…er interfaces

This commit refines the AttachmentQueue and transport adapter interfaces by consolidating configuration options and enhancing type safety. The `AttachmentQueueOptions` now requires either a `remoteStorage` or a `transportAdapter`, ensuring clarity in usage. Additionally, the `saveFile` and `saveFileFromUri` methods have been updated to utilize a unified `SaveAttachmentOptions` type, improving consistency across attachment operations. Documentation in the README.md has also been updated to reflect these changes.
…port streaming

This commit refines the local storage adapter interfaces by replacing the `LocalStorageAdapter` with `StreamingLocalStorageAdapter` in multiple components, including `ExpoFileSystemStorageAdapter`, `ReactNativeFileSystemStorageAdapter`, and `NodeFileSystemAdapter`. The `AttachmentQueue` and related types have been updated to leverage the new streaming capabilities, ensuring that the `saveFileFromUri` method is only available when a streaming-capable adapter is used. Documentation has been adjusted to reflect these changes.
This commit updates the type definition for the `queue` variable in the attachments test file to explicitly use `AttachmentQueue<NodeFileSystemAdapter>`. This change enhances type safety and clarity in the test setup, aligning with recent refinements in the local storage adapter interfaces.
@khawarizmus
khawarizmus force-pushed the attachment-transport branch from a56ffa6 to 037d1fc Compare July 23, 2026 00:47
@khawarizmus
khawarizmus requested a review from simolus3 July 23, 2026 00:51
@khawarizmus

Copy link
Copy Markdown
Contributor Author

@simolus3 Tests that are outside the scope of the PR are failing. But the PR is ready.

@khawarizmus

Copy link
Copy Markdown
Contributor Author

@simolus3 CI is passing now.

* Describes the HTTP request used to upload a file's bytes to remote storage.
* Typically points at a presigned URL.
*/
export interface ExpoUploadRequest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What errors did you get? We might need to tweak some typescript configs but copying the types doesn't sound right.

Comment thread packages/attachments-storage-react-native/src/ExpoFileSystemTransportAdapter.ts Outdated
Comment thread packages/attachments-storage-react-native/src/ExpoFileSystemTransportAdapter.ts Outdated
Comment thread packages/attachments-storage-react-native/src/ReactNativeFSTransportAdapter.ts Outdated
Comment thread packages/attachments-storage-react-native/src/ReactNativeFSTransportAdapter.ts Outdated
this.logger = logger ?? db.logger;
this.attachmentService = new AttachmentService(db, this.logger, tableName, archivedCacheLimit);

if (!transportAdapter && !remoteStorage) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also remove this check then? I don't really see the point in runtime checks for users who get the types wrong.

Comment thread packages/common/src/attachments/AttachmentQueue.ts
Comment thread packages/common/src/attachments/AttachmentTransportAdapter.ts
Comment thread packages/node/src/attachments/NodeFileSystemAdapter.ts
khawarizmus and others added 5 commits July 30, 2026 11:01
…ntegration

This commit refines the integration of the `ReactNativeFileSystemTransportAdapter` within the `ReactNativeFileSystemStorageAdapter`. The transport adapter is now created through a method in the storage adapter, enhancing encapsulation. Additionally, type exports for upload and download requests have been added to improve type safety and clarity in usage. Documentation in the README.md has been updated to reflect these changes.
This commit updates the `expo-file-system` dependency from version 19.0.21 to 56.0.8 in the `package.json` and `pnpm-lock.yaml` files. The README.md has been revised to reflect the new API usage for file uploads and downloads, emphasizing the requirement for Expo SDK 56+. Additionally, the `ExpoFileSystemTransportAdapter` has been modified to utilize the modern upload API, enhancing performance and memory management during file transfers.
This commit introduces a new streaming attachment transport (`AttachmentTransportAdapter`) and the `AttachmentQueue.saveFileFromUri` method, allowing large files to be transferred without being buffered in JS memory. It includes native implementations for Node, Expo (SDK 56+), and React Native. Additionally, the `NodeFileSystemAdapter` is updated to create a streaming transport adapter, and a new `NodeFileSystemTransportAdapter` class is added to handle file uploads and downloads directly between local files and remote storage. Tests for the streaming transport functionality have also been added.
@khawarizmus
khawarizmus requested a review from simolus3 July 30, 2026 21:10

@simolus3 simolus3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 👍

headers,
body: Readable.toWeb(createReadStream(attachment.localUri)) as ReadableStream,
duplex: 'half'
} as RequestInit & { duplex: 'half' });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment pointing to node-fetch/node-fetch#1769 as an explanation.


### With Expo File System (`File.upload` / `File.downloadFileAsync`)

> The Expo streaming transport requires **Expo SDK 56+** (`expo-file-system >=56`). The `ExpoFileSystemStorageAdapter` itself still works on Expo SDK 54+.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's odd to have this as a quote. If you want an alert, add a > [!NOTE] line above. Otherwise, just add it without the quote.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants