Skip to content

Conversation

@joshua-journey-apps
Copy link

@joshua-journey-apps joshua-journey-apps commented Oct 7, 2025

This a port of the Swift, Dart and Kotlin attachment's API to the JS SDK.

This addresses the following issues #715 and #714 by adding the meta_data column to the table and persisting it in the attachments table.

It is currently still a work-in-progress so there are a couple things that still needs to be done:

  • Move attachment package to common
    • Move Node adapter under Node package
    • Move indexDB adapter under Web package
    • Move Expo adapter under RN package
  • Update demos
  • Improve testing
  • Update the README
  • Implement sync throttling
  • Complete clearing archived attachments
  • Removing some holdovers from the previous implementation
    • Supported file data format

@changeset-bot
Copy link

changeset-bot bot commented Oct 7, 2025

🦋 Changeset detected

Latest commit: bd6a5b9

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

This PR includes changesets to release 9 packages
Name Type
@powersync/react-native Minor
@powersync/common Minor
@powersync/node Minor
@powersync/web Minor
@powersync/attachments Patch
@powersync/adapter-sql-js Patch
@powersync/op-sqlite Patch
@powersync/tanstack-react-query 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

Copy link
Collaborator

@stevensJourney stevensJourney left a comment

Choose a reason for hiding this comment

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

This looks like a good improvement so far. I've added some comments for some items I spotted.

joshuabrink and others added 17 commits October 27, 2025 15:49
- Added `getAttachment` method to retrieve an attachment by ID in AttachmentContext.
- Updated `upsertAttachment` to handle null values for optional fields.
- Introduced `generateAttachmentId` method in AttachmentQueue for generating unique IDs.
- Modified `watchActiveAttachments` to accept a throttle parameter.
- Added `deleteFile` method to have attachment deletion.
- Updated tests to cover new functionality and ensure reliability.
…ttachment sync operations

- Added AttachmentErrorHandler interface to manage download, upload, and delete errors for attachments.
- Updated AttachmentQueue and SyncingService to utilize the new error handler.
- Enhanced tests to verify error handling behavior during attachment sync processes.
@joshuabrink joshuabrink force-pushed the attachment-package-refactor branch from 671e23a to bea28d9 Compare November 12, 2025 09:15
@joshuabrink joshuabrink force-pushed the attachment-package-refactor branch from bea28d9 to e42ae45 Compare November 12, 2025 09:15
@joshuabrink joshuabrink marked this pull request as ready for review November 28, 2025 13:10
Copy link
Collaborator

@stevensJourney stevensJourney left a comment

Choose a reason for hiding this comment

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

Left some comments. Overall the APIs and code looks good so far.

Copy link
Collaborator

@stevensJourney stevensJourney left a comment

Choose a reason for hiding this comment

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

I noticed a few items in this review, but I do think this is looking good.

"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-terser": "^0.4.4",
"bson": "^6.10.4",
"expo-file-system": "^15.2.2",
Copy link
Collaborator

Choose a reason for hiding this comment

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

While reviewing #803, I noticed that the latest version of expo-file-system is 19.0.21.

The current latest version uses a * peerDependency constraint for expo, which should theoretically be compatible with any version of Expo.

However, we're already 4 major versions behind expo-file-system, which raises an important question: what happens when the next major version (with breaking changes) is released? Bumping our peerDependency range for expo-file-system by a major version could be considered a breaking change for our users, requiring a major version bump of @powersync/react-native. I'd prefer to avoid making a major version bump solely for a filesystem dependency upgrade that only affects a small portion of this package's functionality (the attachment helpers).

We don't really have this issue for other JS platforms (or other SDKs) since the storage APIs (and general dependencies) we're using are quite standard.

Moving the FileSystem adapter out of this package is also not ideal, but does seem like a potentially more manageable alternative.

Choose a reason for hiding this comment

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

Good call, while not ideal, I'm not sure if there is a cleaner way to workaround the Expo dependency churn.

The lowest friction route I could think of, was to just create a straightforward@powersync/attachments-react-native package that handles both Expo and React Native file system adapters.

It's still a work-in-progress (docs and demos need updating) but the idea is there.


### Features

- Initial release
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's generally safer to add a minor changeset entry for this new package which contains this content.

While it's not the case here (since we have a changeset for the PowerSync SDKs). Merging this PR to main without a changeset, for a new package, could result in that package getting published immediately - while having a changeset would create a release PR where one can smoke check versions before release.

},
"peerDependencies": {
"@powersync/common": "workspace:^1.43.1",
"expo-file-system": ">=15.0.0 <20.0.0",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this version constraint correct? My understanding is that version 19 included breaking changes (compared to previous versions). How does our implementation here handle both APIs?

import { decode as decodeBase64, encode as encodeBase64 } from 'base64-arraybuffer';
import { AttachmentData, EncodingType, LocalStorageAdapter } from '@powersync/common';

type ExpoFileSystemModule = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can't we import the type only from the package?

import type ExpoFS from 'expo-file-system'

// ...

export class ExpoFileSystemStorageAdapter implements LocalStorageAdapter {
  private fs: typeof ExpoFS;

One I made that change, and was using expo-file-system-19 I quickly ran into other issues due to the different API.

I'd suggest having this lib only support version 19.

import { decode as decodeBase64, encode as encodeBase64 } from 'base64-arraybuffer';
import { AttachmentData, EncodingType, LocalStorageAdapter } from '@powersync/common';

type ReactNativeFsModule = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd also recommend just doing type import here.

/** Service for managing attachment-related database operations */
readonly attachmentService: AttachmentService;

watchActiveAttachments: DifferentialWatchedQuery<AttachmentRecord>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should these, and watchAttachments not be private/protected?

) => void;

/** Name of the database table storing attachment records */
readonly tableName?: string;
Copy link
Collaborator

Choose a reason for hiding this comment

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

should this be optional/nullable?

* @returns Promise resolving to the new attachment ID
*/
async generateAttachmentId(): Promise<string> {
return this.attachmentService.withContext(async (ctx) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: this doesn't need an attachment context (since it can't possibly interfere with attachment sync). this can just use the DB.


// Sync storage periodically
this.#periodicSyncTimer = setInterval(async () => {
await this.syncStorage();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also call this if the sync status changed to connected: true this allows for attachments to be processed as soon as the device becomes online (after being offline)

readonly downloadAttachments: boolean = true;

/** Maximum number of archived attachments to keep before cleanup. Default: 100 */
readonly archivedCacheLimit: number;
Copy link
Collaborator

Choose a reason for hiding this comment

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

For consistency with other SDKs, I think a lot of these members can be protected - or, I don't think they need to be public.

localStorage: LocalStorageAdapter;
remoteStorage: RemoteStorageAdapter;
logger: ILogger;
errorHandler?: AttachmentErrorHandler;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, a lot of these members don't need to be public. I don't imagine there's much of a use case for code of the form syncingService.localStorage. ... etc.

}

// Default base64 encoding
const base64String = req.result.replace(/^data:\w+;base64,/, '');
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like the saveFile method stores the data as an ArrayBuffer, where the req.result should be an ArrayBuffer. When reading a file, the replace method is not found on an ArrayBuffer.
image

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.

5 participants