Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-packages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ jobs:

- name: Build typescript
run: pnpm exec tsc -b

- name: Verify @powersync/common API
working-directory: packages/common
run: pnpm exec api-extractor run --verbose
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ Cargo.lock
target/
# Large files generated by Tauri
**/gen/schemas/

/packages/common/temp
454 changes: 454 additions & 0 deletions packages/common/api-extractor.json

Large diffs are not rendered by default.

2,442 changes: 2,442 additions & 0 deletions packages/common/etc/common.api.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"rollup": "catalog:",
"rollup-plugin-dts": "catalog:",
"rsocket-core": "1.0.0-alpha.3",
"rsocket-websocket-client": "1.0.0-alpha.3"
"rsocket-websocket-client": "1.0.0-alpha.3",
"@microsoft/api-extractor": "catalog:"
}
}
12 changes: 6 additions & 6 deletions packages/common/src/attachments/AttachmentErrorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ import { AttachmentRecord } from './Schema.js';
export interface AttachmentErrorHandler {
/**
* Handles a download error for a specific attachment.
* @param attachment The attachment that failed to download
* @param error The error encountered during the download
* @param attachment - The attachment that failed to download
* @param error - The error encountered during the download
* @returns `true` to retry the operation, `false` to archive the attachment
*/
onDownloadError(attachment: AttachmentRecord, error: unknown): Promise<boolean>;

/**
* Handles an upload error for a specific attachment.
* @param attachment The attachment that failed to upload
* @param error The error encountered during the upload
* @param attachment - The attachment that failed to upload
* @param error - The error encountered during the upload
* @returns `true` to retry the operation, `false` to archive the attachment
*/
onUploadError(attachment: AttachmentRecord, error: unknown): Promise<boolean>;

/**
* Handles a delete error for a specific attachment.
* @param attachment The attachment that failed to delete
* @param error The error encountered during the delete
* @param attachment - The attachment that failed to delete
* @param error - The error encountered during the delete
* @returns `true` to retry the operation, `false` to archive the attachment
*/
onDeleteError(attachment: AttachmentRecord, error: unknown): Promise<boolean>;
Expand Down
68 changes: 50 additions & 18 deletions packages/common/src/attachments/AttachmentQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { AttachmentErrorHandler } from './AttachmentErrorHandler.js';
* @experimental
* @alpha This is currently experimental and may change without a major version bump.
*/
export class AttachmentQueue {
export class AttachmentQueue implements AttachmentQueue {
/** Timer for periodic synchronization operations */
private periodicSyncTimer?: ReturnType<typeof setInterval>;

Expand Down Expand Up @@ -86,16 +86,6 @@ export class AttachmentQueue {
* Creates a new AttachmentQueue instance.
*
* @param options - Configuration options
* @param options.db - PowerSync database instance
* @param options.remoteStorage - Remote storage adapter for upload/download operations
* @param options.localStorage - Local storage adapter for file persistence
* @param options.watchAttachments - Callback for monitoring attachment changes in your data model
* @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
* @param options.logger - Logger instance. Defaults to db.logger
* @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
* @param options.syncThrottleDuration - Throttle duration in milliseconds for the reactive watch query that detects attachment changes. Prevents rapid-fire syncs during bulk changes. Default: 30
* @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
* @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
*/
constructor({
db,
Expand All @@ -110,15 +100,45 @@ export class AttachmentQueue {
archivedCacheLimit = 100,
errorHandler
}: {
/**
* PowerSync database instance
*/
db: AbstractPowerSyncDatabase;
/**
* Remote storage adapter for upload/download operations
*/
remoteStorage: RemoteStorageAdapter;
/**
* Local storage adapter for file persistence
*/
localStorage: LocalStorageAdapter;
/**
* Callback for monitoring attachment changes in your data model
*/
watchAttachments: (onUpdate: (attachment: WatchedAttachmentItem[]) => Promise<void>, signal: AbortSignal) => void;
/**
* Name of the table to store attachment records. Default: 'ps_attachment_queue'
*/
tableName?: string;
/**
* Logger instance. Defaults to db.logger
*/
logger?: ILogger;
/**
* Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
*/
syncIntervalMs?: number;
/**
* Throttle duration in milliseconds for the reactive watch query that detects attachment changes. Prevents rapid-fire syncs during bulk changes. Default: 30
*/
syncThrottleDuration?: number;
/**
* Whether to automatically download remote attachments. Default: true
*/
downloadAttachments?: boolean;
/**
* Maximum archived attachments before cleanup. Default: 100
*/
archivedCacheLimit?: number;
errorHandler?: AttachmentErrorHandler;
}) {
Expand Down Expand Up @@ -326,13 +346,6 @@ export class AttachmentQueue {
* Saves a file to local storage and queues it for upload to remote storage.
*
* @param options - File save options
* @param options.data - The file data as ArrayBuffer, Blob, or base64 string
* @param options.fileExtension - File extension (e.g., 'jpg', 'pdf')
* @param options.mediaType - MIME type of the file (e.g., 'image/jpeg')
* @param options.metaData - Optional metadata to associate with the attachment
* @param options.id - Optional custom ID. If not provided, a UUID will be generated
* @param options.updateHook - Optional callback to execute additional database operations
* within the same transaction as the attachment creation
* @returns Promise resolving to the created attachment record
*/
async saveFile({
Expand All @@ -343,11 +356,30 @@ export class AttachmentQueue {
id,
updateHook
}: {
/**
* The file data as ArrayBuffer, Blob, or base64 string
*/
data: AttachmentData;
/**
* File extension (e.g., 'jpg', 'pdf')
*/
fileExtension: string;
/**
* MIME type of the file (e.g., 'image/jpeg')
*/
mediaType?: string;
/**
* Optional metadata to associate with the attachment
*/
metaData?: string;
/**
* Optional custom ID. If not provided, a UUID will be generated
*/
id?: string;
/**
* Optional callback to execute additional database operations within the same transaction as the attachment
* creation.
*/
updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => Promise<void>;
}): Promise<AttachmentRecord> {
const resolvedId = id ?? (await this.generateAttachmentId());
Expand Down
22 changes: 14 additions & 8 deletions packages/common/src/attachments/LocalStorageAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/**
* @alpha
*/
export type AttachmentData = ArrayBuffer | string;

/**
* @alpha
*/
export enum EncodingType {
UTF8 = 'utf8',
Base64 = 'base64'
Expand All @@ -15,41 +21,41 @@ export enum EncodingType {
export interface LocalStorageAdapter {
/**
* Saves data to a local file.
* @param filePath Path where the file will be stored
* @param data Data to store (ArrayBuffer, Blob, or string)
* @param filePath - Path where the file will be stored
* @param data - Data to store (ArrayBuffer, Blob, or string)
* @returns Number of bytes written
*/
saveFile(filePath: string, data: AttachmentData): Promise<number>;

/**
* Retrieves file data as an ArrayBuffer.
* @param filePath Path where the file is stored
* @param filePath - Path where the file is stored
* @returns ArrayBuffer containing the file data
*/
readFile(filePath: string): Promise<ArrayBuffer>;

/**
* Deletes the file at the given path.
* @param filePath Path where the file is stored
* @param filePath - Path where the file is stored
*/
deleteFile(filePath: string): Promise<void>;

/**
* Checks if a file exists at the given path.
* @param filePath Path where the file is stored
* @param filePath - Path where the file is stored
* @returns True if the file exists, false otherwise
*/
fileExists(filePath: string): Promise<boolean>;

/**
* Creates a directory at the specified path.
* @param path The full path to the directory
* @param path - The full path to the directory
*/
makeDir(path: string): Promise<void>;

/**
* Removes a directory at the specified path.
* @param path The full path to the directory
* @param path - The full path to the directory
*/
rmDir(path: string): Promise<void>;

Expand All @@ -65,7 +71,7 @@ export interface LocalStorageAdapter {

/**
* Returns the file path for the provided filename in the storage directory.
* @param filename The filename to get the path for
* @param filename - The filename to get the path for
* @returns The full file path
*/
getLocalUri(filename: string): string;
Expand Down
8 changes: 4 additions & 4 deletions packages/common/src/attachments/RemoteStorageAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import { AttachmentRecord } from './Schema.js';
export interface RemoteStorageAdapter {
/**
* Uploads a file to remote storage.
* @param fileData The binary content of the file to upload
* @param attachment The associated attachment metadata
* @param fileData - The binary content of the file to upload
* @param attachment - The associated attachment metadata
*/
uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void>;

/**
* Downloads a file from remote storage.
* @param attachment The attachment describing the file to download
* @param attachment - The attachment describing the file to download
* @returns The binary data of the downloaded file
*/
downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer>;

/**
* Deletes a file from remote storage.
* @param attachment The attachment describing the file to delete
* @param attachment - The attachment describing the file to delete
*/
deleteFile(attachment: AttachmentRecord): Promise<void>;
}
16 changes: 12 additions & 4 deletions packages/common/src/attachments/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { column } from '../db/schema/Column.js';
import { Table } from '../db/schema/Table.js';
import { TableV2Options } from '../db/schema/Table.js';

/**
* The default name of the local table storing attachment data.
*
* @alpha
*/
export const ATTACHMENT_TABLE = 'attachments';

/**
* AttachmentRecord represents an attachment in the local database.
*
* @experimental
* @alpha
*/
export interface AttachmentRecord {
id: string;
Expand All @@ -27,7 +32,7 @@ export interface AttachmentRecord {
* @param row - The database row object
* @returns The corresponding AttachmentRecord
*
* @experimental
* @alpha
*/
export function attachmentFromSql(row: any): AttachmentRecord {
return {
Expand All @@ -46,7 +51,7 @@ export function attachmentFromSql(row: any): AttachmentRecord {
/**
* AttachmentState represents the current synchronization state of an attachment.
*
* @experimental
* @alpha
*/
export enum AttachmentState {
QUEUED_UPLOAD = 0, // Attachment to be uploaded
Expand All @@ -56,12 +61,15 @@ export enum AttachmentState {
ARCHIVED = 4 // Attachment has been orphaned, i.e. the associated record has been deleted
}

/**
* @alpha
*/
export interface AttachmentTableOptions extends Omit<TableV2Options, 'name' | 'columns'> {}

/**
* AttachmentTable defines the schema for the attachment queue table.
*
* @internal
* @alpha
*/
export class AttachmentTable extends Table {
constructor(options?: AttachmentTableOptions) {
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/attachments/WatchedAttachmentItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WatchedAttachmentItem represents an attachment reference in your application's data model.
* Use either filename OR fileExtension (not both).
*
* @experimental
* @alpha
*/
export type WatchedAttachmentItem =
| {
Expand Down
Loading
Loading