From 77d7b5408dec555c858c7cff5f7b3e19d2aed870 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 12:14:04 +0200 Subject: [PATCH 01/12] Move implementation details into separate package --- packages/adapter-sql-js/package.json | 3 +- packages/adapter-sql-js/src/SQLJSAdapter.ts | 3 +- packages/adapter-sql-js/tsconfig.json | 3 + packages/capacitor/package.json | 3 + .../src/adapter/CapacitorSQLiteAdapter.ts | 5 +- .../src/sync/CapacitorSyncImplementation.ts | 3 +- packages/capacitor/tsconfig.json | 2 +- packages/common/package.json | 44 +- .../src/attachments/AttachmentContext.ts | 6 +- .../common/src/attachments/AttachmentQueue.ts | 11 +- .../src/attachments/AttachmentService.ts | 7 +- .../src/client/CommonPowerSyncDatabase.ts | 606 ++++++++++++++++++ .../common/src/client/compilableQueryWatch.ts | 4 +- .../connection/PowerSyncBackendConnector.ts | 4 +- packages/common/src/client/constants.ts | 4 - .../common/src/client/runOnSchemaChange.ts | 4 +- .../src/client/sync/bucket/CrudEntry.ts | 108 +--- .../src/client/triggers/TriggerManager.ts | 35 - .../common/src/client/watched/GetAllQuery.ts | 6 +- .../common/src/client/watched/WatchedQuery.ts | 17 +- .../processors/DifferentialQueryProcessor.ts | 222 ------- .../processors/OnChangeQueryProcessor.ts | 111 ---- packages/common/src/db/crud/SyncProgress.ts | 44 +- packages/common/src/db/crud/SyncStatus.ts | 210 +----- packages/common/src/index.ts | 17 +- packages/common/src/utils/BaseObserver.ts | 35 - packages/common/src/utils/MetaBaseObserver.ts | 64 +- packages/common/src/utils/mutex.ts | 205 +----- packages/node/package.json | 1 + packages/node/src/db/PowerSyncDatabase.ts | 48 +- packages/node/src/db/WorkerConnectionPool.ts | 4 +- packages/node/src/sync/stream/NodeRemote.ts | 4 +- .../stream/NodeStreamingSyncImplementation.ts | 2 +- packages/node/tsconfig.json | 3 + packages/powersync-op-sqlite/package.json | 3 +- .../src/db/OPSqliteAdapter.ts | 5 +- packages/powersync-op-sqlite/tsconfig.json | 3 + packages/react-native/package.json | 3 +- .../ReactNativeStreamingSyncImplementation.ts | 4 +- packages/react-native/tsconfig.json | 3 + packages/shared-internals/README.md | 5 + .../legacy/sync_protocol.d.ts | 0 packages/shared-internals/package.json | 62 ++ .../rollup.config.mjs | 0 .../src/client/AbstractPowerSyncDatabase.ts | 582 ++--------------- .../src/client/ConnectionManager.ts | 21 +- .../src/client/CustomQuery.ts | 18 +- .../sync/bucket/BucketStorageAdapter.ts | 16 +- .../src/client/sync/bucket/CrudEntry.ts | 116 ++++ .../client/sync/bucket/SqliteBucketStorage.ts | 21 +- .../src/client/sync/options.ts | 0 .../src/client/sync/stream/AbstractRemote.ts | 5 +- .../AbstractStreamingSyncImplementation.ts | 166 ++--- .../sync/stream/WebsocketClientTransport.ts | 0 .../client/sync/stream/core-instruction.ts | 31 - .../triggers/MemoryTriggerClaimManager.ts | 2 +- .../src/client/triggers/TriggerManagerImpl.ts | 43 +- .../client/watched}/AbstractQueryProcessor.ts | 8 +- .../watched/DifferentialQueryProcessor.ts | 229 +++++++ .../client/watched/OnChangeQueryProcessor.ts | 111 ++++ .../src/client/watched/WatchedQuery.ts | 14 + packages/shared-internals/src/constants.ts | 6 + .../src/db/crud/SyncProgress.ts | 39 ++ .../src/db/crud/SyncStatus.ts | 206 ++++++ packages/shared-internals/src/index.ts | 17 + packages/shared-internals/src/reexports.ts | 1 + .../src/utils/AbortOperation.ts | 0 .../src/utils/BaseObserver.ts | 33 + .../src/utils/ControlledExecutor.ts | 0 .../src/utils/MetaBaseObserver.ts | 65 ++ .../src/utils/async.ts | 0 packages/shared-internals/src/utils/mutex.ts | 186 ++++++ .../src/utils/queue.ts | 0 .../src/utils/stream_transform.ts | 0 .../tests/utils/async.test.ts | 0 .../tests/utils/mutex.test.ts | 0 .../tests/utils/queue.test.ts | 0 .../tests/utils/stream_transform.test.ts | 0 packages/shared-internals/tsconfig.json | 19 + packages/web/package.json | 1 + packages/web/src/db/PowerSyncDatabase.ts | 2 +- .../web/src/db/adapters/AsyncWebAdapter.ts | 6 +- packages/web/src/db/adapters/SSRDBAdapter.ts | 5 +- .../wa-sqlite/ConcurrentConnection.ts | 2 +- .../sync/SSRWebStreamingSyncImplementation.ts | 2 +- .../worker/sync/SharedSyncImplementation.ts | 2 +- packages/web/tests/mocks/MockWebRemote.ts | 2 +- .../web/tests/utils/MockStreamOpenFactory.ts | 2 +- packages/web/tsconfig.json | 3 + pnpm-lock.yaml | 106 +-- tsconfig.json | 9 +- 91 files changed, 2133 insertions(+), 1900 deletions(-) create mode 100644 packages/common/src/client/CommonPowerSyncDatabase.ts delete mode 100644 packages/common/src/client/constants.ts create mode 100644 packages/shared-internals/README.md rename packages/{common => shared-internals}/legacy/sync_protocol.d.ts (100%) create mode 100644 packages/shared-internals/package.json rename packages/{common => shared-internals}/rollup.config.mjs (100%) rename packages/{common => shared-internals}/src/client/AbstractPowerSyncDatabase.ts (56%) rename packages/{common => shared-internals}/src/client/ConnectionManager.ts (96%) rename packages/{common => shared-internals}/src/client/CustomQuery.ts (78%) rename packages/{common => shared-internals}/src/client/sync/bucket/BucketStorageAdapter.ts (83%) create mode 100644 packages/shared-internals/src/client/sync/bucket/CrudEntry.ts rename packages/{common => shared-internals}/src/client/sync/bucket/SqliteBucketStorage.ts (94%) create mode 100644 packages/shared-internals/src/client/sync/options.ts rename packages/{common => shared-internals}/src/client/sync/stream/AbstractRemote.ts (98%) rename packages/{common => shared-internals}/src/client/sync/stream/AbstractStreamingSyncImplementation.ts (86%) rename packages/{common => shared-internals}/src/client/sync/stream/WebsocketClientTransport.ts (100%) rename packages/{common => shared-internals}/src/client/sync/stream/core-instruction.ts (61%) rename packages/{common => shared-internals}/src/client/triggers/MemoryTriggerClaimManager.ts (90%) rename packages/{common => shared-internals}/src/client/triggers/TriggerManagerImpl.ts (93%) rename packages/{common/src/client/watched/processors => shared-internals/src/client/watched}/AbstractQueryProcessor.ts (96%) create mode 100644 packages/shared-internals/src/client/watched/DifferentialQueryProcessor.ts create mode 100644 packages/shared-internals/src/client/watched/OnChangeQueryProcessor.ts create mode 100644 packages/shared-internals/src/client/watched/WatchedQuery.ts create mode 100644 packages/shared-internals/src/constants.ts create mode 100644 packages/shared-internals/src/db/crud/SyncProgress.ts create mode 100644 packages/shared-internals/src/db/crud/SyncStatus.ts create mode 100644 packages/shared-internals/src/index.ts create mode 100644 packages/shared-internals/src/reexports.ts rename packages/{common => shared-internals}/src/utils/AbortOperation.ts (100%) create mode 100644 packages/shared-internals/src/utils/BaseObserver.ts rename packages/{common => shared-internals}/src/utils/ControlledExecutor.ts (100%) create mode 100644 packages/shared-internals/src/utils/MetaBaseObserver.ts rename packages/{common => shared-internals}/src/utils/async.ts (100%) create mode 100644 packages/shared-internals/src/utils/mutex.ts rename packages/{common => shared-internals}/src/utils/queue.ts (100%) rename packages/{common => shared-internals}/src/utils/stream_transform.ts (100%) rename packages/{common => shared-internals}/tests/utils/async.test.ts (100%) rename packages/{common => shared-internals}/tests/utils/mutex.test.ts (100%) rename packages/{common => shared-internals}/tests/utils/queue.test.ts (100%) rename packages/{common => shared-internals}/tests/utils/stream_transform.test.ts (100%) create mode 100644 packages/shared-internals/tsconfig.json diff --git a/packages/adapter-sql-js/package.json b/packages/adapter-sql-js/package.json index dd79b27c9..d16255f3b 100644 --- a/packages/adapter-sql-js/package.json +++ b/packages/adapter-sql-js/package.json @@ -32,7 +32,8 @@ "test": "vitest" }, "dependencies": { - "@powersync/common": "workspace:^" + "@powersync/common": "workspace:^", + "@powersync/shared-internals": "workspace:*" }, "devDependencies": { "@powersync/sql-js": "0.0.9", diff --git a/packages/adapter-sql-js/src/SQLJSAdapter.ts b/packages/adapter-sql-js/src/SQLJSAdapter.ts index f45e58254..d276cea44 100644 --- a/packages/adapter-sql-js/src/SQLJSAdapter.ts +++ b/packages/adapter-sql-js/src/SQLJSAdapter.ts @@ -12,15 +12,14 @@ import { DBLockOptions, LockContext, LogLevels, - Mutex, PowerSyncLogger, QueryResult, SqlExecutor, SQLOpenFactory, SQLOpenOptions, - timeoutSignal, Transaction } from '@powersync/common'; +import { Mutex, timeoutSignal } from '@powersync/shared-internals'; // This uses a pure JS version which avoids the need for WebAssembly, which is not supported in React Native. import SQLJs from '@powersync/sql-js/dist/sql-asm.js'; diff --git a/packages/adapter-sql-js/tsconfig.json b/packages/adapter-sql-js/tsconfig.json index baef22195..13e696619 100644 --- a/packages/adapter-sql-js/tsconfig.json +++ b/packages/adapter-sql-js/tsconfig.json @@ -16,6 +16,9 @@ "references": [ { "path": "../common" + }, + { + "path": "../shared-internals" } ], "include": ["src/**/*"] diff --git a/packages/capacitor/package.json b/packages/capacitor/package.json index cea01abe0..b35c587a7 100644 --- a/packages/capacitor/package.json +++ b/packages/capacitor/package.json @@ -89,6 +89,9 @@ "@capacitor-community/sqlite": "^8.1.0", "@powersync/web": "workspace:^1.38.3" }, + "dependencies": { + "@powersync/shared-internals": "workspace:*" + }, "swiftlint": "@ionic/swiftlint-config", "capacitor": { "name": "PowerSyncCapacitor", diff --git a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts index 2a257659e..cc1f94984 100644 --- a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts +++ b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts @@ -10,10 +10,9 @@ import { DBAdapterListener, DBLockOptions, LockContext, - Mutex, - QueryResult, - timeoutSignal + QueryResult } from '@powersync/web'; +import { Mutex, timeoutSignal } from '@powersync/shared-internals'; import { PowerSyncCore } from '../plugin/PowerSyncCore.js'; import { messageForErrorCode } from '../plugin/PowerSyncPlugin.js'; import { CapacitorSQLiteOpenFactoryOptions, DEFAULT_SQLITE_OPTIONS } from './CapacitorSQLiteOpenFactory.js'; diff --git a/packages/capacitor/src/sync/CapacitorSyncImplementation.ts b/packages/capacitor/src/sync/CapacitorSyncImplementation.ts index 4cac5684e..c9696397e 100644 --- a/packages/capacitor/src/sync/CapacitorSyncImplementation.ts +++ b/packages/capacitor/src/sync/CapacitorSyncImplementation.ts @@ -1,4 +1,5 @@ -import { AbstractStreamingSyncImplementation, LockOptions, LockType, Mutex } from '@powersync/web'; +import { AbstractStreamingSyncImplementation, LockOptions, LockType } from '@powersync/web'; +import { Mutex } from '@powersync/shared-internals'; type MutexMap = { /** diff --git a/packages/capacitor/tsconfig.json b/packages/capacitor/tsconfig.json index 0f31d57b4..7b2b09450 100644 --- a/packages/capacitor/tsconfig.json +++ b/packages/capacitor/tsconfig.json @@ -16,5 +16,5 @@ }, "files": ["src/index.ts"], "exclude": ["node_modules"], - "references": [{ "path": "../common" }, { "path": "../web" }] + "references": [{ "path": "../common" }, { "path": "../shared-internals" }, { "path": "../web" }] } diff --git a/packages/common/package.json b/packages/common/package.json index 09c439955..e35480031 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -7,32 +7,10 @@ }, "description": "API definitions for PowerSync", "type": "module", - "main": "dist/bundle.mjs", - "module": "dist/bundle.mjs", - "types": "lib/index.d.ts", "exports": { ".": { - "node": { - "import": { - "types": "./lib/index.d.ts", - "default": "./dist/bundle.node.mjs" - }, - "require": { - "types": "./dist/index.d.cts", - "require": "./dist/bundle.node.cjs" - } - }, - "import": { - "types": "./lib/index.d.ts", - "default": "./dist/bundle.mjs" - }, - "require": { - "types": "./dist/index.d.cts", - "require": "./dist/bundle.cjs" - } - }, - "./internal/sync_protocol": { - "types": "./legacy/sync_protocol.d.ts" + "types": "./lib/index.d.ts", + "default": "./lib/index.js" } }, "author": "PowerSync", @@ -52,30 +30,18 @@ }, "homepage": "https://docs.powersync.com", "scripts": { - "build": "tsc -b && rollup -c rollup.config.mjs", - "build:prod": "tsc -b && rollup -c rollup.config.mjs", + "build": "tsc -b", + "build:prod": "tsc -b", "clean": "rm -rf lib dist tsconfig.tsbuildinfo", "test": "vitest", - "test:exports": "attw --pack . --exclude-entrypoints internal/sync_protocol" + "test:exports": "attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm" }, "dependencies": { "event-iterator": "^2.0.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "catalog:", - "@rollup/plugin-inject": "catalog:", - "@rollup/plugin-json": "catalog:", - "@rollup/plugin-node-resolve": "catalog:", "@types/node": "catalog:", "@types/uuid": "catalog:", - "buffer": "^6.0.3", - "cross-fetch": "^4.1.0", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21", - "rollup": "catalog:", - "rollup-plugin-dts": "catalog:", - "rsocket-core": "1.0.0-alpha.3", - "rsocket-websocket-client": "1.0.0-alpha.3", "@microsoft/api-extractor": "catalog:" } } diff --git a/packages/common/src/attachments/AttachmentContext.ts b/packages/common/src/attachments/AttachmentContext.ts index 0e8b02e47..21adeacca 100644 --- a/packages/common/src/attachments/AttachmentContext.ts +++ b/packages/common/src/attachments/AttachmentContext.ts @@ -1,7 +1,7 @@ -import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js'; import { LogLevels, PowerSyncLogger } from '../utils/Logger.js'; import { Transaction } from '../db/DBAdapter.js'; import { AttachmentRecord, AttachmentState, attachmentFromSql } from './Schema.js'; +import { CommonPowerSyncDatabase } from '../client/CommonPowerSyncDatabase.js'; /** * AttachmentContext provides database operations for managing attachment records. @@ -14,7 +14,7 @@ import { AttachmentRecord, AttachmentState, attachmentFromSql } from './Schema.j */ export class AttachmentContext { /** PowerSync database instance for executing queries */ - readonly db: AbstractPowerSyncDatabase; + readonly db: CommonPowerSyncDatabase; /** Name of the database table storing attachment records */ readonly tableName: string; @@ -33,7 +33,7 @@ export class AttachmentContext { * @param logger - Logger instance for diagnostic output */ constructor( - db: AbstractPowerSyncDatabase, + db: CommonPowerSyncDatabase, tableName: string = 'attachments', logger: PowerSyncLogger, archivedCacheLimit: number diff --git a/packages/common/src/attachments/AttachmentQueue.ts b/packages/common/src/attachments/AttachmentQueue.ts index 5ecbb15ed..27099475c 100644 --- a/packages/common/src/attachments/AttachmentQueue.ts +++ b/packages/common/src/attachments/AttachmentQueue.ts @@ -1,5 +1,3 @@ -import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js'; -import { DEFAULT_WATCH_THROTTLE_MS } from '../client/watched/WatchedQuery.js'; import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js'; import { LogLevels, PowerSyncLogger } from '../utils/Logger.js'; import { Transaction } from '../db/DBAdapter.js'; @@ -11,6 +9,7 @@ import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js'; import { SyncingService } from './SyncingService.js'; import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; +import { CommonPowerSyncDatabase } from '../client/CommonPowerSyncDatabase.js'; /** * AttachmentQueue manages the lifecycle and synchronization of attachments @@ -62,7 +61,7 @@ export class AttachmentQueue implements AttachmentQueue { * quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls * how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls * for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */ - readonly syncThrottleDuration: number; + readonly syncThrottleDuration?: number; /** Whether to automatically download remote attachments. Default: true */ readonly downloadAttachments: boolean = true; @@ -74,7 +73,7 @@ export class AttachmentQueue implements AttachmentQueue { private readonly attachmentService: AttachmentService; /** PowerSync database instance */ - private readonly db: AbstractPowerSyncDatabase; + private readonly db: CommonPowerSyncDatabase; /** Cleanup function for status change listener */ private statusListenerDispose?: () => void; @@ -96,7 +95,7 @@ export class AttachmentQueue implements AttachmentQueue { logger, tableName = ATTACHMENT_TABLE, syncIntervalMs = 30 * 1000, - syncThrottleDuration = DEFAULT_WATCH_THROTTLE_MS, + syncThrottleDuration, downloadAttachments = true, archivedCacheLimit = 100, errorHandler @@ -104,7 +103,7 @@ export class AttachmentQueue implements AttachmentQueue { /** * PowerSync database instance */ - db: AbstractPowerSyncDatabase; + db: CommonPowerSyncDatabase; /** * Remote storage adapter for upload/download operations */ diff --git a/packages/common/src/attachments/AttachmentService.ts b/packages/common/src/attachments/AttachmentService.ts index 482ed0dd6..0e41ca7ce 100644 --- a/packages/common/src/attachments/AttachmentService.ts +++ b/packages/common/src/attachments/AttachmentService.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js'; +import { CommonPowerSyncDatabase } from '../client/CommonPowerSyncDatabase.js'; import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js'; import { PowerSyncLogger, LogLevels } from '../utils/Logger.js'; import { Mutex } from '../utils/mutex.js'; @@ -11,15 +11,16 @@ import { AttachmentRecord, AttachmentState } from './Schema.js'; * @internal */ export class AttachmentService { - private mutex = new Mutex(); + private mutex: Mutex; private context: AttachmentContext; constructor( - private db: AbstractPowerSyncDatabase, + private db: CommonPowerSyncDatabase, private logger: PowerSyncLogger, private tableName: string = 'attachments', archivedCacheLimit: number = 100 ) { + this.mutex = db.createMutex(); this.context = new AttachmentContext(db, tableName, logger, archivedCacheLimit); } diff --git a/packages/common/src/client/CommonPowerSyncDatabase.ts b/packages/common/src/client/CommonPowerSyncDatabase.ts new file mode 100644 index 000000000..5c76bd251 --- /dev/null +++ b/packages/common/src/client/CommonPowerSyncDatabase.ts @@ -0,0 +1,606 @@ +import { DBAdapter, LockContext, QueryResult, Transaction } from '../db/DBAdapter.js'; +import { SyncStatus } from '../db/crud/SyncStatus.js'; +import { Schema } from '../db/schema/Schema.js'; +import { BaseListener, BaseObserverInterface } from '../utils/BaseObserver.js'; +import { WatchedQueryComparator } from './watched/processors/comparators.js'; +import { PowerSyncLogger } from '../utils/Logger.js'; +import { DatabaseSource } from './SQLOpenFactory.js'; +import { TriggerManager } from './triggers/TriggerManager.js'; +import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js'; +import { SyncOptions } from './sync/options.js'; +import { SyncStream } from './sync/sync-streams.js'; +import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js'; +import { CrudBatch } from './sync/bucket/CrudBatch.js'; +import { CrudTransaction } from './sync/bucket/CrudTransaction.js'; +import { ArrayQueryDefinition, Query } from './Query.js'; +import { WatchCompatibleQuery } from './watched/WatchedQuery.js'; +import { Mutex } from '../utils/mutex.js'; + +/** + * @public + */ +export interface DisconnectAndClearOptions { + /** When set to false, data in local-only tables is preserved. */ + clearLocal?: boolean; +} + +/** + * Options required regardless of how a PowerSync database is opened. + * + * @public + */ +export interface BasePowerSyncDatabaseOptions { + /** Schema used for the local database. */ + schema: Schema; + logger?: PowerSyncLogger; +} + +/** + * @public + */ +export type PowerSyncDatabaseOptions = BasePowerSyncDatabaseOptions & DatabaseSource; + +/** + * @public + */ +export interface SQLOnChangeOptions { + signal?: AbortSignal; + tables?: string[]; + /** The minimum interval between queries. */ + throttleMs?: number; + /** + * @deprecated All tables specified in {@link SQLOnChangeOptions.tables} will be watched, including PowerSync tables + * with prefixes. + * + * Allows for watching any SQL table + * by not removing PowerSync table name prefixes + */ + rawTableNames?: boolean; + /** + * Emits an empty result set immediately + */ + triggerImmediate?: boolean; +} + +/** + * @public + */ +export interface SQLWatchOptions extends SQLOnChangeOptions { + /** + * Optional comparator which will be used to compare the results of the query. + * The watched query will only yield results if the comparator returns false. + */ + comparator?: WatchedQueryComparator; +} + +/** + * @public + */ +export interface WatchOnChangeEvent { + changedTables: string[]; +} + +/** + * @public + */ +export interface WatchHandler { + onResult: (results: QueryResult) => void; + onError?: (error: Error) => void; +} + +/** + * @public + */ +export interface WatchOnChangeHandler { + onChange: (event: WatchOnChangeEvent) => Promise | void; + onError?: (error: Error) => void; +} + +/** + * @public + */ +export interface PowerSyncDBListener extends BaseListener { + initialized: () => void; + schemaChanged: (schema: Schema) => void; + statusChanged?: ((status: SyncStatus) => void) | undefined; + closing: () => Promise | void; + closed: () => Promise | void; +} + +/** + * @public + */ +export interface PowerSyncCloseOptions { + /** + * Disconnect the sync stream client if connected. + * This is usually true, but can be false for Web when using + * multiple tabs and a shared sync provider. + */ + disconnect?: boolean; +} + +/** + * @public + */ +export interface PowerSyncDatabaseConstructor { + new (options: Options): CommonPowerSyncDatabase; +} + +/** + * @public + */ +export interface CommonPowerSyncDatabase extends BaseObserverInterface { + /** + * Returns true if the connection is closed. + */ + readonly closed: boolean; + readonly ready: boolean; + + /** + * Current connection status. + */ + readonly currentStatus: SyncStatus; + + readonly sdkVersion: string; + + /** + * @experimental + * Allows creating SQLite triggers which can be used to track various operations on SQLite tables. + */ + readonly triggers: TriggerManager; + + readonly logger: PowerSyncLogger; + + /** + * Schema used for the local database. + */ + readonly schema: Schema; + + /** + * The underlying database. + * + * For the most part, behavior is the same whether querying on the underlying database, or on {@link CommonPowerSyncDatabase}. + */ + get database(): DBAdapter; + + /** + * Whether a connection to the PowerSync service is currently open. + */ + get connected(): boolean; + + get connecting(): boolean; + + /** + * @returns A promise which will resolve once initialization is completed. + */ + waitForReady(): Promise; + + /** + * Wait for the first sync operation to complete. + * + * @param request - Either an abort signal (after which the promise will complete regardless of + * whether a full sync was completed) or an object providing an abort signal and a priority target. + * When a priority target is set, the promise may complete when all buckets with the given (or higher) + * priorities have been synchronized. This can be earlier than a complete sync. + * @returns A promise which will resolve once the first full sync has completed. + */ + waitForFirstSync(request?: AbortSignal | { signal?: AbortSignal; priority?: number }): Promise; + + /** + * Waits for the first sync status for which the `status` callback returns a truthy value. + */ + waitForStatus(predicate: (status: SyncStatus) => any, signal?: AbortSignal): Promise; + + /** + * Replace the schema with a new version. This is for advanced use cases - typically the schema should just be specified once in the constructor. + * + * Cannot be used while connected - this should only be called before {@link CommonPowerSyncDatabase.connect}. + */ + updateSchema(schema: Schema): Promise; + + /** + * Wait for initialization to complete. + * While initializing is automatic, this helps to catch and report initialization errors. + */ + init(): Promise; + + /** + * Connects to stream of events from the PowerSync instance. + */ + connect(connector: PowerSyncBackendConnector, options?: SyncOptions): Promise; + + /** + * Close the sync connection. + * + * Use {@link CommonPowerSyncDatabase.connect} to connect again. + */ + disconnect(): Promise; + + /** + * Disconnect and clear the database. + * Use this when logging out. + * The database can still be queried after this is called, but the tables + * would be empty. + * + * To preserve data in local-only tables, set clearLocal to false. + */ + disconnectAndClear(options?: DisconnectAndClearOptions): Promise; + + /** + * Create a sync stream to query its status or to subscribe to it. + * + * @param name - The name of the stream to subscribe to. + * @param params - Optional parameters for the stream subscription. + * @returns A {@link SyncStream} instance that can be subscribed to. + */ + syncStream(name: string, params?: Record): SyncStream; + + /** + * Close the database, releasing resources. + * + * Also disconnects any active connection. + * + * Once close is called, this connection cannot be used again - a new one + * must be constructed. + */ + close(options?: PowerSyncCloseOptions): Promise; + + /** + * Get upload queue size estimate and count. + */ + getUploadQueueStats(includeSize?: boolean): Promise; + + /** + * Get a batch of CRUD data to upload. + * + * Returns null if there is no data to upload. + * + * Use this from the {@link PowerSyncBackendConnector.uploadData} callback. + * + * Once the data have been successfully uploaded, call {@link CrudBatch.complete} before + * requesting the next batch. + * + * Use the `limit` parameter to specify the maximum number of updates to return in a single + * batch. + * + * This method does include transaction ids in the result, but does not group + * data by transaction. One batch may contain data from multiple transactions, + * and a single transaction may be split over multiple batches. + * + * @param limit - Maximum number of CRUD entries to include in the batch + * @returns A batch of CRUD operations to upload, or null if there are none + */ + getCrudBatch(limit?: number): Promise; + + /** + * Get the next recorded transaction to upload. + * + * Returns null if there is no data to upload. + * + * Use this from the {@link PowerSyncBackendConnector.uploadData} callback. + * + * Once the data have been successfully uploaded, call {@link CrudTransaction.complete} before + * requesting the next transaction. + * + * Unlike {@link AbstractPowerSyncDatabase.getCrudBatch}, this only returns data from a single transaction at a time. + * All data for the transaction is loaded into memory. + * + * @returns A transaction of CRUD operations to upload, or null if there are none + */ + getNextCrudTransaction(): Promise; + + /** + * Returns an async iterator of completed transactions with local writes against the database. + * + * This is typically used from the {@link PowerSyncBackendConnector.uploadData} callback. Each entry emitted by the + * returned iterator is a full transaction containing all local writes made while that transaction was active. + * + * Unlike {@link AbstractPowerSyncDatabase.getNextCrudTransaction}, which always returns the oldest transaction that hasn't been + * {@link CrudTransaction.complete}d yet, this iterator can be used to receive multiple transactions. Calling + * {@link CrudTransaction.complete} will mark that and all prior transactions emitted by the iterator as completed. + * + * This can be used to upload multiple transactions in a single batch, e.g with: + * + * ```JavaScript + * let lastTransaction = null; + * let batch = []; + * + * for await (const transaction of database.getCrudTransactions()) { + * batch.push(...transaction.crud); + * lastTransaction = transaction; + * + * if (batch.length > 10) { + * break; + * } + * } + * ``` + * + * If there is no local data to upload, the async iterator complete without emitting any items. + * + * Note that iterating over async iterables requires a [polyfill](https://github.com/powersync-ja/powersync-js/tree/main/packages/react-native#babel-plugins-watched-queries) + * for React Native. + */ + getCrudTransactions(): AsyncIterable; + + /** + * Get an unique client id for this database. + * + * The id is not reset when the database is cleared, only when the database is deleted. + * + * @returns A unique identifier for the database instance + */ + getClientId(): Promise; + + /** + * Execute a SQL write (INSERT/UPDATE/DELETE) query + * and optionally return results. + * + * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure), + * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements. + * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed. + * + * @param sql - The SQL query to execute + * @param parameters - Optional array of parameters to bind to the query + * @returns The query result as an object with structured key-value pairs + */ + execute(sql: string, parameters?: any[]): Promise; + + /** + * Execute a SQL write (INSERT/UPDATE/DELETE) query directly on the database without any PowerSync processing. + * This bypasses certain PowerSync abstractions and is useful for accessing the raw database results. + * + * @param sql - The SQL query to execute + * @param parameters - Optional array of parameters to bind to the query + * @returns The raw query result from the underlying database as a nested array of raw values, where each row is + * represented as an array of column values without field names. + */ + executeRaw(sql: string, parameters?: any[]): Promise; + + /** + * Execute a write query (INSERT/UPDATE/DELETE) multiple times with each parameter set + * and optionally return results. + * This is faster than executing separately with each parameter set. + * + * @param sql - The SQL query to execute + * @param parameters - Optional 2D array of parameter sets, where each inner array is a set of parameters for one execution + * @returns The query result + */ + executeBatch(sql: string, parameters?: any[][]): Promise; + + /** + * Execute a read-only query and return results. + * + * @param sql - The SQL query to execute + * @param parameters - Optional array of parameters to bind to the query + * @returns An array of results + */ + getAll(sql: string, parameters?: any[]): Promise; + + /** + * Execute a read-only query and return the first result, or null if the ResultSet is empty. + * + * @param sql - The SQL query to execute + * @param parameters - Optional array of parameters to bind to the query + * @returns The first result if found, or null if no results are returned + */ + getOptional(sql: string, parameters?: any[]): Promise; + + /** + * Execute a read-only query and return the first result, error if the ResultSet is empty. + * + * @param sql - The SQL query to execute + * @param parameters - Optional array of parameters to bind to the query + * @returns The first result matching the query + * @throws Error if no rows are returned + */ + get(sql: string, parameters?: any[]): Promise; + + /** + * Takes a read lock, without starting a transaction. + * In most cases, {@link CommonPowerSyncDatabase.readTransaction} should be used instead. + */ + readLock(callback: (db: LockContext) => Promise): Promise; + + /** + * Takes a global lock, without starting a transaction. + * In most cases, {@link CommonPowerSyncDatabase.writeTransaction} should be used instead. + */ + writeLock(callback: (db: LockContext) => Promise): Promise; + + /** + * Open a read-only transaction. + * Read transactions can run concurrently to a write transaction. + * Changes from any write transaction are not visible to read transactions started before it. + * + * @param callback - Function to execute within the transaction + * @param lockTimeout - Time in milliseconds to wait for a lock before throwing an error + * @returns The result of the callback + * @throws Error if the lock cannot be obtained within the timeout period + */ + readTransaction(callback: (tx: Transaction) => Promise, lockTimeout?: number): Promise; + + /** + * Open a read-write transaction. + * This takes a global lock - only one write transaction can execute against the database at a time. + * Statements within the transaction must be done on the provided {@link Transaction} interface. + * + * @param callback - Function to execute within the transaction + * @param lockTimeout - Time in milliseconds to wait for a lock before throwing an error + * @returns The result of the callback + * @throws Error if the lock cannot be obtained within the timeout period + */ + writeTransaction(callback: (tx: Transaction) => Promise, lockTimeout?: number): Promise; + + /** + * This version of `watch` uses `AsyncGenerator`, for documentation see {@link AbstractPowerSyncDatabase.watchWithAsyncGenerator}. + * Can be overloaded to use a callback handler instead, for documentation see {@link AbstractPowerSyncDatabase.watchWithCallback}. + * + * @example + * ```javascript + * async *attachmentIds() { + * for await (const result of this.powersync.watch( + * `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`, + * [] + * )) { + * yield result.rows?._array.map((r) => r.id) ?? []; + * } + * } + * ``` + */ + watch(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable; + /** + * See {@link AbstractPowerSyncDatabase.watchWithCallback}. + * + * @example + * ```javascript + * onAttachmentIdsChange(onResult) { + * this.powersync.watch( + * `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`, + * [], + * { + * onResult: (result) => onResult(result.rows?._array.map((r) => r.id) ?? []) + * } + * ); + * } + * ``` + */ + watch(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void; + + /** + * Allows defining a query which can be used to build a {@link WatchedQuery}. + * The defined query will be executed with {@link AbstractPowerSyncDatabase#getAll}. + * An optional mapper function can be provided to transform the results. + * + * @example + * ```javascript + * const watchedTodos = powersync.query({ + * sql: `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`, + * parameters: [], + * mapper: (row) => ({ + * ...row, + * created_at: new Date(row.created_at as string) + * }) + * }) + * .watch() + * // OR use .differentialWatch() for fine-grained watches. + * ``` + */ + query(query: ArrayQueryDefinition): Query; + + /** + * Allows building a {@link WatchedQuery} using an existing {@link WatchCompatibleQuery}. + * The watched query will use the provided {@link WatchCompatibleQuery.execute} method to query results. + * + * @example + * ```javascript + * + * // Potentially a query from an ORM like Drizzle + * const query = db.select().from(lists); + * + * const watchedTodos = powersync.customQuery(query) + * .watch() + * // OR use .differentialWatch() for fine-grained watches. + * ``` + */ + customQuery(query: WatchCompatibleQuery): Query; + + /** + * Execute a read query every time the source tables are modified. + * Use {@link SQLOnChangeOptions.throttleMs} to specify the minimum interval between queries. + * Source tables are automatically detected using `EXPLAIN QUERY PLAN`. + * + * Note that the `onChange` callback member of the handler is required. + * + * @param sql - The SQL query to execute + * @param parameters - Optional array of parameters to bind to the query + * @param handler - Callbacks for handling results and errors + * @param options - Options for configuring watch behavior + */ + watchWithCallback(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void; + + /** + * Execute a read query every time the source tables are modified. + * Use {@link SQLOnChangeOptions.throttleMs} to specify the minimum interval between queries. + * Source tables are automatically detected using `EXPLAIN QUERY PLAN`. + * + * @param sql - The SQL query to execute + * @param parameters - Optional array of parameters to bind to the query + * @param options - Options for configuring watch behavior + * @returns An AsyncIterable that yields QueryResults whenever the data changes + */ + watchWithAsyncGenerator(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable; + + /** + * Resolves the list of tables that are used in a SQL query. + * If tables are specified in the options, those are used directly. + * Otherwise, analyzes the query using EXPLAIN to determine which tables are accessed. + * + * @param sql - The SQL query to analyze + * @param parameters - Optional parameters for the SQL query + * @param options - Optional watch options that may contain explicit table list + * @returns Array of table names that the query depends on + */ + resolveTables(sql: string, parameters?: any[], options?: SQLWatchOptions): Promise; + + /** + * This version of `onChange` uses `AsyncGenerator`, for documentation see {@link AbstractPowerSyncDatabase.onChangeWithAsyncGenerator}. + * Can be overloaded to use a callback handler instead, for documentation see {@link AbstractPowerSyncDatabase.onChangeWithCallback}. + * + * @example + * ```javascript + * async monitorChanges() { + * for await (const event of this.powersync.onChange({tables: ['todos']})) { + * console.log('Detected change event:', event); + * } + * } + * ``` + */ + onChange(options?: SQLOnChangeOptions): AsyncIterable; + /** + * See {@link AbstractPowerSyncDatabase.onChangeWithCallback}. + * + * @example + * ```javascript + * monitorChanges() { + * this.powersync.onChange({ + * onChange: (event) => { + * console.log('Change detected:', event); + * } + * }, { tables: ['todos'] }); + * } + * ``` + */ + onChange(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void; + + /** + * Invoke the provided callback on any changes to any of the specified tables. + * + * This is preferred over {@link AbstractPowerSyncDatabase.watchWithCallback} when multiple queries need to be performed + * together when data is changed. + * + * Note that the `onChange` callback member of the handler is required. + * + * @param handler - Callbacks for handling change events and errors + * @param options - Options for configuring watch behavior + * @returns A dispose function to stop watching for changes + */ + onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void; + + /** + * Create a Stream of changes to any of the specified tables. + * + * This is preferred over {@link AbstractPowerSyncDatabase.watchWithAsyncGenerator} when multiple queries need to be + * performed together when data is changed. + * + * Note: do not declare this as `async *onChange` as it will not work in React Native. + * + * @param options - Options for configuring watch behavior + * @returns An AsyncIterable that yields change events whenever the specified tables change + */ + onChangeWithAsyncGenerator(options?: SQLWatchOptions): AsyncIterable; + + /** + * @internal + */ + createMutex(): Mutex; +} diff --git a/packages/common/src/client/compilableQueryWatch.ts b/packages/common/src/client/compilableQueryWatch.ts index a7371eddd..6ef5ad127 100644 --- a/packages/common/src/client/compilableQueryWatch.ts +++ b/packages/common/src/client/compilableQueryWatch.ts @@ -1,5 +1,5 @@ import { CompilableQuery } from './../types/types.js'; -import { AbstractPowerSyncDatabase, SQLWatchOptions } from './AbstractPowerSyncDatabase.js'; +import { CommonPowerSyncDatabase, SQLWatchOptions } from './CommonPowerSyncDatabase.js'; import { runOnSchemaChange } from './runOnSchemaChange.js'; /** @@ -14,7 +14,7 @@ export interface CompilableQueryWatchHandler { * @public */ export function compilableQueryWatch( - db: AbstractPowerSyncDatabase, + db: CommonPowerSyncDatabase, query: CompilableQuery, handler: CompilableQueryWatchHandler, options?: SQLWatchOptions diff --git a/packages/common/src/client/connection/PowerSyncBackendConnector.ts b/packages/common/src/client/connection/PowerSyncBackendConnector.ts index 65b462bda..c185a7632 100644 --- a/packages/common/src/client/connection/PowerSyncBackendConnector.ts +++ b/packages/common/src/client/connection/PowerSyncBackendConnector.ts @@ -1,5 +1,5 @@ import { PowerSyncCredentials } from './PowerSyncCredentials.js'; -import type { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js'; +import { CommonPowerSyncDatabase } from '../CommonPowerSyncDatabase.js'; /** * @public @@ -24,5 +24,5 @@ export interface PowerSyncBackendConnector { * * Any thrown errors will result in a retry after the configured wait period (default: 5 seconds). */ - uploadData: (database: AbstractPowerSyncDatabase) => Promise; + uploadData: (database: CommonPowerSyncDatabase) => Promise; } diff --git a/packages/common/src/client/constants.ts b/packages/common/src/client/constants.ts deleted file mode 100644 index 319320da0..000000000 --- a/packages/common/src/client/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * @internal - */ -export const MAX_OP_ID = '9223372036854775807'; diff --git a/packages/common/src/client/runOnSchemaChange.ts b/packages/common/src/client/runOnSchemaChange.ts index 73dd09a18..2ea0593e8 100644 --- a/packages/common/src/client/runOnSchemaChange.ts +++ b/packages/common/src/client/runOnSchemaChange.ts @@ -1,11 +1,11 @@ -import { AbstractPowerSyncDatabase, SQLWatchOptions } from './AbstractPowerSyncDatabase.js'; +import { CommonPowerSyncDatabase, SQLWatchOptions } from './CommonPowerSyncDatabase.js'; /** * @internal */ export function runOnSchemaChange( callback: (signal: AbortSignal) => void, - db: AbstractPowerSyncDatabase, + db: CommonPowerSyncDatabase, options?: SQLWatchOptions ): void { const triggerWatchedQuery = () => { diff --git a/packages/common/src/client/sync/bucket/CrudEntry.ts b/packages/common/src/client/sync/bucket/CrudEntry.ts index 82686e540..265829480 100644 --- a/packages/common/src/client/sync/bucket/CrudEntry.ts +++ b/packages/common/src/client/sync/bucket/CrudEntry.ts @@ -21,44 +21,12 @@ export enum UpdateType { DELETE = 'DELETE' } -/** - * @internal - */ -export type CrudEntryJSON = { - id: string; - data: string; - tx_id?: number; -}; - -type CrudEntryDataJSON = { - data: Record; - old?: Record; - op: UpdateType; - type: string; - id: string; - metadata?: string; -}; - -/** - * The output JSON seems to be a third type of JSON, not the same as the input JSON. - */ -type CrudEntryOutputJSON = { - op_id: number; - op: UpdateType; - type: string; - id: string; - tx_id?: number; - data?: Record; - old?: Record; - metadata?: string; -}; - /** * A single client-side change. * * @public */ -export class CrudEntry { +export interface CrudEntry { /** * Auto-incrementing client-side id. */ @@ -99,83 +67,15 @@ export class CrudEntry { */ metadata?: string; - static fromRow(dbRow: CrudEntryJSON) { - const data: CrudEntryDataJSON = JSON.parse(dbRow.data); - return new CrudEntry( - parseInt(dbRow.id), - data.op, - data.type, - data.id, - dbRow.tx_id, - data.data, - data.old, - data.metadata - ); - } - - constructor( - clientId: number, - op: UpdateType, - table: string, - id: string, - transactionId?: number, - opData?: Record, - previousValues?: Record, - metadata?: string - ) { - this.clientId = clientId; - this.id = id; - this.op = op; - this.opData = opData; - this.table = table; - this.transactionId = transactionId; - this.previousValues = previousValues; - this.metadata = metadata; - } - /** * Converts the change to JSON format. */ - toJSON(): CrudEntryOutputJSON { - return { - op_id: this.clientId, - op: this.op, - type: this.table, - id: this.id, - tx_id: this.transactionId, - data: this.opData, - old: this.previousValues, - metadata: this.metadata - }; - } - - equals(entry: CrudEntry) { - return JSON.stringify(this.toComparisonArray()) == JSON.stringify(entry.toComparisonArray()); - } + toJSON(): unknown; - /** - * The hash code for this object. - * @deprecated This should not be necessary in the JS SDK. - * Use the @see CrudEntry#equals method instead. - * TODO remove in the next major release. - */ - hashCode() { - return JSON.stringify(this.toComparisonArray()); - } + equals(entry: CrudEntry): boolean; /** * Generates an array for use in deep comparison operations */ - toComparisonArray() { - return [ - this.transactionId, - this.clientId, - this.op, - this.table, - this.id, - this.opData, - this.previousValues, - this.metadata - ]; - } + toComparisonArray(): unknown[]; } diff --git a/packages/common/src/client/triggers/TriggerManager.ts b/packages/common/src/client/triggers/TriggerManager.ts index b70234169..2ebcf8292 100644 --- a/packages/common/src/client/triggers/TriggerManager.ts +++ b/packages/common/src/client/triggers/TriggerManager.ts @@ -472,38 +472,3 @@ export interface TriggerManager { */ trackTableDiff(options: TrackDiffOptions): Promise; } - -/** - * @experimental - * @internal - * Manages claims on persisted SQLite triggers and destination tables to enable proper cleanup - * when they are no longer actively in use. - * - * When using persisted triggers (especially for OPFS multi-tab scenarios), we need a reliable way to determine which resources are still actively in use across different connections/tabs so stale resources can be safely cleaned up without interfering with active triggers. - * - * A cleanup process runs - * on database creation (and every 2 minutes) that: - * 1. Queries for existing managed persisted resources - * 2. Checks with the claim manager if any consumer is actively using those resources - * 3. Deletes unused resources - */ - -export interface TriggerClaimManager { - /** - * Obtains or marks a claim on a certain identifier. - * @returns a callback to release the claim. - */ - obtainClaim: (identifier: string) => Promise<() => Promise>; - /** - * Checks if a claim is present for an identifier. - */ - checkClaim: (identifier: string) => Promise; -} - -/** - * @experimental - * @internal - */ -export interface TriggerManagerConfig { - claimManager: TriggerClaimManager; -} diff --git a/packages/common/src/client/watched/GetAllQuery.ts b/packages/common/src/client/watched/GetAllQuery.ts index 9d0b8fb6b..7f6eada66 100644 --- a/packages/common/src/client/watched/GetAllQuery.ts +++ b/packages/common/src/client/watched/GetAllQuery.ts @@ -1,5 +1,5 @@ import { CompiledQuery } from '../../types/types.js'; -import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js'; +import { CommonPowerSyncDatabase } from '../CommonPowerSyncDatabase.js'; import { WatchCompatibleQuery } from './WatchedQuery.js'; /** @@ -24,7 +24,7 @@ export type GetAllQueryOptions = { }; /** - * Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query. + * Performs a {@link CommonPowerSyncDatabase.getAll} operation for a watched query. * * @public */ @@ -38,7 +38,7 @@ export class GetAllQuery implements WatchCompatibleQuery { + async execute(options: { db: CommonPowerSyncDatabase }): Promise { const { db } = options; const { sql, parameters = [] } = this.compile(); const rawResult = await db.getAll>(sql, [...parameters]); diff --git a/packages/common/src/client/watched/WatchedQuery.ts b/packages/common/src/client/watched/WatchedQuery.ts index 472d62d3b..b1be752a1 100644 --- a/packages/common/src/client/watched/WatchedQuery.ts +++ b/packages/common/src/client/watched/WatchedQuery.ts @@ -1,7 +1,7 @@ import { CompiledQuery } from '../../types/types.js'; import { BaseListener } from '../../utils/BaseObserver.js'; import { MetaBaseObserverInterface } from '../../utils/MetaBaseObserver.js'; -import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js'; +import { CommonPowerSyncDatabase } from '../CommonPowerSyncDatabase.js'; /** * State for {@link WatchedQuery} instances. @@ -41,7 +41,7 @@ export interface WatchedQueryState { export interface WatchExecuteOptions { sql: string; parameters: any[]; - db: AbstractPowerSyncDatabase; + db: CommonPowerSyncDatabase; } /** @@ -96,19 +96,6 @@ export interface WatchedQueryListener extends BaseListener { [WatchedQueryListenerEvent.CLOSED]?: () => void | Promise; } -/** - * @internal - */ -export const DEFAULT_WATCH_THROTTLE_MS = 30; - -/** - * @internal - */ -export const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions = { - throttleMs: DEFAULT_WATCH_THROTTLE_MS, - reportFetching: true -}; - /** * @public */ diff --git a/packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts b/packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts index ac68cd90a..cafae6004 100644 --- a/packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts +++ b/packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts @@ -1,10 +1,4 @@ import { WatchCompatibleQuery, WatchedQuery, WatchedQueryListener, WatchedQueryOptions } from '../WatchedQuery.js'; -import { - AbstractQueryProcessor, - AbstractQueryProcessorOptions, - LinkQueryOptions, - MutableWatchedQueryState -} from './AbstractQueryProcessor.js'; /** * Represents an updated row in a differential watched query. @@ -110,219 +104,3 @@ export type DifferentialWatchedQuery = WatchedQuery< DifferentialWatchedQuerySettings, DifferentialWatchedQueryListener >; - -/** - * @internal - */ -export interface DifferentialQueryProcessorOptions extends AbstractQueryProcessorOptions< - RowType[], - DifferentialWatchedQuerySettings -> { - rowComparator?: DifferentialWatchedQueryComparator; -} - -type DataHashMap = Map; - -/** - * An empty differential result set. - * This is used as the initial state for differential incrementally watched queries. - * - * @internal - */ -export const EMPTY_DIFFERENTIAL = { - added: [], - all: [], - removed: [], - updated: [], - unchanged: [] -}; - -/** - * Default implementation of the {@link DifferentialWatchedQueryComparator} for watched queries. - * It keys items by their `id` property if available, alternatively it uses JSON stringification - * of the entire item for the key and comparison. - * - * @internal - */ -export const DEFAULT_ROW_COMPARATOR: DifferentialWatchedQueryComparator = { - keyBy: (item) => { - if (item && typeof item == 'object' && typeof item['id'] == 'string') { - return item['id']; - } - return JSON.stringify(item); - }, - compareBy: (item) => JSON.stringify(item) -}; - -/** - * Uses the PowerSync onChange event to trigger watched queries. - * Results are emitted on every change of the relevant tables. - * @internal - */ -export class DifferentialQueryProcessor - extends AbstractQueryProcessor>, DifferentialWatchedQuerySettings> - implements DifferentialWatchedQuery -{ - protected comparator: DifferentialWatchedQueryComparator; - - constructor(protected options: DifferentialQueryProcessorOptions) { - super(options); - this.comparator = options.rowComparator ?? DEFAULT_ROW_COMPARATOR; - } - - /* - * @returns If the sets are equal - */ - protected differentiate( - current: RowType[], - previousMap: DataHashMap - ): { diff: WatchedQueryDifferential; map: DataHashMap; hasChanged: boolean } { - const { keyBy, compareBy } = this.comparator; - - let hasChanged = false; - const currentMap = new Map(); - const removedTracker = new Set(previousMap.keys()); - - // Allow mutating to populate the data temporarily. - const diff = { - all: [] as RowType[], - added: [] as RowType[], - removed: [] as RowType[], - updated: [] as WatchedQueryRowDifferential[], - unchanged: [] as RowType[] - }; - - /** - * Looping over the current result set array is important to preserve - * the ordering of the result set. - * We can replace items in the current array with previous object references if they are equal. - */ - for (const item of current) { - const key = keyBy(item); - const hash = compareBy(item); - currentMap.set(key, { hash, item }); - - const previousItem = previousMap.get(key); - if (!previousItem) { - // New item - hasChanged = true; - diff.added.push(item); - diff.all.push(item); - } else { - // Existing item - if (hash == previousItem.hash) { - diff.unchanged.push(previousItem.item); - // Use the previous object reference - diff.all.push(previousItem.item); - // update the map to preserve the reference - currentMap.set(key, previousItem); - } else { - hasChanged = true; - diff.updated.push({ current: item, previous: previousItem.item }); - // Use the new reference - diff.all.push(item); - } - } - // The item is present, we don't consider it removed - removedTracker.delete(key); - } - - diff.removed = Array.from(removedTracker).map((key) => previousMap.get(key)!.item); - hasChanged = hasChanged || diff.removed.length > 0; - - return { - diff, - hasChanged, - map: currentMap - }; - } - - protected async linkQuery(options: LinkQueryOptions>): Promise { - const { db, watchOptions } = this.options; - const { abortSignal } = options; - - const compiledQuery = watchOptions.query.compile(); - const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[], { - tables: options.settings.triggerOnTables - }); - - let currentMap: DataHashMap = new Map(); - - // populate the currentMap from the placeholder data - this.state.data.forEach((item) => { - currentMap.set(this.comparator.keyBy(item), { - hash: this.comparator.compareBy(item), - item - }); - }); - - db.onChangeWithCallback( - { - onChange: async () => { - if (this.closed || abortSignal.aborted) { - return; - } - // This fires for each change of the relevant tables - try { - if (this.reportFetching && !this.state.isFetching) { - await this.updateState({ isFetching: true }); - } - - const partialStateUpdate: Partial> = {}; - - // Always run the query if an underlying table has changed - const result = await watchOptions.query.execute({ - sql: compiledQuery.sql, - // Allows casting from ReadOnlyArray[unknown] to Array - // This allows simpler compatibility with PowerSync queries - parameters: [...compiledQuery.parameters], - db: this.options.db - }); - - if (abortSignal.aborted) { - return; - } - - if (this.reportFetching) { - partialStateUpdate.isFetching = false; - } - - if (this.state.isLoading) { - partialStateUpdate.isLoading = false; - } - - const { diff, hasChanged, map } = this.differentiate(result, currentMap); - // Update for future comparisons - currentMap = map; - - if (hasChanged) { - await this.iterateAsyncListenersWithError((l) => l.onDiff?.(diff)); - Object.assign(partialStateUpdate, { - data: diff.all - }); - } - - if (this.state.error) { - partialStateUpdate.error = null; - } - - if (Object.keys(partialStateUpdate).length > 0) { - await this.updateState(partialStateUpdate); - } - } catch (error: any) { - await this.updateState({ error }); - } - }, - onError: async (error) => { - await this.updateState({ error }); - } - }, - { - signal: abortSignal, - tables, - throttleMs: watchOptions.throttleMs, - triggerImmediate: true // used to emit the initial state - } - ); - } -} diff --git a/packages/common/src/client/watched/processors/OnChangeQueryProcessor.ts b/packages/common/src/client/watched/processors/OnChangeQueryProcessor.ts index 5e6f2b500..4454bd1a4 100644 --- a/packages/common/src/client/watched/processors/OnChangeQueryProcessor.ts +++ b/packages/common/src/client/watched/processors/OnChangeQueryProcessor.ts @@ -1,11 +1,4 @@ import { WatchCompatibleQuery, WatchedQuery, WatchedQueryOptions } from '../WatchedQuery.js'; -import { - AbstractQueryProcessor, - AbstractQueryProcessorOptions, - LinkQueryOptions, - MutableWatchedQueryState -} from './AbstractQueryProcessor.js'; -import { WatchedQueryComparator } from './comparators.js'; /** * Settings for {@link WatchedQuery} instances created via {@link Query#watch}. @@ -22,107 +15,3 @@ export interface WatchedQuerySettings extends WatchedQueryOptions { * @public */ export type StandardWatchedQuery = WatchedQuery>; - -/** - * @internal - */ -export interface OnChangeQueryProcessorOptions extends AbstractQueryProcessorOptions< - Data, - WatchedQuerySettings -> { - comparator?: WatchedQueryComparator; -} - -/** - * Uses the PowerSync onChange event to trigger watched queries. - * Results are emitted on every change of the relevant tables. - * @internal - */ -export class OnChangeQueryProcessor extends AbstractQueryProcessor> { - constructor(protected options: OnChangeQueryProcessorOptions) { - super(options); - } - - /** - * @returns If the sets are equal - */ - protected checkEquality(current: Data, previous: Data): boolean { - // Use the provided comparator if available. Assume values are unique if not available. - return this.options.comparator?.checkEquality?.(current, previous) ?? false; - } - - protected async linkQuery(options: LinkQueryOptions): Promise { - const { db, watchOptions } = this.options; - const { abortSignal } = options; - - const compiledQuery = watchOptions.query.compile(); - const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[], { - tables: options.settings.triggerOnTables - }); - - db.onChangeWithCallback( - { - onChange: async () => { - if (this.closed || abortSignal.aborted) { - return; - } - // This fires for each change of the relevant tables - try { - if (this.reportFetching && !this.state.isFetching) { - await this.updateState({ isFetching: true }); - } - - const partialStateUpdate: Partial> & { data?: Data } = {}; - - // Always run the query if an underlying table has changed - const result = await watchOptions.query.execute({ - sql: compiledQuery.sql, - // Allows casting from ReadOnlyArray[unknown] to Array - // This allows simpler compatibility with PowerSync queries - parameters: [...compiledQuery.parameters], - db: this.options.db - }); - - if (abortSignal.aborted) { - return; - } - - if (this.reportFetching) { - partialStateUpdate.isFetching = false; - } - - if (this.state.isLoading) { - partialStateUpdate.isLoading = false; - } - - // Check if the result has changed - if (!this.checkEquality(result, this.state.data)) { - Object.assign(partialStateUpdate, { - data: result - }); - } - - if (this.state.error) { - partialStateUpdate.error = null; - } - - if (Object.keys(partialStateUpdate).length > 0) { - await this.updateState(partialStateUpdate); - } - } catch (error: any) { - await this.updateState({ error }); - } - }, - onError: async (error) => { - await this.updateState({ error }); - } - }, - { - signal: abortSignal, - tables, - throttleMs: watchOptions.throttleMs, - triggerImmediate: true // used to emit the initial state - } - ); - } -} diff --git a/packages/common/src/db/crud/SyncProgress.ts b/packages/common/src/db/crud/SyncProgress.ts index 2d266a534..3ed71a75b 100644 --- a/packages/common/src/db/crud/SyncProgress.ts +++ b/packages/common/src/db/crud/SyncProgress.ts @@ -1,15 +1,5 @@ -import type { BucketProgress } from '../../client/sync/stream/core-instruction.js'; import type { SyncStatus } from './SyncStatus.js'; -// (bucket, progress) pairs -/** @internal */ -export type InternalProgressInformation = Record; - -/** - * @internal The priority used by the core extension to indicate that a full sync was completed. - */ -export const FULL_SYNC_PRIORITY = 2147483647; - /** * Information about a progressing download made by the PowerSync SDK. * @@ -64,42 +54,12 @@ export interface ProgressWithOperations { * * @public */ -export class SyncProgress implements ProgressWithOperations { - totalOperations: number; - downloadedOperations: number; - downloadedFraction: number; - - constructor(protected internal: InternalProgressInformation) { - const untilCompletion = this.untilPriority(FULL_SYNC_PRIORITY); - - this.totalOperations = untilCompletion.totalOperations; - this.downloadedOperations = untilCompletion.downloadedOperations; - this.downloadedFraction = untilCompletion.downloadedFraction; - } - +export interface SyncProgress extends ProgressWithOperations { /** * Returns download progress towards all data up until the specified priority being received. * * The returned {@link ProgressWithOperations} tracks the target amount of operations that need * to be downloaded in total and how many of them have already been received. */ - untilPriority(priority: number): ProgressWithOperations { - let total = 0; - let downloaded = 0; - - for (const progress of Object.values(this.internal)) { - // Include higher-priority buckets, which are represented by lower numbers. - if (progress.priority <= priority) { - downloaded += progress.since_last; - total += progress.target_count - progress.at_last; - } - } - - let progress = total == 0 ? 0.0 : downloaded / total; - return { - totalOperations: total, - downloadedOperations: downloaded, - downloadedFraction: progress - }; - } + untilPriority(priority: number): ProgressWithOperations; } diff --git a/packages/common/src/db/crud/SyncStatus.ts b/packages/common/src/db/crud/SyncStatus.ts index 9bd2a2ebb..8cad9271a 100644 --- a/packages/common/src/db/crud/SyncStatus.ts +++ b/packages/common/src/db/crud/SyncStatus.ts @@ -1,12 +1,18 @@ -import { CoreStreamSubscription } from '../../client/sync/stream/core-instruction.js'; import { SyncStreamDescription, SyncSubscriptionDescription } from '../../client/sync/sync-streams.js'; -import { InternalProgressInformation, ProgressWithOperations, SyncProgress } from './SyncProgress.js'; +import { ProgressWithOperations, SyncProgress } from './SyncProgress.js'; /** * @public */ export type SyncDataFlowStatus = Partial<{ + /** + * true if actively downloading changes. + * This is only true when {@link connected} is also true. + */ downloading: boolean; + /** + * true if uploading changes. + */ uploading: boolean; /** * Error during downloading (including connecting). @@ -19,16 +25,6 @@ export type SyncDataFlowStatus = Partial<{ * Cleared on the next successful upload. */ uploadError?: Error; - /** - * Internal information about how far we are downloading operations in buckets. - * - * @internal Please use the {@link SyncStatus#downloadProgress} property to track sync progress. - */ - downloadProgress: InternalProgressInformation | null; - /** - * @internal - */ - internalStreamSubscriptions: CoreStreamSubscription[] | null; }>; /** @@ -40,41 +36,23 @@ export interface SyncPriorityStatus { hasSynced?: boolean; } -/** - * @internal - */ -export type SyncStatusOptions = { - connected?: boolean; - connecting?: boolean; - dataFlow?: SyncDataFlowStatus; - lastSyncedAt?: Date; - hasSynced?: boolean; - priorityStatusEntries?: SyncPriorityStatus[]; -}; - /** * @public */ -export class SyncStatus { - constructor(protected options: SyncStatusOptions) {} - +export interface SyncStatus { /** * Indicates if the client is currently connected to the PowerSync service. * * @returns True if connected, false otherwise. Defaults to false if not specified. */ - get connected(): boolean { - return this.options.connected ?? false; - } + get connected(): boolean; /** * Indicates if the client is in the process of establishing a connection to the PowerSync service. * * @returns True if connecting, false otherwise. Defaults to false if not specified. */ - get connecting(): boolean { - return this.options.connecting ?? false; - } + get connecting(): boolean; /** * Time that a last sync has fully completed, if any. @@ -82,9 +60,7 @@ export class SyncStatus { * * @returns The timestamp of the last successful sync, or undefined if no sync has completed. */ - get lastSyncedAt(): Date | undefined { - return this.options.lastSyncedAt; - } + get lastSyncedAt(): Date | undefined; /** * Indicates whether there has been at least one full sync completed since initialization. @@ -92,9 +68,7 @@ export class SyncStatus { * @returns True if at least one sync has completed, false if no sync has completed, * or undefined when the state is still being loaded from the database. */ - get hasSynced(): boolean | undefined { - return this.options.hasSynced; - } + get hasSynced(): boolean | undefined; /** * Provides the current data flow status regarding uploads and downloads. @@ -104,21 +78,7 @@ export class SyncStatus { * - uploading: True if actively uploading changes * Defaults to `{downloading: false, uploading: false}` if not specified. */ - get dataFlowStatus(): SyncDataFlowStatus { - return ( - this.options.dataFlow ?? { - /** - * true if actively downloading changes. - * This is only true when {@link connected} is also true. - */ - downloading: false, - /** - * true if uploading changes. - */ - uploading: false - } - ); - } + get dataFlowStatus(): SyncDataFlowStatus; /** * All sync streams currently being tracked in teh database. @@ -126,21 +86,12 @@ export class SyncStatus { * This returns null when the database is currently being opened and we don't have reliable information about all * included streams yet. */ - get syncStreams(): SyncStreamStatus[] | undefined { - return this.options.dataFlow?.internalStreamSubscriptions?.map((core) => new SyncStreamStatusView(this, core)); - } + get syncStreams(): SyncStreamStatus[] | undefined; /** * If the `stream` appears in {@link SyncStatus.syncStreams}, returns the current status for that stream. */ - forStream(stream: SyncStreamDescription): SyncStreamStatus | undefined { - const asJson = JSON.stringify(stream.parameters); - const raw = this.options.dataFlow?.internalStreamSubscriptions?.find( - (r) => r.name == stream.name && asJson == JSON.stringify(r.parameters) - ); - - return raw && new SyncStreamStatusView(this, raw); - } + forStream(stream: SyncStreamDescription): SyncStreamStatus | undefined; /** * Provides sync status information for all bucket priorities, sorted by priority (highest first). @@ -148,9 +99,7 @@ export class SyncStatus { * @returns An array of status entries for different sync priority levels, * sorted with highest priorities (lower numbers) first. */ - get priorityStatusEntries(): SyncPriorityStatus[] { - return (this.options.priorityStatusEntries ?? []).slice().sort(SyncStatus.comparePriorities); - } + get priorityStatusEntries(): SyncPriorityStatus[] | undefined; /** * A realtime progress report on how many operations have been downloaded and @@ -158,14 +107,7 @@ export class SyncStatus { * * This field is only set when {@link SyncDataFlowStatus#downloading} is also true. */ - get downloadProgress(): SyncProgress | null { - const internalProgress = this.options.dataFlow?.downloadProgress; - if (internalProgress == null) { - return null; - } - - return new SyncProgress(internalProgress); - } + get downloadProgress(): SyncProgress | null; /** * Reports the sync status (a pair of {@link SyncStatus#hasSynced} and {@link SyncStatus#lastSyncedAt} fields) @@ -186,22 +128,7 @@ export class SyncStatus { * @param priority - The bucket priority for which the status should be reported * @returns Status information for the requested priority level or the next higher level with available status */ - statusForPriority(priority: number): SyncPriorityStatus { - // priorityStatusEntries are sorted by ascending priorities (so higher numbers to lower numbers). - for (const known of this.priorityStatusEntries) { - // We look for the first entry that doesn't have a higher priority. - if (known.priority >= priority) { - return known; - } - } - - // If we have a complete sync, that necessarily includes all priorities. - return { - priority, - lastSyncedAt: this.lastSyncedAt, - hasSynced: this.hasSynced - }; - } + statusForPriority(priority: number): SyncPriorityStatus | undefined; /** * Compares this SyncStatus instance with another to determine if they are equal. @@ -210,24 +137,7 @@ export class SyncStatus { * @param status - The SyncStatus instance to compare against * @returns True if the instances are considered equal, false otherwise */ - isEqual(status: SyncStatus) { - /** - * By default Error object are serialized to an empty object. - * This replaces Errors with more useful information before serialization. - */ - const replacer = (_: string, value: any) => { - if (value instanceof Error) { - return { - name: value.name, - message: value.message, - stack: value.stack - }; - } - return value; - }; - - return JSON.stringify(this.options, replacer) == JSON.stringify(status.options, replacer); - } + isEqual(status: SyncStatus): boolean; /** * Creates a human-readable string representation of the current sync status. @@ -235,49 +145,7 @@ export class SyncStatus { * * @returns A string representation of the sync status */ - getMessage() { - const dataFlow = this.dataFlowStatus; - return `SyncStatus`; - } - - /** - * Serializes the SyncStatus instance to a plain object. - * - * @returns A plain object representation of the sync status - */ - toJSON(): SyncStatusOptions { - return { - connected: this.connected, - connecting: this.connecting, - dataFlow: { - ...this.dataFlowStatus, - uploadError: this.serializeError(this.dataFlowStatus.uploadError), - downloadError: this.serializeError(this.dataFlowStatus.downloadError) - }, - lastSyncedAt: this.lastSyncedAt, - hasSynced: this.hasSynced, - priorityStatusEntries: this.priorityStatusEntries - }; - } - - /** - * Not all errors are serializable over a MessagePort. E.g. some `DomExceptions` fail to be passed across workers. - * This explicitly serializes errors in the SyncStatus. - */ - protected serializeError(error?: Error) { - if (typeof error == 'undefined') { - return undefined; - } - return { - name: error.name, - message: error.message, - stack: error.stack - }; - } - - private static comparePriorities(a: SyncPriorityStatus, b: SyncPriorityStatus) { - return b.priority - a.priority; // Reverse because higher priorities have lower numbers - } + getMessage(): string; } /** @@ -290,39 +158,3 @@ export interface SyncStreamStatus { subscription: SyncSubscriptionDescription; priority: number | null; } - -class SyncStreamStatusView implements SyncStreamStatus { - subscription: SyncSubscriptionDescription; - - constructor( - private status: SyncStatus, - private core: CoreStreamSubscription - ) { - this.subscription = { - name: core.name, - parameters: core.parameters, - active: core.active, - isDefault: core.is_default, - hasExplicitSubscription: core.has_explicit_subscription, - expiresAt: core.expires_at != null ? new Date(core.expires_at * 1000) : null, - hasSynced: core.last_synced_at != null, - lastSyncedAt: core.last_synced_at != null ? new Date(core.last_synced_at * 1000) : null - }; - } - - get progress() { - if (this.status.dataFlowStatus.downloadProgress == null) { - // Don't make download progress public if we're not currently downloading. - return null; - } - - const { total, downloaded } = this.core.progress; - const progress = total == 0 ? 0.0 : downloaded / total; - - return { totalOperations: total, downloadedOperations: downloaded, downloadedFraction: progress }; - } - - get priority() { - return this.core.priority; - } -} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index b3c01c10d..b5e92e6ab 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -8,20 +8,14 @@ export * from './attachments/Schema.js'; export * from './attachments/SyncingService.js'; export * from './attachments/WatchedAttachmentItem.js'; -export * from './client/AbstractPowerSyncDatabase.js'; +export * from './client/CommonPowerSyncDatabase.js'; export { compilableQueryWatch, CompilableQueryWatchHandler } from './client/compilableQueryWatch.js'; export * from './client/connection/PowerSyncBackendConnector.js'; export * from './client/connection/PowerSyncCredentials.js'; -export { MAX_OP_ID } from './client/constants.js'; -export { runOnSchemaChange } from './client/runOnSchemaChange.js'; export * from './client/SQLOpenFactory.js'; -export * from './client/sync/bucket/BucketStorageAdapter.js'; export * from './client/sync/bucket/CrudBatch.js'; export { CrudEntry, OpId, UpdateType } from './client/sync/bucket/CrudEntry.js'; export * from './client/sync/bucket/CrudTransaction.js'; -export * from './client/sync/bucket/SqliteBucketStorage.js'; -export * from './client/sync/stream/AbstractRemote.js'; -export * from './client/sync/stream/AbstractStreamingSyncImplementation.js'; export * from './client/sync/stream/JsonValue.js'; export * from './client/sync/sync-streams.js'; export { @@ -33,7 +27,6 @@ export { resolveSyncOptions } from './client/sync/options.js'; -export * from './client/ConnectionManager.js'; export * from './db/ConnectionClosedError.js'; export { ProgressWithOperations, SyncProgress } from './db/crud/SyncProgress.js'; export * from './db/crud/SyncStatus.js'; @@ -48,23 +41,17 @@ export * from './db/schema/Table.js'; export * from './db/schema/TableV2.js'; export * from './client/Query.js'; -export { MEMORY_TRIGGER_CLAIM_MANAGER } from './client/triggers/MemoryTriggerClaimManager.js'; export * from './client/triggers/sanitizeSQL.js'; export * from './client/triggers/TriggerManager.js'; -export { TriggerManagerImpl } from './client/triggers/TriggerManagerImpl.js'; export * from './client/watched/GetAllQuery.js'; -export * from './client/watched/processors/AbstractQueryProcessor.js'; export * from './client/watched/processors/comparators.js'; export * from './client/watched/processors/DifferentialQueryProcessor.js'; export * from './client/watched/processors/OnChangeQueryProcessor.js'; export * from './client/watched/WatchedQuery.js'; -export * from './utils/AbortOperation.js'; export * from './utils/BaseObserver.js'; -export * from './utils/ControlledExecutor.js'; +export * from './utils/MetaBaseObserver.js'; export * from './utils/Logger.js'; -export * from './utils/mutex.js'; export * from './utils/parseQuery.js'; -export type { SimpleAsyncIterator } from './utils/stream_transform.js'; export * from './types/types.js'; diff --git a/packages/common/src/utils/BaseObserver.ts b/packages/common/src/utils/BaseObserver.ts index ed5500b85..548cadc24 100644 --- a/packages/common/src/utils/BaseObserver.ts +++ b/packages/common/src/utils/BaseObserver.ts @@ -16,38 +16,3 @@ export type BaseListener = Record any) | undefined>; export interface BaseObserverInterface { registerListener(listener: Partial): () => void; } - -/** - * @internal - */ -export class BaseObserver implements BaseObserverInterface { - protected listeners = new Set>(); - - constructor() {} - - dispose(): void { - this.listeners.clear(); - } - - /** - * Register a listener for updates to the PowerSync client. - */ - registerListener(listener: Partial): () => void { - this.listeners.add(listener); - return () => { - this.listeners.delete(listener); - }; - } - - iterateListeners(cb: (listener: Partial) => any) { - for (const listener of this.listeners) { - cb(listener); - } - } - - async iterateAsyncListeners(cb: (listener: Partial) => Promise) { - for (let i of Array.from(this.listeners.values())) { - await cb(i); - } - } -} diff --git a/packages/common/src/utils/MetaBaseObserver.ts b/packages/common/src/utils/MetaBaseObserver.ts index 73bb038f8..3b5a71fbb 100644 --- a/packages/common/src/utils/MetaBaseObserver.ts +++ b/packages/common/src/utils/MetaBaseObserver.ts @@ -1,4 +1,4 @@ -import { BaseListener, BaseObserver, BaseObserverInterface } from './BaseObserver.js'; +import { BaseListener, BaseObserverInterface } from './BaseObserver.js'; /** * Represents the counts of listeners for each event type in a BaseListener. @@ -14,68 +14,12 @@ export interface MetaListener extends BaseL listenersChanged?: (counts: ListenerCounts) => void; } -export interface ListenerMetaManager - extends BaseObserverInterface> { +export interface ListenerMetaManager extends BaseObserverInterface< + MetaListener +> { counts: ListenerCounts; } export interface MetaBaseObserverInterface extends BaseObserverInterface { listenerMeta: ListenerMetaManager; } - -/** - * A BaseObserver that tracks the counts of listeners for each event type. - */ -export class MetaBaseObserver - extends BaseObserver - implements MetaBaseObserverInterface -{ - protected get listenerCounts(): ListenerCounts { - const counts = {} as Partial>; - let total = 0; - for (const listener of this.listeners) { - for (const key in listener) { - if (listener[key]) { - counts[key] = (counts[key] ?? 0) + 1; - total++; - } - } - } - return { - ...counts, - total - }; - } - - get listenerMeta(): ListenerMetaManager { - return { - counts: this.listenerCounts, - // Allows registering a meta listener that will be notified of changes in listener counts - registerListener: (listener: Partial>) => { - return this.metaListener.registerListener(listener); - } - }; - } - - protected metaListener: BaseObserver>; - - constructor() { - super(); - this.metaListener = new BaseObserver>(); - } - - registerListener(listener: Partial): () => void { - const dispose = super.registerListener(listener); - const updatedCount = this.listenerCounts; - this.metaListener.iterateListeners((l) => { - l.listenersChanged?.(updatedCount); - }); - return () => { - dispose(); - const updatedCount = this.listenerCounts; - this.metaListener.iterateListeners((l) => { - l.listenersChanged?.(updatedCount); - }); - }; - } -} diff --git a/packages/common/src/utils/mutex.ts b/packages/common/src/utils/mutex.ts index 503a68659..18251fcc9 100644 --- a/packages/common/src/utils/mutex.ts +++ b/packages/common/src/utils/mutex.ts @@ -1,204 +1,7 @@ -import { Queue } from './queue.js'; - /** - * @internal + * @internal This is implemented in `@powersync/shared-internals`, but we need it in the attachment service + * implementation. */ -export type UnlockFn = () => void; - -/** - * An asynchronous semaphore implementation with associated items per lease. - * - * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API. - */ -export class Semaphore { - // Available items that are not currently assigned to a waiter. - private readonly available: Queue; - - readonly size: number; - // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing - // aborted waiters from the middle of the list efficiently. - private firstWaiter?: SemaphoreWaitNode; - private lastWaiter?: SemaphoreWaitNode; - - constructor(elements: Iterable) { - this.available = new Queue(elements); - this.size = this.available.length; - } - - private addWaiter(requestedItems: number, onAcquire: () => void): SemaphoreWaitNode { - const node: SemaphoreWaitNode = { - isActive: true, - acquiredItems: [], - remainingItems: requestedItems, - onAcquire, - prev: this.lastWaiter - }; - if (this.lastWaiter) { - this.lastWaiter.next = node; - this.lastWaiter = node; - } else { - // First waiter - this.lastWaiter = this.firstWaiter = node; - } - - return node; - } - - private deactivateWaiter(waiter: SemaphoreWaitNode) { - const { prev, next } = waiter; - waiter.isActive = false; - - if (prev) prev.next = next; - if (next) next.prev = prev; - if (waiter == this.firstWaiter) this.firstWaiter = next; - if (waiter == this.lastWaiter) this.lastWaiter = prev; - } - - private requestPermits(amount: number, abort?: AbortSignal): Promise<{ items: T[]; release: UnlockFn }> { - if (amount <= 0 || amount > this.size) { - throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`); - } - - return new Promise((resolve, reject) => { - function rejectAborted() { - reject(abort?.reason ?? new Error('Semaphore acquire aborted')); - } - if (abort?.aborted) { - return rejectAborted(); - } - - let waiter: SemaphoreWaitNode; - - const markCompleted = () => { - const items = waiter.acquiredItems; - waiter.acquiredItems = []; // Avoid releasing items twice. - - for (const element of items) { - // Give to next waiter, if possible. - const nextWaiter = this.firstWaiter; - if (nextWaiter) { - nextWaiter.acquiredItems.push(element); - nextWaiter.remainingItems--; - if (nextWaiter.remainingItems == 0) { - nextWaiter.onAcquire(); - } - } else { - // No pending waiter, return lease into pool. - this.available.addLast(element); - } - } - }; - - const onAbort = () => { - abort?.removeEventListener('abort', onAbort); - - if (waiter.isActive) { - this.deactivateWaiter(waiter); - rejectAborted(); - } - }; - - const resolvePromise = () => { - this.deactivateWaiter(waiter); - abort?.removeEventListener('abort', onAbort); - - const items = waiter.acquiredItems; - resolve({ items, release: markCompleted }); - }; - - waiter = this.addWaiter(amount, resolvePromise); - - // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is - // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter). - while (!this.available.isEmpty && waiter.remainingItems > 0) { - waiter.acquiredItems.push(this.available.removeFirst()); - waiter.remainingItems--; - } - - if (waiter.remainingItems == 0) { - return resolvePromise(); - } - - abort?.addEventListener('abort', onAbort); - }); - } - - /** - * Requests a single item from the pool. - * - * The returned `release` callback must be invoked to return the item into the pool. - */ - async requestOne(abort?: AbortSignal): Promise<{ item: T; release: UnlockFn }> { - const { items, release } = await this.requestPermits(1, abort); - return { release, item: items[0] }; - } - - /** - * Requests access to all items from the pool. - * - * The returned `release` callback must be invoked to return items into the pool. - */ - requestAll(abort?: AbortSignal): Promise<{ items: T[]; release: UnlockFn }> { - return this.requestPermits(this.size, abort); - } -} - -interface SemaphoreWaitNode { - /** - * Whether the waiter is currently active (not aborted and not fullfilled). - */ - isActive: boolean; - acquiredItems: T[]; - remainingItems: number; - onAcquire: () => void; - prev?: SemaphoreWaitNode; - next?: SemaphoreWaitNode; -} - -/** - * An asynchronous mutex implementation. - * - * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API. - */ -export class Mutex { - private inner = new Semaphore([null]); - - async acquire(abort?: AbortSignal): Promise { - const { release } = await this.inner.requestOne(abort); - return release; - } - - async runExclusive(fn: () => PromiseLike | T, abort?: AbortSignal): Promise { - const returnMutex = await this.acquire(abort); - - try { - return await fn(); - } finally { - returnMutex(); - } - } -} - -/** - * Creates a signal aborting after the set timeout. - * - * @internal - */ -export function timeoutSignal(timeout: number): AbortSignal; - -/** - * @internal - */ -export function timeoutSignal(timeout?: number): AbortSignal | undefined; - -/** - * @internal - */ -export function timeoutSignal(timeout?: number): AbortSignal | undefined { - if (timeout == null) return; - if ('timeout' in AbortSignal) return AbortSignal.timeout(timeout); - - const controller = new AbortController(); - setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout); - return controller.signal; +export interface Mutex { + runExclusive(fn: () => PromiseLike | T, abort?: AbortSignal): Promise; } diff --git a/packages/node/package.json b/packages/node/package.json index 2738aa0c2..6b6198874 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -57,6 +57,7 @@ }, "dependencies": { "@powersync/common": "workspace:*", + "@powersync/shared-internals": "workspace:*", "comlink": "catalog:", "undici": "^7.11.0" }, diff --git a/packages/node/src/db/PowerSyncDatabase.ts b/packages/node/src/db/PowerSyncDatabase.ts index b2b8720c3..5b6535f72 100644 --- a/packages/node/src/db/PowerSyncDatabase.ts +++ b/packages/node/src/db/PowerSyncDatabase.ts @@ -1,17 +1,20 @@ import { - AbstractPowerSyncDatabase, - AbstractRemoteOptions, - AbstractStreamingSyncImplementation, BasePowerSyncDatabaseOptions, - BucketStorageAdapter, - CreateSyncImplementationOptions, DatabaseSource, DBAdapter, openDatabase, PowerSyncBackendConnector, - SqliteBucketStorage + PowerSyncDatabaseConstructor } from '@powersync/common'; +import { + AbstractPowerSyncDatabase, + AbstractStreamingSyncImplementation, + BucketStorageAdapter, + CreateSyncImplementationOptions, + SqliteBucketStorage +} from '@powersync/shared-internals'; + import { NodeRemote, NodeRemoteOptions } from '../sync/stream/NodeRemote.js'; import { NodeStreamingSyncImplementation } from '../sync/stream/NodeStreamingSyncImplementation.js'; @@ -26,21 +29,7 @@ export type NodePowerSyncDatabaseOptions = BasePowerSyncDatabaseOptions & remoteOptions?: Partial; }; -/** - * A PowerSync database which provides SQLite functionality - * which is automatically synced. - * - * @example - * ```typescript - * export const db = new PowerSyncDatabase({ - * schema: AppSchema, - * database: { - * dbFilename: 'example.db' - * } - * }); - * ``` - */ -export class PowerSyncDatabase extends AbstractPowerSyncDatabase { +class NodePowerSyncDatabase extends AbstractPowerSyncDatabase { constructor(options: NodePowerSyncDatabaseOptions) { super(options); } @@ -79,3 +68,20 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase = NodePowerSyncDatabase; diff --git a/packages/node/src/db/WorkerConnectionPool.ts b/packages/node/src/db/WorkerConnectionPool.ts index b5013088b..184ac0b44 100644 --- a/packages/node/src/db/WorkerConnectionPool.ts +++ b/packages/node/src/db/WorkerConnectionPool.ts @@ -4,7 +4,6 @@ import * as path from 'node:path'; import { Worker } from 'node:worker_threads'; import { - BaseObserver, BatchedUpdateNotification, ConnectionPool, DBAdapterDefaultMixin, @@ -12,10 +11,9 @@ import { DBLockOptions, LockContext, QueryResult, - Semaphore, - timeoutSignal, Transaction } from '@powersync/common'; +import { BaseObserver, Semaphore, timeoutSignal } from '@powersync/shared-internals'; import { Remote } from 'comlink'; import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js'; import { RemoteConnection } from './RemoteConnection.js'; diff --git a/packages/node/src/sync/stream/NodeRemote.ts b/packages/node/src/sync/stream/NodeRemote.ts index 4b9f8074c..72c2eea75 100644 --- a/packages/node/src/sync/stream/NodeRemote.ts +++ b/packages/node/src/sync/stream/NodeRemote.ts @@ -5,10 +5,10 @@ import { AbstractRemoteOptions, FetchImplementation, FetchImplementationProvider, - PowerSyncLogger, RemoteConnector -} from '@powersync/common'; +} from '@powersync/shared-internals'; import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, ProxyAgent, WebSocket as UndiciWebSocket } from 'undici'; +import { PowerSyncLogger } from '@powersync/common'; export const STREAMING_POST_TIMEOUT_MS = 30_000; diff --git a/packages/node/src/sync/stream/NodeStreamingSyncImplementation.ts b/packages/node/src/sync/stream/NodeStreamingSyncImplementation.ts index 49d5ec83f..8d8e4ac82 100644 --- a/packages/node/src/sync/stream/NodeStreamingSyncImplementation.ts +++ b/packages/node/src/sync/stream/NodeStreamingSyncImplementation.ts @@ -4,7 +4,7 @@ import { LockOptions, LockType, Mutex -} from '@powersync/common'; +} from '@powersync/shared-internals'; /** * Global locks which prevent multiple instances from syncing diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index b881f3493..44fe94d7a 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -18,6 +18,9 @@ "references": [ { "path": "../common" + }, + { + "path": "../shared-internals" } ] } diff --git a/packages/powersync-op-sqlite/package.json b/packages/powersync-op-sqlite/package.json index 81d05b98e..61eab0969 100644 --- a/packages/powersync-op-sqlite/package.json +++ b/packages/powersync-op-sqlite/package.json @@ -71,7 +71,8 @@ "react-native": "*" }, "dependencies": { - "@powersync/common": "workspace:*" + "@powersync/common": "workspace:*", + "@powersync/shared-internals": "workspace:*" }, "devDependencies": { "@op-engineering/op-sqlite": "catalog:", diff --git a/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts b/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts index d956e08d0..7fe9c87f0 100644 --- a/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts +++ b/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts @@ -7,10 +7,9 @@ import { DBAdapterListener, DBLockOptions, QueryResult, - Transaction, - timeoutSignal, - Semaphore + Transaction } from '@powersync/common'; +import { timeoutSignal, Semaphore } from '@powersync/shared-internals'; import { Platform } from 'react-native'; import { OPSQLiteConnection } from './OPSQLiteConnection'; import { SqliteOptions } from './SqliteOptions'; diff --git a/packages/powersync-op-sqlite/tsconfig.json b/packages/powersync-op-sqlite/tsconfig.json index d4901a9cb..8074dcd30 100644 --- a/packages/powersync-op-sqlite/tsconfig.json +++ b/packages/powersync-op-sqlite/tsconfig.json @@ -16,6 +16,9 @@ "references": [ { "path": "../common" + }, + { + "path": "../shared-internals" } ] } diff --git a/packages/react-native/package.json b/packages/react-native/package.json index c9b8a7d8d..61ddd56f3 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -45,7 +45,8 @@ }, "dependencies": { "@powersync/common": "workspace:*", - "@powersync/react": "workspace:*" + "@powersync/react": "workspace:*", + "@powersync/shared-internals": "workspace:*" }, "devDependencies": { "@craftzdog/react-native-buffer": "^6.0.5", diff --git a/packages/react-native/src/sync/stream/ReactNativeStreamingSyncImplementation.ts b/packages/react-native/src/sync/stream/ReactNativeStreamingSyncImplementation.ts index 77fab0d27..babbdbdba 100644 --- a/packages/react-native/src/sync/stream/ReactNativeStreamingSyncImplementation.ts +++ b/packages/react-native/src/sync/stream/ReactNativeStreamingSyncImplementation.ts @@ -2,9 +2,9 @@ import { AbstractStreamingSyncImplementation, AbstractStreamingSyncImplementationOptions, LockOptions, - LockType, - Mutex + LockType } from '@powersync/common'; +import { Mutex } from '@powersync/shared-internals'; /** * Global locks which prevent multiple instances from syncing diff --git a/packages/react-native/tsconfig.json b/packages/react-native/tsconfig.json index 819023b01..b6dc5579f 100644 --- a/packages/react-native/tsconfig.json +++ b/packages/react-native/tsconfig.json @@ -12,6 +12,9 @@ }, { "path": "../react" + }, + { + "path": "../shared-internals" } ], "include": ["src/**/*"] diff --git a/packages/shared-internals/README.md b/packages/shared-internals/README.md new file mode 100644 index 000000000..73b7299e4 --- /dev/null +++ b/packages/shared-internals/README.md @@ -0,0 +1,5 @@ +# PowerSync shared internals + +This package provides internal definitions shared across PowerSync SDKs. + +This package is not part of any public PowerSync API, and should not be imported in user code. diff --git a/packages/common/legacy/sync_protocol.d.ts b/packages/shared-internals/legacy/sync_protocol.d.ts similarity index 100% rename from packages/common/legacy/sync_protocol.d.ts rename to packages/shared-internals/legacy/sync_protocol.d.ts diff --git a/packages/shared-internals/package.json b/packages/shared-internals/package.json new file mode 100644 index 000000000..844a57c21 --- /dev/null +++ b/packages/shared-internals/package.json @@ -0,0 +1,62 @@ +{ + "name": "@powersync/shared-internals", + "version": "1.0.0", + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "description": "Internal helpers used by PowerSync, not meant to be used in other contexts.", + "type": "module", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./internal/sync_protocol": { + "types": "./legacy/sync_protocol.d.ts" + } + }, + "author": "PowerSync", + "license": "Apache-2.0", + "files": [ + "lib", + "dist", + "src", + "legacy" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/powersync-ja/powersync-js.git" + }, + "bugs": { + "url": "https://github.com/powersync-ja/powersync-js/issues" + }, + "homepage": "https://docs.powersync.com", + "scripts": { + "build": "tsc -b && rollup -c rollup.config.mjs", + "build:prod": "tsc -b && rollup -c rollup.config.mjs", + "clean": "rm -rf lib dist tsconfig.tsbuildinfo", + "test": "vitest", + "test:exports": "attw --pack . --exclude-entrypoints internal/sync_protocol" + }, + "dependencies": { + "event-iterator": "^2.0.0" + }, + "devDependencies": { + "@powersync/common": "workspace:*", + "@rollup/plugin-commonjs": "catalog:", + "@rollup/plugin-inject": "catalog:", + "@rollup/plugin-json": "catalog:", + "@rollup/plugin-node-resolve": "catalog:", + "@types/node": "catalog:", + "@types/uuid": "catalog:", + "buffer": "^6.0.3", + "cross-fetch": "^4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21", + "rollup": "catalog:", + "rollup-plugin-dts": "catalog:", + "rsocket-core": "1.0.0-alpha.3", + "rsocket-websocket-client": "1.0.0-alpha.3" + } +} diff --git a/packages/common/rollup.config.mjs b/packages/shared-internals/rollup.config.mjs similarity index 100% rename from packages/common/rollup.config.mjs rename to packages/shared-internals/rollup.config.mjs diff --git a/packages/common/src/client/AbstractPowerSyncDatabase.ts b/packages/shared-internals/src/client/AbstractPowerSyncDatabase.ts similarity index 56% rename from packages/common/src/client/AbstractPowerSyncDatabase.ts rename to packages/shared-internals/src/client/AbstractPowerSyncDatabase.ts index 109fd6133..69f74d703 100644 --- a/packages/common/src/client/AbstractPowerSyncDatabase.ts +++ b/packages/shared-internals/src/client/AbstractPowerSyncDatabase.ts @@ -1,149 +1,58 @@ -import { EventIterator } from 'event-iterator'; import { + ArrayQueryDefinition, + BasePowerSyncDatabaseOptions, BatchedUpdateNotification, + CommonPowerSyncDatabase, + createConsoleLogger, + CrudBatch, + CrudEntry, + CrudTransaction, DBAdapter, + DisconnectAndClearOptions, + isBatchedUpdateNotification, LockContext, + LogLevels, + PowerSyncBackendConnector, + PowerSyncCloseOptions, + PowerSyncDBListener, + PowerSyncLogger, + Query, QueryResult, + Schema, + SQLOnChangeOptions, + SQLWatchOptions, + SyncOptions, + SyncStatus, + SyncStream, Transaction, + TriggerManager, UpdateNotification, - isBatchedUpdateNotification -} from '../db/DBAdapter.js'; -import { SyncStatus } from '../db/crud/SyncStatus.js'; -import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js'; -import { Schema } from '../db/schema/Schema.js'; -import { BaseObserver } from '../utils/BaseObserver.js'; -import { ControlledExecutor } from '../utils/ControlledExecutor.js'; -import { throttleTrailing } from '../utils/async.js'; + UploadQueueStats, + WatchCompatibleQuery, + WatchHandler, + WatchOnChangeEvent, + WatchOnChangeHandler +} from '@powersync/common'; +import { BucketStorageAdapter, PSInternalTable } from './sync/bucket/BucketStorageAdapter.js'; +import { EventIterator } from 'event-iterator'; +import { SyncStatusSnapshot } from '../db/crud/SyncStatus.js'; import { ConnectionManager, CreateSyncImplementationOptions, InternalSubscriptionAdapter } from './ConnectionManager.js'; +import { Mutex } from '../utils/mutex.js'; +import { BaseObserver } from '../utils/BaseObserver.js'; +import { TriggerManagerConfig, TriggerManagerImpl } from './triggers/TriggerManagerImpl.js'; +import { StreamingSyncImplementation } from './sync/stream/AbstractStreamingSyncImplementation.js'; +import { CoreSyncStatus } from './sync/stream/core-instruction.js'; +import { CrudEntryImpl, CrudEntryJSON } from './sync/bucket/CrudEntry.js'; +import { OnChangeQueryProcessor } from './watched/OnChangeQueryProcessor.js'; +import { throttleTrailing } from '../utils/async.js'; +import { ControlledExecutor } from '../utils/ControlledExecutor.js'; +import { DEFAULT_WATCH_THROTTLE_MS } from './watched/WatchedQuery.js'; import { CustomQuery } from './CustomQuery.js'; -import { ArrayQueryDefinition, Query } from './Query.js'; -import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js'; -import { BucketStorageAdapter, PSInternalTable } from './sync/bucket/BucketStorageAdapter.js'; -import { CrudBatch } from './sync/bucket/CrudBatch.js'; -import { CrudEntry, CrudEntryJSON } from './sync/bucket/CrudEntry.js'; -import { CrudTransaction } from './sync/bucket/CrudTransaction.js'; -import { - StreamingSyncImplementation, - StreamingSyncImplementationListener -} from './sync/stream/AbstractStreamingSyncImplementation.js'; -import { CoreSyncStatus, coreStatusToJs } from './sync/stream/core-instruction.js'; -import { SyncStream } from './sync/sync-streams.js'; import { MEMORY_TRIGGER_CLAIM_MANAGER } from './triggers/MemoryTriggerClaimManager.js'; -import { TriggerManager, TriggerManagerConfig } from './triggers/TriggerManager.js'; -import { TriggerManagerImpl } from './triggers/TriggerManagerImpl.js'; -import { DEFAULT_WATCH_THROTTLE_MS, WatchCompatibleQuery } from './watched/WatchedQuery.js'; -import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js'; -import { WatchedQueryComparator } from './watched/processors/comparators.js'; -import { Mutex } from '../utils/mutex.js'; -import { createConsoleLogger, LogLevels, PowerSyncLogger } from '../utils/Logger.js'; -import { SyncOptions } from './sync/options.js'; -import { DatabaseSource, openDatabase, SQLOpenOptions } from './SQLOpenFactory.js'; - -/** - * @public - */ -export interface DisconnectAndClearOptions { - /** When set to false, data in local-only tables is preserved. */ - clearLocal?: boolean; -} - -/** - * Options required regardless of how a PowerSync database is opened. - * - * @public - */ -export interface BasePowerSyncDatabaseOptions { - /** Schema used for the local database. */ - schema: Schema; - logger?: PowerSyncLogger; -} - -/** - * @public - */ -export type PowerSyncDatabaseOptions = BasePowerSyncDatabaseOptions & DatabaseSource; - -/** - * @public - */ -export interface SQLOnChangeOptions { - signal?: AbortSignal; - tables?: string[]; - /** The minimum interval between queries. */ - throttleMs?: number; - /** - * @deprecated All tables specified in {@link SQLOnChangeOptions.tables} will be watched, including PowerSync tables - * with prefixes. - * - * Allows for watching any SQL table - * by not removing PowerSync table name prefixes - */ - rawTableNames?: boolean; - /** - * Emits an empty result set immediately - */ - triggerImmediate?: boolean; -} - -/** - * @public - */ -export interface SQLWatchOptions extends SQLOnChangeOptions { - /** - * Optional comparator which will be used to compare the results of the query. - * The watched query will only yield results if the comparator returns false. - */ - comparator?: WatchedQueryComparator; -} - -/** - * @public - */ -export interface WatchOnChangeEvent { - changedTables: string[]; -} - -/** - * @public - */ -export interface WatchHandler { - onResult: (results: QueryResult) => void; - onError?: (error: Error) => void; -} - -/** - * @public - */ -export interface WatchOnChangeHandler { - onChange: (event: WatchOnChangeEvent) => Promise | void; - onError?: (error: Error) => void; -} - -/** - * @public - */ -export interface PowerSyncDBListener extends StreamingSyncImplementationListener { - initialized: () => void; - schemaChanged: (schema: Schema) => void; - closing: () => Promise | void; - closed: () => Promise | void; -} - -/** - * @public - */ -export interface PowerSyncCloseOptions { - /** - * Disconnect the sync stream client if connected. - * This is usually true, but can be false for Web when using - * multiple tabs and a shared sync provider. - */ - disconnect?: boolean; -} const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/; @@ -169,22 +78,16 @@ const DEFAULT_CRUD_BATCH_LIMIT = 100; */ export const DEFAULT_LOCK_TIMEOUT_MS = 120_000; // 2 mins -/** - * @public - */ export abstract class AbstractPowerSyncDatabase< Options extends BasePowerSyncDatabaseOptions = BasePowerSyncDatabaseOptions -> extends BaseObserver { - /** - * Returns true if the connection is closed. - */ +> + extends BaseObserver + implements CommonPowerSyncDatabase +{ closed: boolean; ready: boolean; - /** - * Current connection status. - */ - currentStatus: SyncStatus; + currentStatus: SyncStatusSnapshot; sdkVersion: string; @@ -242,7 +145,7 @@ export abstract class AbstractPowerSyncDatabase< this._database = this.openDBAdapter(); this.bucketStorageAdapter = this.generateBucketStorageAdapter(); this.closed = false; - this.currentStatus = new SyncStatus({}); + this.currentStatus = new SyncStatusSnapshot(null, {}); this.options = { ...options }; this._schema = schema; this.ready = false; @@ -265,11 +168,8 @@ export abstract class AbstractPowerSyncDatabase< return this.runExclusive(async () => { const sync = this.generateSyncStreamImplementation(connector, options); const onDispose = sync.registerListener({ - statusChanged: (status) => { - this.currentStatus = new SyncStatus({ - ...status.toJSON(), - hasSynced: this.currentStatus?.hasSynced || !!status.lastSyncedAt - }); + statusChanged: (status, dataFlow) => { + this.currentStatus = new SyncStatusSnapshot(status, dataFlow); this.iterateListeners((cb) => cb.statusChanged?.(this.currentStatus)); } }); @@ -293,9 +193,6 @@ export abstract class AbstractPowerSyncDatabase< }); } - /** - * Schema used for the local database. - */ get schema() { return this._schema; } @@ -342,9 +239,6 @@ export abstract class AbstractPowerSyncDatabase< protected abstract generateBucketStorageAdapter(): BucketStorageAdapter; - /** - * @returns A promise which will resolve once initialization is completed. - */ async waitForReady(): Promise { if (this.ready) { return; @@ -353,15 +247,6 @@ export abstract class AbstractPowerSyncDatabase< await this._isReadyPromise; } - /** - * Wait for the first sync operation to complete. - * - * @param request - Either an abort signal (after which the promise will complete regardless of - * whether a full sync was completed) or an object providing an abort signal and a priority target. - * When a priority target is set, the promise may complete when all buckets with the given (or higher) - * priorities have been synchronized. This can be earlier than a complete sync. - * @returns A promise which will resolve once the first full sync has completed. - */ async waitForFirstSync(request?: AbortSignal | { signal?: AbortSignal; priority?: number }): Promise { const signal = request instanceof AbortSignal ? request : request?.signal; const priority = request && 'priority' in request ? request.priority : undefined; @@ -369,14 +254,11 @@ export abstract class AbstractPowerSyncDatabase< const statusMatches = priority === undefined ? (status: SyncStatus) => status.hasSynced - : (status: SyncStatus) => status.statusForPriority(priority).hasSynced; + : (status: SyncStatus) => status.statusForPriority(priority)?.hasSynced == true; return this.waitForStatus(statusMatches, signal); } - /** - * Waits for the first sync status for which the `status` callback returns a truthy value. - */ async waitForStatus(predicate: (status: SyncStatus) => any, signal?: AbortSignal): Promise { if (predicate(this.currentStatus)) { return; @@ -408,7 +290,7 @@ export abstract class AbstractPowerSyncDatabase< * Allows for extended implementations to execute custom initialization * logic as part of the total init process */ - abstract _initialize(): Promise; + protected abstract _initialize(): Promise; /** * Entry point for executing initialization logic. @@ -454,10 +336,7 @@ export abstract class AbstractPowerSyncDatabase< const result = await this.database.get<{ r: string }>('SELECT powersync_offline_sync_status() as r'); const parsed = JSON.parse(result.r) as CoreSyncStatus; - const updatedStatus = new SyncStatus({ - ...this.currentStatus.toJSON(), - ...coreStatusToJs(parsed) - }); + const updatedStatus = new SyncStatusSnapshot(parsed, this.currentStatus.dataFlowStatus); if (!updatedStatus.isEqual(this.currentStatus)) { this.currentStatus = updatedStatus; @@ -465,11 +344,6 @@ export abstract class AbstractPowerSyncDatabase< } } - /** - * Replace the schema with a new version. This is for advanced use cases - typically the schema should just be specified once in the constructor. - * - * Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}. - */ async updateSchema(schema: Schema) { if (this.syncStreamImplementation) { throw new Error('Cannot update schema while connected'); @@ -496,10 +370,6 @@ export abstract class AbstractPowerSyncDatabase< this.iterateListeners(async (cb) => cb.schemaChanged?.(schema)); } - /** - * Wait for initialization to complete. - * While initializing is automatic, this helps to catch and report initialization errors. - */ async init() { return this.waitForReady(); } @@ -520,30 +390,14 @@ export abstract class AbstractPowerSyncDatabase< return this.runExclusiveMutex.runExclusive(callback); } - /** - * Connects to stream of events from the PowerSync instance. - */ async connect(connector: PowerSyncBackendConnector, options?: SyncOptions) { return this.connectionManager.connect(connector, options ?? {}, this.schema.toJSON()); } - /** - * Close the sync connection. - * - * Use {@link AbstractPowerSyncDatabase.connect} to connect again. - */ async disconnect() { return this.connectionManager.disconnect(); } - /** - * Disconnect and clear the database. - * Use this when logging out. - * The database can still be queried after this is called, but the tables - * would be empty. - * - * To preserve data in local-only tables, set clearLocal to false. - */ async disconnectAndClear(options = DEFAULT_DISCONNECT_CLEAR_OPTIONS) { await this.disconnect(); await this.waitForReady(); @@ -555,30 +409,13 @@ export abstract class AbstractPowerSyncDatabase< }); // The data has been deleted - reset the sync status - this.currentStatus = new SyncStatus({}); - this.iterateListeners((l) => l.statusChanged?.(this.currentStatus)); + await this.resolveOfflineSyncStatus(); } - /** - * Create a sync stream to query its status or to subscribe to it. - * - * @param name - The name of the stream to subscribe to. - * @param params - Optional parameters for the stream subscription. - * @returns A {@link SyncStream} instance that can be subscribed to. - * @experimental Sync streams are currently in alpha. - */ syncStream(name: string, params?: Record): SyncStream { return this.connectionManager.stream(this.subscriptions, name, params ?? null); } - /** - * Close the database, releasing resources. - * - * Also disconnects any active connection. - * - * Once close is called, this connection cannot be used again - a new one - * must be constructed. - */ async close(options: PowerSyncCloseOptions = DEFAULT_POWERSYNC_CLOSE_OPTIONS) { await this.waitForReady(); @@ -601,9 +438,6 @@ export abstract class AbstractPowerSyncDatabase< await this.iterateAsyncListeners(async (cb) => cb.closed?.()); } - /** - * Get upload queue size estimate and count. - */ async getUploadQueueStats(includeSize?: boolean): Promise { return this.readTransaction(async (tx) => { if (includeSize) { @@ -621,33 +455,13 @@ export abstract class AbstractPowerSyncDatabase< }); } - /** - * Get a batch of CRUD data to upload. - * - * Returns null if there is no data to upload. - * - * Use this from the {@link PowerSyncBackendConnector.uploadData} callback. - * - * Once the data have been successfully uploaded, call {@link CrudBatch.complete} before - * requesting the next batch. - * - * Use the `limit` parameter to specify the maximum number of updates to return in a single - * batch. - * - * This method does include transaction ids in the result, but does not group - * data by transaction. One batch may contain data from multiple transactions, - * and a single transaction may be split over multiple batches. - * - * @param limit - Maximum number of CRUD entries to include in the batch - * @returns A batch of CRUD operations to upload, or null if there are none - */ async getCrudBatch(limit: number = DEFAULT_CRUD_BATCH_LIMIT): Promise { const result = await this.getAll( `SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} ORDER BY id ASC LIMIT ?`, [limit + 1] ); - const all: CrudEntry[] = result.map((row) => CrudEntry.fromRow(row)) ?? []; + const all: CrudEntry[] = result.map((row) => CrudEntryImpl.fromRow(row)) ?? []; let haveMore = false; if (all.length > limit) { @@ -664,57 +478,11 @@ export abstract class AbstractPowerSyncDatabase< ); } - /** - * Get the next recorded transaction to upload. - * - * Returns null if there is no data to upload. - * - * Use this from the {@link PowerSyncBackendConnector.uploadData} callback. - * - * Once the data have been successfully uploaded, call {@link CrudTransaction.complete} before - * requesting the next transaction. - * - * Unlike {@link AbstractPowerSyncDatabase.getCrudBatch}, this only returns data from a single transaction at a time. - * All data for the transaction is loaded into memory. - * - * @returns A transaction of CRUD operations to upload, or null if there are none - */ async getNextCrudTransaction(): Promise { const iterator = this.getCrudTransactions()[Symbol.asyncIterator](); return (await iterator.next()).value; } - /** - * Returns an async iterator of completed transactions with local writes against the database. - * - * This is typically used from the {@link PowerSyncBackendConnector.uploadData} callback. Each entry emitted by the - * returned iterator is a full transaction containing all local writes made while that transaction was active. - * - * Unlike {@link AbstractPowerSyncDatabase.getNextCrudTransaction}, which always returns the oldest transaction that hasn't been - * {@link CrudTransaction.complete}d yet, this iterator can be used to receive multiple transactions. Calling - * {@link CrudTransaction.complete} will mark that and all prior transactions emitted by the iterator as completed. - * - * This can be used to upload multiple transactions in a single batch, e.g with: - * - * ```JavaScript - * let lastTransaction = null; - * let batch = []; - * - * for await (const transaction of database.getCrudTransactions()) { - * batch.push(...transaction.crud); - * lastTransaction = transaction; - * - * if (batch.length > 10) { - * break; - * } - * } - * ``` - * - * If there is no local data to upload, the async iterator complete without emitting any items. - * - * Note that iterating over async iterables requires a [polyfill](https://github.com/powersync-ja/powersync-js/tree/main/packages/react-native#babel-plugins-watched-queries) - * for React Native. - */ getCrudTransactions(): AsyncIterable { return { [Symbol.asyncIterator]: () => { @@ -737,7 +505,7 @@ SELECT * FROM crud_entries; return { done: true, value: null }; } - const items = nextTransaction.map((row) => CrudEntry.fromRow(row)); + const items = nextTransaction.map((row) => CrudEntryImpl.fromRow(row)); const last = items[items.length - 1]; const txId = last.transactionId; lastCrudItemId = last.clientId; @@ -756,13 +524,6 @@ SELECT * FROM crud_entries; }; } - /** - * Get an unique client id for this database. - * - * The id is not reset when the database is cleared, only when the database is deleted. - * - * @returns A unique identifier for the database instance - */ async getClientId(): Promise { return this.bucketStorageAdapter.getClientId(); } @@ -785,115 +546,45 @@ SELECT * FROM crud_entries; }); } - /** - * Execute a SQL write (INSERT/UPDATE/DELETE) query - * and optionally return results. - * - * When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure), - * the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements. - * Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed. - * - * @param sql - The SQL query to execute - * @param parameters - Optional array of parameters to bind to the query - * @returns The query result as an object with structured key-value pairs - */ async execute(sql: string, parameters?: any[]) { return this.writeLock((tx) => tx.execute(sql, parameters)); } - /** - * Execute a SQL write (INSERT/UPDATE/DELETE) query directly on the database without any PowerSync processing. - * This bypasses certain PowerSync abstractions and is useful for accessing the raw database results. - * - * @param sql - The SQL query to execute - * @param parameters - Optional array of parameters to bind to the query - * @returns The raw query result from the underlying database as a nested array of raw values, where each row is - * represented as an array of column values without field names. - */ async executeRaw(sql: string, parameters?: any[]) { await this.waitForReady(); return this.database.executeRaw(sql, parameters); } - /** - * Execute a write query (INSERT/UPDATE/DELETE) multiple times with each parameter set - * and optionally return results. - * This is faster than executing separately with each parameter set. - * - * @param sql - The SQL query to execute - * @param parameters - Optional 2D array of parameter sets, where each inner array is a set of parameters for one execution - * @returns The query result - */ async executeBatch(sql: string, parameters?: any[][]) { await this.waitForReady(); return this.database.executeBatch(sql, parameters); } - /** - * Execute a read-only query and return results. - * - * @param sql - The SQL query to execute - * @param parameters - Optional array of parameters to bind to the query - * @returns An array of results - */ async getAll(sql: string, parameters?: any[]): Promise { await this.waitForReady(); return this.database.getAll(sql, parameters); } - /** - * Execute a read-only query and return the first result, or null if the ResultSet is empty. - * - * @param sql - The SQL query to execute - * @param parameters - Optional array of parameters to bind to the query - * @returns The first result if found, or null if no results are returned - */ async getOptional(sql: string, parameters?: any[]): Promise { await this.waitForReady(); return this.database.getOptional(sql, parameters); } - /** - * Execute a read-only query and return the first result, error if the ResultSet is empty. - * - * @param sql - The SQL query to execute - * @param parameters - Optional array of parameters to bind to the query - * @returns The first result matching the query - * @throws Error if no rows are returned - */ async get(sql: string, parameters?: any[]): Promise { await this.waitForReady(); return this.database.get(sql, parameters); } - /** - * Takes a read lock, without starting a transaction. - * In most cases, {@link AbstractPowerSyncDatabase.readTransaction} should be used instead. - */ async readLock(callback: (db: LockContext) => Promise) { await this.waitForReady(); return this.database.readLock(callback); } - /** - * Takes a global lock, without starting a transaction. - * In most cases, {@link AbstractPowerSyncDatabase.writeTransaction} should be used instead. - */ async writeLock(callback: (db: LockContext) => Promise) { await this.waitForReady(); return this.database.writeLock(callback); } - /** - * Open a read-only transaction. - * Read transactions can run concurrently to a write transaction. - * Changes from any write transaction are not visible to read transactions started before it. - * - * @param callback - Function to execute within the transaction - * @param lockTimeout - Time in milliseconds to wait for a lock before throwing an error - * @returns The result of the callback - * @throws Error if the lock cannot be obtained within the timeout period - */ async readTransaction( callback: (tx: Transaction) => Promise, lockTimeout: number = DEFAULT_LOCK_TIMEOUT_MS @@ -909,16 +600,6 @@ SELECT * FROM crud_entries; ); } - /** - * Open a read-write transaction. - * This takes a global lock - only one write transaction can execute against the database at a time. - * Statements within the transaction must be done on the provided {@link Transaction} interface. - * - * @param callback - Function to execute within the transaction - * @param lockTimeout - Time in milliseconds to wait for a lock before throwing an error - * @returns The result of the callback - * @throws Error if the lock cannot be obtained within the timeout period - */ async writeTransaction( callback: (tx: Transaction) => Promise, lockTimeout: number = DEFAULT_LOCK_TIMEOUT_MS @@ -934,39 +615,8 @@ SELECT * FROM crud_entries; ); } - /** - * This version of `watch` uses `AsyncGenerator`, for documentation see {@link AbstractPowerSyncDatabase.watchWithAsyncGenerator}. - * Can be overloaded to use a callback handler instead, for documentation see {@link AbstractPowerSyncDatabase.watchWithCallback}. - * - * @example - * ```javascript - * async *attachmentIds() { - * for await (const result of this.powersync.watch( - * `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`, - * [] - * )) { - * yield result.rows?._array.map((r) => r.id) ?? []; - * } - * } - * ``` - */ watch(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable; - /** - * See {@link AbstractPowerSyncDatabase.watchWithCallback}. - * - * @example - * ```javascript - * onAttachmentIdsChange(onResult) { - * this.powersync.watch( - * `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`, - * [], - * { - * onResult: (result) => onResult(result.rows?._array.map((r) => r.id) ?? []) - * } - * ); - * } - * ``` - */ + watch(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void; watch( @@ -986,25 +636,6 @@ SELECT * FROM crud_entries; return this.watchWithAsyncGenerator(sql, parameters, options); } - /** - * Allows defining a query which can be used to build a {@link WatchedQuery}. - * The defined query will be executed with {@link AbstractPowerSyncDatabase#getAll}. - * An optional mapper function can be provided to transform the results. - * - * @example - * ```javascript - * const watchedTodos = powersync.query({ - * sql: `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`, - * parameters: [], - * mapper: (row) => ({ - * ...row, - * created_at: new Date(row.created_at as string) - * }) - * }) - * .watch() - * // OR use .differentialWatch() for fine-grained watches. - * ``` - */ query(query: ArrayQueryDefinition): Query { const { sql, parameters = [], mapper } = query; const compatibleQuery: WatchCompatibleQuery = { @@ -1020,21 +651,6 @@ SELECT * FROM crud_entries; return this.customQuery(compatibleQuery); } - /** - * Allows building a {@link WatchedQuery} using an existing {@link WatchCompatibleQuery}. - * The watched query will use the provided {@link WatchCompatibleQuery.execute} method to query results. - * - * @example - * ```javascript - * - * // Potentially a query from an ORM like Drizzle - * const query = db.select().from(lists); - * - * const watchedTodos = powersync.customQuery(query) - * .watch() - * // OR use .differentialWatch() for fine-grained watches. - * ``` - */ customQuery(query: WatchCompatibleQuery): Query { return new CustomQuery({ db: this, @@ -1042,18 +658,6 @@ SELECT * FROM crud_entries; }); } - /** - * Execute a read query every time the source tables are modified. - * Use {@link SQLOnChangeOptions.throttleMs} to specify the minimum interval between queries. - * Source tables are automatically detected using `EXPLAIN QUERY PLAN`. - * - * Note that the `onChange` callback member of the handler is required. - * - * @param sql - The SQL query to execute - * @param parameters - Optional array of parameters to bind to the query - * @param handler - Callbacks for handling results and errors - * @param options - Options for configuring watch behavior - */ watchWithCallback(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void { const { onResult, @@ -1103,16 +707,6 @@ SELECT * FROM crud_entries; }); } - /** - * Execute a read query every time the source tables are modified. - * Use {@link SQLOnChangeOptions.throttleMs} to specify the minimum interval between queries. - * Source tables are automatically detected using `EXPLAIN QUERY PLAN`. - * - * @param sql - The SQL query to execute - * @param parameters - Optional array of parameters to bind to the query - * @param options - Options for configuring watch behavior - * @returns An AsyncIterable that yields QueryResults whenever the data changes - */ watchWithAsyncGenerator(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable { return new EventIterator((eventOptions) => { const handler: WatchHandler = { @@ -1132,16 +726,6 @@ SELECT * FROM crud_entries; }); } - /** - * Resolves the list of tables that are used in a SQL query. - * If tables are specified in the options, those are used directly. - * Otherwise, analyzes the query using EXPLAIN to determine which tables are accessed. - * - * @param sql - The SQL query to analyze - * @param parameters - Optional parameters for the SQL query - * @param options - Optional watch options that may contain explicit table list - * @returns Array of table names that the query depends on - */ async resolveTables(sql: string, parameters?: any[], options?: SQLWatchOptions): Promise { const resolvedTables = options?.tables ? [...options.tables] : []; if (!options?.tables) { @@ -1161,34 +745,7 @@ SELECT * FROM crud_entries; return resolvedTables; } - /** - * This version of `onChange` uses `AsyncGenerator`, for documentation see {@link AbstractPowerSyncDatabase.onChangeWithAsyncGenerator}. - * Can be overloaded to use a callback handler instead, for documentation see {@link AbstractPowerSyncDatabase.onChangeWithCallback}. - * - * @example - * ```javascript - * async monitorChanges() { - * for await (const event of this.powersync.onChange({tables: ['todos']})) { - * console.log('Detected change event:', event); - * } - * } - * ``` - */ onChange(options?: SQLOnChangeOptions): AsyncIterable; - /** - * See {@link AbstractPowerSyncDatabase.onChangeWithCallback}. - * - * @example - * ```javascript - * monitorChanges() { - * this.powersync.onChange({ - * onChange: (event) => { - * console.log('Change detected:', event); - * } - * }, { tables: ['todos'] }); - * } - * ``` - */ onChange(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void; onChange( @@ -1206,18 +763,6 @@ SELECT * FROM crud_entries; return this.onChangeWithAsyncGenerator(options); } - /** - * Invoke the provided callback on any changes to any of the specified tables. - * - * This is preferred over {@link AbstractPowerSyncDatabase.watchWithCallback} when multiple queries need to be performed - * together when data is changed. - * - * Note that the `onChange` callback member of the handler is required. - * - * @param handler - Callbacks for handling change events and errors - * @param options - Options for configuring watch behavior - * @returns A dispose function to stop watching for changes - */ onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void { const { onChange, @@ -1271,17 +816,6 @@ SELECT * FROM crud_entries; return () => dispose(); } - /** - * Create a Stream of changes to any of the specified tables. - * - * This is preferred over {@link AbstractPowerSyncDatabase.watchWithAsyncGenerator} when multiple queries need to be - * performed together when data is changed. - * - * Note: do not declare this as `async *onChange` as it will not work in React Native. - * - * @param options - Options for configuring watch behavior - * @returns An AsyncIterable that yields change events whenever the specified tables change - */ onChangeWithAsyncGenerator(options?: SQLWatchOptions): AsyncIterable { const resolvedOptions = options ?? {}; @@ -1307,6 +841,10 @@ SELECT * FROM crud_entries; }); } + createMutex() { + return new Mutex(); + } + private handleTableChanges( changedTables: Set, watchedTables: Set, diff --git a/packages/common/src/client/ConnectionManager.ts b/packages/shared-internals/src/client/ConnectionManager.ts similarity index 96% rename from packages/common/src/client/ConnectionManager.ts rename to packages/shared-internals/src/client/ConnectionManager.ts index e7800c0dd..93a97f632 100644 --- a/packages/common/src/client/ConnectionManager.ts +++ b/packages/shared-internals/src/client/ConnectionManager.ts @@ -1,15 +1,20 @@ -import { LogLevels, PowerSyncLogger } from '../utils/Logger.js'; -import { SyncStatus } from '../db/crud/SyncStatus.js'; -import { BaseListener, BaseObserver } from '../utils/BaseObserver.js'; -import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js'; -import { StreamingSyncImplementation, SubscribedStream } from './sync/stream/AbstractStreamingSyncImplementation.js'; import { + LogLevels, + PowerSyncLogger, + SyncStatus, + BaseListener, + PowerSyncBackendConnector, SyncStream, SyncStreamDescription, SyncStreamSubscribeOptions, - SyncStreamSubscription -} from './sync/sync-streams.js'; -import { ResolvedSyncOptions, resolveSyncOptions, SyncOptions } from './sync/options.js'; + SyncStreamSubscription, + ResolvedSyncOptions, + resolveSyncOptions, + SyncOptions +} from '@powersync/common'; + +import { BaseObserver } from '../utils/BaseObserver.js'; +import { StreamingSyncImplementation, SubscribedStream } from './sync/stream/AbstractStreamingSyncImplementation.js'; /** * @internal diff --git a/packages/common/src/client/CustomQuery.ts b/packages/shared-internals/src/client/CustomQuery.ts similarity index 78% rename from packages/common/src/client/CustomQuery.ts rename to packages/shared-internals/src/client/CustomQuery.ts index c7dfdd6bb..beb1dfa41 100644 --- a/packages/common/src/client/CustomQuery.ts +++ b/packages/shared-internals/src/client/CustomQuery.ts @@ -1,12 +1,16 @@ -import { AbstractPowerSyncDatabase } from './AbstractPowerSyncDatabase.js'; -import { Query, StandardWatchedQueryOptions } from './Query.js'; -import { FalsyComparator } from './watched/processors/comparators.js'; import { - DifferentialQueryProcessor, + Query, + StandardWatchedQueryOptions, + FalsyComparator, + WatchCompatibleQuery, + WatchedQueryOptions, DifferentialWatchedQueryOptions -} from './watched/processors/DifferentialQueryProcessor.js'; -import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js'; -import { DEFAULT_WATCH_QUERY_OPTIONS, WatchCompatibleQuery, WatchedQueryOptions } from './watched/WatchedQuery.js'; +} from '@powersync/common'; + +import { AbstractPowerSyncDatabase } from './AbstractPowerSyncDatabase.js'; +import { DifferentialQueryProcessor } from './watched/DifferentialQueryProcessor.js'; +import { OnChangeQueryProcessor } from './watched/OnChangeQueryProcessor.js'; +import { DEFAULT_WATCH_QUERY_OPTIONS } from './watched/WatchedQuery.js'; /** * @internal diff --git a/packages/common/src/client/sync/bucket/BucketStorageAdapter.ts b/packages/shared-internals/src/client/sync/bucket/BucketStorageAdapter.ts similarity index 83% rename from packages/common/src/client/sync/bucket/BucketStorageAdapter.ts rename to packages/shared-internals/src/client/sync/bucket/BucketStorageAdapter.ts index 0b957a58a..bf7267b60 100644 --- a/packages/common/src/client/sync/bucket/BucketStorageAdapter.ts +++ b/packages/shared-internals/src/client/sync/bucket/BucketStorageAdapter.ts @@ -1,10 +1,5 @@ -import { BaseListener, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js'; -import { CrudBatch } from './CrudBatch.js'; -import { CrudEntry } from './CrudEntry.js'; +import { BaseListener, BaseObserverInterface, Disposable, CrudBatch, CrudEntry } from '@powersync/common'; -/** - * @internal - */ export enum PSInternalTable { DATA = 'ps_data', CRUD = 'ps_crud', @@ -13,9 +8,6 @@ export enum PSInternalTable { UNTYPED = 'ps_untyped' } -/** - * @internal - */ export enum PowerSyncControlCommand { PROCESS_TEXT_LINE = 'line_text', PROCESS_BSON_LINE = 'line_binary', @@ -30,16 +22,10 @@ export enum PowerSyncControlCommand { CONNECTION_STATE = 'connection' } -/** - * @internal - */ export interface BucketStorageListener extends BaseListener { crudUpdate: () => void; } -/** - * @internal - */ export interface BucketStorageAdapter extends BaseObserverInterface, Disposable { init(): Promise; diff --git a/packages/shared-internals/src/client/sync/bucket/CrudEntry.ts b/packages/shared-internals/src/client/sync/bucket/CrudEntry.ts new file mode 100644 index 000000000..e731cfea4 --- /dev/null +++ b/packages/shared-internals/src/client/sync/bucket/CrudEntry.ts @@ -0,0 +1,116 @@ +import { CrudEntry, UpdateType } from '@powersync/common'; + +/** + * @internal + */ +export type CrudEntryJSON = { + id: string; + data: string; + tx_id?: number; +}; + +type CrudEntryDataJSON = { + data: Record; + old?: Record; + op: UpdateType; + type: string; + id: string; + metadata?: string; +}; + +/** + * The output JSON seems to be a third type of JSON, not the same as the input JSON. + */ +type CrudEntryOutputJSON = { + op_id: number; + op: UpdateType; + type: string; + id: string; + tx_id?: number; + data?: Record; + old?: Record; + metadata?: string; +}; + +/** + * A single client-side change. + * + * @public + */ +export class CrudEntryImpl implements CrudEntry { + clientId: number; + id: string; + op: UpdateType; + opData?: Record; + previousValues?: Record; + table: string; + transactionId?: number; + metadata?: string; + + static fromRow(dbRow: CrudEntryJSON): CrudEntry { + const data: CrudEntryDataJSON = JSON.parse(dbRow.data); + return new CrudEntryImpl( + parseInt(dbRow.id), + data.op, + data.type, + data.id, + dbRow.tx_id, + data.data, + data.old, + data.metadata + ); + } + + constructor( + clientId: number, + op: UpdateType, + table: string, + id: string, + transactionId?: number, + opData?: Record, + previousValues?: Record, + metadata?: string + ) { + this.clientId = clientId; + this.id = id; + this.op = op; + this.opData = opData; + this.table = table; + this.transactionId = transactionId; + this.previousValues = previousValues; + this.metadata = metadata; + } + + toJSON(): CrudEntryOutputJSON { + return { + op_id: this.clientId, + op: this.op, + type: this.table, + id: this.id, + tx_id: this.transactionId, + data: this.opData, + old: this.previousValues, + metadata: this.metadata + }; + } + + equals(entry: CrudEntry) { + return JSON.stringify(this.toComparisonArray()) == JSON.stringify(entry.toComparisonArray()); + } + + /** + * Generates an array for use in deep comparison operations + */ + toComparisonArray(): unknown[] { + return [ + this.transactionId, + this.clientId, + this.op, + this.table, + this.id, + this.opData, + this.previousValues, + this.metadata + ]; + } +} diff --git a/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts b/packages/shared-internals/src/client/sync/bucket/SqliteBucketStorage.ts similarity index 94% rename from packages/common/src/client/sync/bucket/SqliteBucketStorage.ts rename to packages/shared-internals/src/client/sync/bucket/SqliteBucketStorage.ts index 2a2b62dba..66aeba269 100644 --- a/packages/common/src/client/sync/bucket/SqliteBucketStorage.ts +++ b/packages/shared-internals/src/client/sync/bucket/SqliteBucketStorage.ts @@ -1,15 +1,22 @@ -import { LogLevels, PowerSyncLogger } from '../../../utils/Logger.js'; -import { DBAdapter, extractTableUpdates, Transaction } from '../../../db/DBAdapter.js'; +import { + LogLevels, + PowerSyncLogger, + DBAdapter, + extractTableUpdates, + Transaction, + CrudEntry, + CrudBatch +} from '@powersync/common'; + import { BaseObserver } from '../../../utils/BaseObserver.js'; -import { MAX_OP_ID } from '../../constants.js'; import { BucketStorageAdapter, BucketStorageListener, PowerSyncControlCommand, PSInternalTable } from './BucketStorageAdapter.js'; -import { CrudBatch } from './CrudBatch.js'; -import { CrudEntry, CrudEntryJSON } from './CrudEntry.js'; +import { CrudEntryImpl, CrudEntryJSON } from './CrudEntry.js'; +import { MAX_OP_ID } from '../../../constants.js'; /** * @internal @@ -125,7 +132,7 @@ export class SqliteBucketStorage extends BaseObserver imp if (!next) { return; } - return CrudEntry.fromRow(next); + return CrudEntryImpl.fromRow(next); } async hasCrud(): Promise { @@ -146,7 +153,7 @@ export class SqliteBucketStorage extends BaseObserver imp const all: CrudEntry[] = []; for (const row of crudResult) { - all.push(CrudEntry.fromRow(row)); + all.push(CrudEntryImpl.fromRow(row)); } if (all.length === 0) { diff --git a/packages/shared-internals/src/client/sync/options.ts b/packages/shared-internals/src/client/sync/options.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/common/src/client/sync/stream/AbstractRemote.ts b/packages/shared-internals/src/client/sync/stream/AbstractRemote.ts similarity index 98% rename from packages/common/src/client/sync/stream/AbstractRemote.ts rename to packages/shared-internals/src/client/sync/stream/AbstractRemote.ts index 4f369166d..5d06b33e4 100644 --- a/packages/common/src/client/sync/stream/AbstractRemote.ts +++ b/packages/shared-internals/src/client/sync/stream/AbstractRemote.ts @@ -1,9 +1,9 @@ import { type fetch } from 'cross-fetch'; import { Requestable, RSocket, RSocketConnector } from 'rsocket-core'; import PACKAGE from '../../../../package.json' with { type: 'json' }; +import { FetchStrategy, PowerSyncCredentials, LogLevels, PowerSyncLogger } from '@powersync/common'; + import { AbortOperation } from '../../../utils/AbortOperation.js'; -import { LogLevels, PowerSyncLogger } from '../../../utils/Logger.js'; -import { PowerSyncCredentials } from '../../connection/PowerSyncCredentials.js'; import { WebsocketClientTransport } from './WebsocketClientTransport.js'; import { doneResult, @@ -13,7 +13,6 @@ import { } from '../../../utils/stream_transform.js'; import { EventIterator } from 'event-iterator'; import type { Queue } from 'event-iterator/lib/event-iterator.js'; -import { FetchStrategy } from '../options.js'; /** * @internal diff --git a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts similarity index 86% rename from packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts rename to packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index 2014c708b..1046d9aff 100644 --- a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -1,14 +1,24 @@ -import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js'; +import { + BaseObserverInterface, + BaseListener, + CrudEntry, + Disposable, + LogLevels, + PowerSyncLogger, + SyncStreamConnectionMethod, + ResolvedSyncOptions, + SyncStatus, + SyncDataFlowStatus +} from '@powersync/common'; + import { AbortOperation } from '../../../utils/AbortOperation.js'; -import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js'; -import { LogLevels, PowerSyncLogger } from '../../../utils/Logger.js'; +import { BaseObserver } from '../../../utils/BaseObserver.js'; import { BucketStorageAdapter, PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js'; -import { CrudEntry } from '../bucket/CrudEntry.js'; import { AbstractRemote, SyncStreamOptions } from './AbstractRemote.js'; import { + CoreSyncStatus, Instruction, NonInterruptingInstruction, - coreStatusToJs, isInterruptingInstruction } from './core-instruction.js'; import { @@ -19,7 +29,7 @@ import { valueResult } from '../../../utils/stream_transform.js'; import { asyncNotifier } from '../../../utils/async.js'; -import { ResolvedSyncOptions, SyncStreamConnectionMethod } from '../options.js'; +import { SyncStatusSnapshot } from '../../../db/crud/SyncStatus.js'; /** * @internal @@ -64,15 +74,10 @@ export interface AbstractStreamingSyncImplementationOptions { * @internal */ export interface StreamingSyncImplementationListener extends BaseListener { - /** - * Triggered whenever a status update has been attempted to be made or - * refreshed. - */ - statusUpdated?: ((statusUpdate: SyncStatusOptions) => void) | undefined; /** * Triggers whenever the status' members have changed in value */ - statusChanged?: ((status: SyncStatus) => void) | undefined; + statusChanged?: ((core: CoreSyncStatus | null, dataFlow: SyncDataFlowStatus) => void) | undefined; } /** @@ -94,7 +99,6 @@ export interface StreamingSyncImplementation syncStatus: SyncStatus; triggerCrudUpload: () => void; waitForReady(): Promise; - waitForStatus(status: SyncStatusOptions): Promise; waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise; updateSubscriptions(subscriptions: SubscribedStream[]): void; markConnectionMayHaveChanged(): void; @@ -127,7 +131,7 @@ export abstract class AbstractStreamingSyncImplementation private notifyCompletedUploads?: () => void; private handleActiveStreamsChange?: () => void; - syncStatus: SyncStatus; + syncStatus: SyncStatusSnapshot; constructor(options: AbstractStreamingSyncImplementationOptions) { super(); @@ -135,15 +139,7 @@ export abstract class AbstractStreamingSyncImplementation this.activeStreams = options.subscriptions; this.logger = options.logger; - this.syncStatus = new SyncStatus({ - connected: false, - connecting: false, - lastSyncedAt: undefined, - dataFlow: { - uploading: false, - downloading: false - } - }); + this.syncStatus = new SyncStatusSnapshot(null, {}); this.abortController = null; } @@ -153,26 +149,6 @@ export abstract class AbstractStreamingSyncImplementation async waitForReady() {} - waitForStatus(status: SyncStatusOptions): Promise { - return this.waitUntilStatusMatches((currentStatus) => { - /** - * Match only the partial status options provided in the - * matching status - */ - const matchPartialObject = (compA: object, compB: any): any => { - return Object.entries(compA).every(([key, value]) => { - const comparisonBValue = compB[key]; - if (typeof value == 'object' && typeof comparisonBValue == 'object') { - return matchPartialObject(value, comparisonBValue); - } - return value == comparisonBValue; - }); - }; - - return matchPartialObject(status, currentStatus); - }); - } - waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise { return new Promise((resolve) => { if (predicate(this.syncStatus)) { @@ -181,8 +157,8 @@ export abstract class AbstractStreamingSyncImplementation } const l = this.registerListener({ - statusChanged: (updatedStatus) => { - if (predicate(updatedStatus)) { + statusChanged: () => { + if (predicate(this.syncStatus)) { resolve(); l?.(); } @@ -245,11 +221,7 @@ export abstract class AbstractStreamingSyncImplementation */ const nextCrudItem = await this.options.adapter.nextCrudItem(); if (nextCrudItem) { - this.updateSyncStatus({ - dataFlow: { - uploading: true - } - }); + this.updateDataFlowStatus({ uploading: true }); if (nextCrudItem.clientId == checkedCrudItem?.clientId) { // This will force a higher log level than exceptions which are caught here. @@ -265,11 +237,7 @@ The next upload iteration will be delayed.` checkedCrudItem = nextCrudItem; await this.options.uploadCrud(); - this.updateSyncStatus({ - dataFlow: { - uploadError: undefined - } - }); + this.updateDataFlowStatus({ uploadError: undefined }); } else { // Uploading is completed const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint()); @@ -283,12 +251,7 @@ The next upload iteration will be delayed.` } } catch (ex) { checkedCrudItem = undefined; - this.updateSyncStatus({ - dataFlow: { - uploading: false, - uploadError: ex as Error - } - }); + this.updateDataFlowStatus({ uploading: false, uploadError: ex as Error }); await this.delayRetry(signal, options.retryDelayMs); if (!this.isConnected) { // Exit the upload loop if the sync stream is no longer connected @@ -300,11 +263,7 @@ The next upload iteration will be delayed.` error: ex }); } finally { - this.updateSyncStatus({ - dataFlow: { - uploading: false - } - }); + this.updateDataFlowStatus({ uploading: false }); } } } @@ -328,13 +287,13 @@ The next upload iteration will be delayed.` // Return a promise that resolves when the connection status is updated to indicate that we're connected. return new Promise((resolve) => { const disposer = this.registerListener({ - statusChanged: (status) => { - if (status.dataFlowStatus.downloadError != null) { + statusChanged: (status, dataFlow) => { + if (dataFlow.downloadError != null) { this.logger.log({ level: LogLevels.warn, message: 'Initial connect attempt did not successfully connect to server' }); - } else if (status.connecting) { + } else if (status && status.connecting) { // Still connecting. return; } @@ -366,7 +325,18 @@ The next upload iteration will be delayed.` this.streamingSyncPromise = undefined; this.abortController = null; - this.updateSyncStatus({ connected: false, connecting: false }); + this.markAsDisconnected(); + } + + private markAsDisconnected() { + const current = this.syncStatus.core; + this.updateSyncStatus({ + connected: false, + connecting: false, + priority_status: current?.priority_status ?? [], + downloading: null, + streams: current?.streams ?? [] + }); } private async streamingSync(signal: AbortSignal, options: ResolvedSyncOptions): Promise { @@ -391,14 +361,7 @@ The next upload iteration will be delayed.` nestedAbortController.abort(signal?.reason ?? new AbortOperation('Received command to disconnect from upstream')); this.crudUpdateListener?.(); this.crudUpdateListener = undefined; - this.updateSyncStatus({ - connected: false, - connecting: false, - dataFlow: { - downloading: false, - downloadProgress: null - } - }); + this.markAsDisconnected(); }); /** @@ -408,7 +371,6 @@ The next upload iteration will be delayed.` * - Close any sync stream ReadableStreams (which will also close any established network requests) */ while (true) { - this.updateSyncStatus({ connecting: true }); let shouldDelayRetry = true; let result: RustIterationResult | null = null; @@ -444,11 +406,7 @@ The next upload iteration will be delayed.` this.logger.log({ level: LogLevels.error, message: 'Sync error', error: ex }); } - this.updateSyncStatus({ - dataFlow: { - downloadError: ex as Error - } - }); + this.updateDataFlowStatus({ downloadError: ex as Error }); } finally { this.notifyCompletedUploads = undefined; @@ -458,10 +416,7 @@ The next upload iteration will be delayed.` } if (result?.immediateRestart != true) { - this.updateSyncStatus({ - connected: false, - connecting: true // May be unnecessary - }); + this.markAsDisconnected(); // On error, wait a little before retrying if (shouldDelayRetry) { @@ -472,7 +427,7 @@ The next upload iteration will be delayed.` } // Mark as disconnected if here - this.updateSyncStatus({ connected: false, connecting: false }); + this.markAsDisconnected(); } markConnectionMayHaveChanged() { @@ -668,7 +623,7 @@ The next upload iteration will be delayed.` break; } } else if ('UpdateSyncStatus' in instruction) { - syncImplementation.updateSyncStatus(coreStatusToJs(instruction.UpdateSyncStatus.status)); + syncImplementation.updateSyncStatus(instruction.UpdateSyncStatus.status); } else if ('FetchCredentials' in instruction) { if (instruction.FetchCredentials.did_expire) { remote.invalidateCredentials(); @@ -692,11 +647,7 @@ The next upload iteration will be delayed.` } else if ('FlushFileSystem' in instruction) { // Not necessary on JS platforms. } else if ('DidCompleteSync' in instruction) { - syncImplementation.updateSyncStatus({ - dataFlow: { - downloadError: undefined - } - }); + syncImplementation.updateDataFlowStatus({ downloadError: undefined }); } } @@ -782,26 +733,21 @@ The next upload iteration will be delayed.` return { immediateRestart: hideDisconnectOnRestart }; } - protected updateSyncStatus(options: SyncStatusOptions) { - const updatedStatus = new SyncStatus({ - connected: options.connected ?? this.syncStatus.connected, - connecting: !options.connected && (options.connecting ?? this.syncStatus.connecting), - lastSyncedAt: options.lastSyncedAt ?? this.syncStatus.lastSyncedAt, - dataFlow: { - ...this.syncStatus.dataFlowStatus, - ...options.dataFlow - }, - priorityStatusEntries: options.priorityStatusEntries ?? this.syncStatus.priorityStatusEntries + protected updateSyncStatus(core: CoreSyncStatus | null, dataFlow?: SyncDataFlowStatus) { + const updated = new SyncStatusSnapshot(core, { + ...this.syncStatus.dataFlowStatus, + ...dataFlow }); - if (!this.syncStatus.isEqual(updatedStatus)) { - this.syncStatus = updatedStatus; + if (!this.syncStatus.isEqual(updated)) { + this.syncStatus = updated; // Only trigger this is there was a change - this.iterateListeners((cb) => cb.statusChanged?.(updatedStatus)); + this.iterateListeners((cb) => cb.statusChanged?.(updated.core, updated.dataFlowStatus)); } + } - // trigger this for all updates - this.iterateListeners((cb) => cb.statusUpdated?.(options)); + protected updateDataFlowStatus(dataFlow: SyncDataFlowStatus) { + this.updateSyncStatus(this.syncStatus.core, dataFlow); } private async delayRetry(signal: AbortSignal, delay: number): Promise { diff --git a/packages/common/src/client/sync/stream/WebsocketClientTransport.ts b/packages/shared-internals/src/client/sync/stream/WebsocketClientTransport.ts similarity index 100% rename from packages/common/src/client/sync/stream/WebsocketClientTransport.ts rename to packages/shared-internals/src/client/sync/stream/WebsocketClientTransport.ts diff --git a/packages/common/src/client/sync/stream/core-instruction.ts b/packages/shared-internals/src/client/sync/stream/core-instruction.ts similarity index 61% rename from packages/common/src/client/sync/stream/core-instruction.ts rename to packages/shared-internals/src/client/sync/stream/core-instruction.ts index 767b57338..08fcc4cd8 100644 --- a/packages/common/src/client/sync/stream/core-instruction.ts +++ b/packages/shared-internals/src/client/sync/stream/core-instruction.ts @@ -1,6 +1,3 @@ -import * as sync_status from '../../../db/crud/SyncStatus.js'; -import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js'; - /** * An internal instruction emitted by the sync client in the core extension in response to the JS * SDK passing sync data into the extension. @@ -76,34 +73,6 @@ export interface FetchCredentials { did_expire: boolean; } -function priorityToJs(status: SyncPriorityStatus): sync_status.SyncPriorityStatus { - return { - priority: status.priority, - hasSynced: status.has_synced ?? undefined, - lastSyncedAt: status.last_synced_at != null ? new Date(status.last_synced_at * 1000) : undefined - }; -} - -export function coreStatusToJs(status: CoreSyncStatus): sync_status.SyncStatusOptions { - const coreCompleteSync = status.priority_status.find((s) => s.priority == FULL_SYNC_PRIORITY); - const completeSync = coreCompleteSync != null ? priorityToJs(coreCompleteSync) : null; - - return { - connected: status.connected, - connecting: status.connecting, - dataFlow: { - // We expose downloading as a boolean field, the core extension reports download information as a nullable - // download status. When that status is non-null, a download is in progress. - downloading: status.downloading != null, - downloadProgress: status.downloading?.buckets, - internalStreamSubscriptions: status.streams - }, - lastSyncedAt: completeSync?.lastSyncedAt, - hasSynced: completeSync?.hasSynced, - priorityStatusEntries: status.priority_status.map(priorityToJs) - }; -} - export function isInterruptingInstruction(instruction: Instruction): instruction is InterruptingInstruction { return 'EstablishSyncStream' in instruction || 'CloseSyncStream' in instruction; } diff --git a/packages/common/src/client/triggers/MemoryTriggerClaimManager.ts b/packages/shared-internals/src/client/triggers/MemoryTriggerClaimManager.ts similarity index 90% rename from packages/common/src/client/triggers/MemoryTriggerClaimManager.ts rename to packages/shared-internals/src/client/triggers/MemoryTriggerClaimManager.ts index 877f3afd4..989714a43 100644 --- a/packages/common/src/client/triggers/MemoryTriggerClaimManager.ts +++ b/packages/shared-internals/src/client/triggers/MemoryTriggerClaimManager.ts @@ -1,4 +1,4 @@ -import { TriggerClaimManager } from './TriggerManager.js'; +import { TriggerClaimManager } from './TriggerManagerImpl.js'; const CLAIM_STORE = new Map Promise>(); diff --git a/packages/common/src/client/triggers/TriggerManagerImpl.ts b/packages/shared-internals/src/client/triggers/TriggerManagerImpl.ts similarity index 93% rename from packages/common/src/client/triggers/TriggerManagerImpl.ts rename to packages/shared-internals/src/client/triggers/TriggerManagerImpl.ts index 7d663ee5a..fa1f05f5e 100644 --- a/packages/common/src/client/triggers/TriggerManagerImpl.ts +++ b/packages/shared-internals/src/client/triggers/TriggerManagerImpl.ts @@ -1,18 +1,51 @@ -import { LockContext } from '../../db/DBAdapter.js'; -import { Schema } from '../../db/schema/Schema.js'; -import { LogLevels } from '../../utils/Logger.js'; import type { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js'; import { DEFAULT_WATCH_THROTTLE_MS } from '../watched/WatchedQuery.js'; import { CreateDiffTriggerOptions, DiffTriggerOperation, + LockContext, + LogLevels, + Schema, TrackDiffOptions, TriggerManager, - TriggerManagerConfig, TriggerRemoveCallback, TriggerRemoveCallbackOptions, WithDiffOptions -} from './TriggerManager.js'; +} from '@powersync/common'; + +/** + * @experimental + * @internal + */ +export interface TriggerManagerConfig { + claimManager: TriggerClaimManager; +} + +/** + * @experimental + * Manages claims on persisted SQLite triggers and destination tables to enable proper cleanup + * when they are no longer actively in use. + * + * When using persisted triggers (especially for OPFS multi-tab scenarios), we need a reliable way to determine which resources are still actively in use across different connections/tabs so stale resources can be safely cleaned up without interfering with active triggers. + * + * A cleanup process runs + * on database creation (and every 2 minutes) that: + * 1. Queries for existing managed persisted resources + * 2. Checks with the claim manager if any consumer is actively using those resources + * 3. Deletes unused resources + */ + +export interface TriggerClaimManager { + /** + * Obtains or marks a claim on a certain identifier. + * @returns a callback to release the claim. + */ + obtainClaim: (identifier: string) => Promise<() => Promise>; + /** + * Checks if a claim is present for an identifier. + */ + checkClaim: (identifier: string) => Promise; +} export type TriggerManagerImplOptions = TriggerManagerConfig & { db: AbstractPowerSyncDatabase; diff --git a/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts b/packages/shared-internals/src/client/watched/AbstractQueryProcessor.ts similarity index 96% rename from packages/common/src/client/watched/processors/AbstractQueryProcessor.ts rename to packages/shared-internals/src/client/watched/AbstractQueryProcessor.ts index 106e9df68..04230db12 100644 --- a/packages/common/src/client/watched/processors/AbstractQueryProcessor.ts +++ b/packages/shared-internals/src/client/watched/AbstractQueryProcessor.ts @@ -1,13 +1,13 @@ -import { LogLevels } from '../../../utils/Logger.js'; -import { AbstractPowerSyncDatabase } from '../../../client/AbstractPowerSyncDatabase.js'; -import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js'; import { + LogLevels, WatchedQuery, WatchedQueryListener, WatchedQueryListenerEvent, WatchedQueryOptions, WatchedQueryState -} from '../WatchedQuery.js'; +} from '@powersync/common'; +import { MetaBaseObserver } from '../../utils/MetaBaseObserver.js'; +import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js'; /** * @internal diff --git a/packages/shared-internals/src/client/watched/DifferentialQueryProcessor.ts b/packages/shared-internals/src/client/watched/DifferentialQueryProcessor.ts new file mode 100644 index 000000000..e356eef5f --- /dev/null +++ b/packages/shared-internals/src/client/watched/DifferentialQueryProcessor.ts @@ -0,0 +1,229 @@ +import { + DifferentialWatchedQuery, + DifferentialWatchedQueryComparator, + DifferentialWatchedQuerySettings, + WatchedQueryDifferential, + WatchedQueryRowDifferential +} from '@powersync/common'; +import { + AbstractQueryProcessor, + AbstractQueryProcessorOptions, + LinkQueryOptions, + MutableWatchedQueryState +} from './AbstractQueryProcessor.js'; + +/** + * @internal + */ +export interface DifferentialQueryProcessorOptions extends AbstractQueryProcessorOptions< + RowType[], + DifferentialWatchedQuerySettings +> { + rowComparator?: DifferentialWatchedQueryComparator; +} + +type DataHashMap = Map; + +/** + * An empty differential result set. + * This is used as the initial state for differential incrementally watched queries. + * + * @internal + */ +export const EMPTY_DIFFERENTIAL = { + added: [], + all: [], + removed: [], + updated: [], + unchanged: [] +}; + +/** + * Default implementation of the {@link DifferentialWatchedQueryComparator} for watched queries. + * It keys items by their `id` property if available, alternatively it uses JSON stringification + * of the entire item for the key and comparison. + * + * @internal + */ +export const DEFAULT_ROW_COMPARATOR: DifferentialWatchedQueryComparator = { + keyBy: (item) => { + if (item && typeof item == 'object' && typeof item['id'] == 'string') { + return item['id']; + } + return JSON.stringify(item); + }, + compareBy: (item) => JSON.stringify(item) +}; + +/** + * Uses the PowerSync onChange event to trigger watched queries. + * Results are emitted on every change of the relevant tables. + * @internal + */ +export class DifferentialQueryProcessor + extends AbstractQueryProcessor>, DifferentialWatchedQuerySettings> + implements DifferentialWatchedQuery +{ + protected comparator: DifferentialWatchedQueryComparator; + + constructor(protected options: DifferentialQueryProcessorOptions) { + super(options); + this.comparator = options.rowComparator ?? DEFAULT_ROW_COMPARATOR; + } + + /* + * @returns If the sets are equal + */ + protected differentiate( + current: RowType[], + previousMap: DataHashMap + ): { diff: WatchedQueryDifferential; map: DataHashMap; hasChanged: boolean } { + const { keyBy, compareBy } = this.comparator; + + let hasChanged = false; + const currentMap = new Map(); + const removedTracker = new Set(previousMap.keys()); + + // Allow mutating to populate the data temporarily. + const diff = { + all: [] as RowType[], + added: [] as RowType[], + removed: [] as RowType[], + updated: [] as WatchedQueryRowDifferential[], + unchanged: [] as RowType[] + }; + + /** + * Looping over the current result set array is important to preserve + * the ordering of the result set. + * We can replace items in the current array with previous object references if they are equal. + */ + for (const item of current) { + const key = keyBy(item); + const hash = compareBy(item); + currentMap.set(key, { hash, item }); + + const previousItem = previousMap.get(key); + if (!previousItem) { + // New item + hasChanged = true; + diff.added.push(item); + diff.all.push(item); + } else { + // Existing item + if (hash == previousItem.hash) { + diff.unchanged.push(previousItem.item); + // Use the previous object reference + diff.all.push(previousItem.item); + // update the map to preserve the reference + currentMap.set(key, previousItem); + } else { + hasChanged = true; + diff.updated.push({ current: item, previous: previousItem.item }); + // Use the new reference + diff.all.push(item); + } + } + // The item is present, we don't consider it removed + removedTracker.delete(key); + } + + diff.removed = Array.from(removedTracker).map((key) => previousMap.get(key)!.item); + hasChanged = hasChanged || diff.removed.length > 0; + + return { + diff, + hasChanged, + map: currentMap + }; + } + + protected async linkQuery(options: LinkQueryOptions>): Promise { + const { db, watchOptions } = this.options; + const { abortSignal } = options; + + const compiledQuery = watchOptions.query.compile(); + const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[], { + tables: options.settings.triggerOnTables + }); + + let currentMap: DataHashMap = new Map(); + + // populate the currentMap from the placeholder data + this.state.data.forEach((item) => { + currentMap.set(this.comparator.keyBy(item), { + hash: this.comparator.compareBy(item), + item + }); + }); + + db.onChangeWithCallback( + { + onChange: async () => { + if (this.closed || abortSignal.aborted) { + return; + } + // This fires for each change of the relevant tables + try { + if (this.reportFetching && !this.state.isFetching) { + await this.updateState({ isFetching: true }); + } + + const partialStateUpdate: Partial> = {}; + + // Always run the query if an underlying table has changed + const result = await watchOptions.query.execute({ + sql: compiledQuery.sql, + // Allows casting from ReadOnlyArray[unknown] to Array + // This allows simpler compatibility with PowerSync queries + parameters: [...compiledQuery.parameters], + db: this.options.db + }); + + if (abortSignal.aborted) { + return; + } + + if (this.reportFetching) { + partialStateUpdate.isFetching = false; + } + + if (this.state.isLoading) { + partialStateUpdate.isLoading = false; + } + + const { diff, hasChanged, map } = this.differentiate(result, currentMap); + // Update for future comparisons + currentMap = map; + + if (hasChanged) { + await this.iterateAsyncListenersWithError((l) => l.onDiff?.(diff)); + Object.assign(partialStateUpdate, { + data: diff.all + }); + } + + if (this.state.error) { + partialStateUpdate.error = null; + } + + if (Object.keys(partialStateUpdate).length > 0) { + await this.updateState(partialStateUpdate); + } + } catch (error: any) { + await this.updateState({ error }); + } + }, + onError: async (error) => { + await this.updateState({ error }); + } + }, + { + signal: abortSignal, + tables, + throttleMs: watchOptions.throttleMs, + triggerImmediate: true // used to emit the initial state + } + ); + } +} diff --git a/packages/shared-internals/src/client/watched/OnChangeQueryProcessor.ts b/packages/shared-internals/src/client/watched/OnChangeQueryProcessor.ts new file mode 100644 index 000000000..ac92c4e4a --- /dev/null +++ b/packages/shared-internals/src/client/watched/OnChangeQueryProcessor.ts @@ -0,0 +1,111 @@ +import { WatchedQueryComparator, WatchedQuerySettings } from '@powersync/common'; +import { + AbstractQueryProcessor, + AbstractQueryProcessorOptions, + LinkQueryOptions, + MutableWatchedQueryState +} from './AbstractQueryProcessor.js'; + +/** + * @internal + */ +export interface OnChangeQueryProcessorOptions extends AbstractQueryProcessorOptions< + Data, + WatchedQuerySettings +> { + comparator?: WatchedQueryComparator; +} + +/** + * Uses the PowerSync onChange event to trigger watched queries. + * Results are emitted on every change of the relevant tables. + * @internal + */ +export class OnChangeQueryProcessor extends AbstractQueryProcessor> { + constructor(protected options: OnChangeQueryProcessorOptions) { + super(options); + } + + /** + * @returns If the sets are equal + */ + protected checkEquality(current: Data, previous: Data): boolean { + // Use the provided comparator if available. Assume values are unique if not available. + return this.options.comparator?.checkEquality?.(current, previous) ?? false; + } + + protected async linkQuery(options: LinkQueryOptions): Promise { + const { db, watchOptions } = this.options; + const { abortSignal } = options; + + const compiledQuery = watchOptions.query.compile(); + const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[], { + tables: options.settings.triggerOnTables + }); + + db.onChangeWithCallback( + { + onChange: async () => { + if (this.closed || abortSignal.aborted) { + return; + } + // This fires for each change of the relevant tables + try { + if (this.reportFetching && !this.state.isFetching) { + await this.updateState({ isFetching: true }); + } + + const partialStateUpdate: Partial> & { data?: Data } = {}; + + // Always run the query if an underlying table has changed + const result = await watchOptions.query.execute({ + sql: compiledQuery.sql, + // Allows casting from ReadOnlyArray[unknown] to Array + // This allows simpler compatibility with PowerSync queries + parameters: [...compiledQuery.parameters], + db: this.options.db + }); + + if (abortSignal.aborted) { + return; + } + + if (this.reportFetching) { + partialStateUpdate.isFetching = false; + } + + if (this.state.isLoading) { + partialStateUpdate.isLoading = false; + } + + // Check if the result has changed + if (!this.checkEquality(result, this.state.data)) { + Object.assign(partialStateUpdate, { + data: result + }); + } + + if (this.state.error) { + partialStateUpdate.error = null; + } + + if (Object.keys(partialStateUpdate).length > 0) { + await this.updateState(partialStateUpdate); + } + } catch (error: any) { + await this.updateState({ error }); + } + }, + onError: async (error) => { + await this.updateState({ error }); + } + }, + { + signal: abortSignal, + tables, + throttleMs: watchOptions.throttleMs, + triggerImmediate: true // used to emit the initial state + } + ); + } +} diff --git a/packages/shared-internals/src/client/watched/WatchedQuery.ts b/packages/shared-internals/src/client/watched/WatchedQuery.ts new file mode 100644 index 000000000..62e51d1e2 --- /dev/null +++ b/packages/shared-internals/src/client/watched/WatchedQuery.ts @@ -0,0 +1,14 @@ +import { WatchedQueryOptions } from '@powersync/common'; + +/** + * @internal + */ +export const DEFAULT_WATCH_THROTTLE_MS = 30; + +/** + * @internal + */ +export const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions = { + throttleMs: DEFAULT_WATCH_THROTTLE_MS, + reportFetching: true +}; diff --git a/packages/shared-internals/src/constants.ts b/packages/shared-internals/src/constants.ts new file mode 100644 index 000000000..5a263eb27 --- /dev/null +++ b/packages/shared-internals/src/constants.ts @@ -0,0 +1,6 @@ +/** + * @internal The priority used by the core extension to indicate that a full sync was completed. + */ +export const FULL_SYNC_PRIORITY = 2147483647; + +export const MAX_OP_ID = '9223372036854775807'; diff --git a/packages/shared-internals/src/db/crud/SyncProgress.ts b/packages/shared-internals/src/db/crud/SyncProgress.ts new file mode 100644 index 000000000..2e56a1aa5 --- /dev/null +++ b/packages/shared-internals/src/db/crud/SyncProgress.ts @@ -0,0 +1,39 @@ +// (bucket, progress) pairs + +import { ProgressWithOperations, SyncProgress } from '@powersync/common'; +import { FULL_SYNC_PRIORITY } from '../../constants.js'; +import { DownloadProgress } from '../../client/sync/stream/core-instruction.js'; + +export class SyncProgressImpl implements SyncProgress { + totalOperations: number; + downloadedOperations: number; + downloadedFraction: number; + + constructor(protected internal: DownloadProgress) { + const untilCompletion = this.untilPriority(FULL_SYNC_PRIORITY); + + this.totalOperations = untilCompletion.totalOperations; + this.downloadedOperations = untilCompletion.downloadedOperations; + this.downloadedFraction = untilCompletion.downloadedFraction; + } + + untilPriority(priority: number): ProgressWithOperations { + let total = 0; + let downloaded = 0; + + for (const progress of Object.values(this.internal.buckets)) { + // Include higher-priority buckets, which are represented by lower numbers. + if (progress.priority <= priority) { + downloaded += progress.since_last; + total += progress.target_count - progress.at_last; + } + } + + let progress = total == 0 ? 0.0 : downloaded / total; + return { + totalOperations: total, + downloadedOperations: downloaded, + downloadedFraction: progress + }; + } +} diff --git a/packages/shared-internals/src/db/crud/SyncStatus.ts b/packages/shared-internals/src/db/crud/SyncStatus.ts new file mode 100644 index 000000000..40ccc4e8a --- /dev/null +++ b/packages/shared-internals/src/db/crud/SyncStatus.ts @@ -0,0 +1,206 @@ +import { + SyncDataFlowStatus, + SyncPriorityStatus, + SyncProgress, + SyncStatus, + SyncStreamDescription, + SyncStreamStatus, + SyncSubscriptionDescription +} from '@powersync/common'; +import { CoreStreamSubscription, CoreSyncStatus } from '../../client/sync/stream/core-instruction.js'; +import { SyncPriorityStatus as CoreSyncPriorityStatus } from '../../client/sync/stream/core-instruction.js'; +import { SyncProgressImpl } from './SyncProgress.js'; +import { FULL_SYNC_PRIORITY } from '../../constants.js'; + +export class SyncStatusSnapshot implements SyncStatus { + readonly dataFlowStatus: SyncDataFlowStatus; + + constructor( + readonly core: CoreSyncStatus | null, + dataFlow: SyncDataFlowStatus + ) { + this.dataFlowStatus = { + uploading: false, + ...dataFlow, + downloading: core?.downloading != null + }; + } + + get connected(): boolean { + return this.core?.connected ?? false; + } + + get connecting(): boolean { + return this.core?.connecting ?? false; + } + + get lastSyncedAt(): Date | undefined { + return this.statusForPriority(FULL_SYNC_PRIORITY)?.lastSyncedAt; + } + + get hasSynced(): boolean | undefined { + return this.statusForPriority(FULL_SYNC_PRIORITY)?.hasSynced; + } + + get syncStreams(): SyncStreamStatus[] | undefined { + return this.core?.streams.map((core) => new SyncStreamStatusView(this, core)); + } + + forStream(stream: SyncStreamDescription): SyncStreamStatus | undefined { + const asJson = JSON.stringify(stream.parameters); + const raw = this.core?.streams?.find((r) => r.name == stream.name && asJson == JSON.stringify(r.parameters)); + + return raw && new SyncStreamStatusView(this, raw); + } + + get priorityStatusEntries(): SyncPriorityStatus[] | undefined { + return this.core?.priority_status.map(priorityToJs); + } + + get downloadProgress(): SyncProgress | null { + const internalProgress = this.core?.downloading; + if (internalProgress == null) { + return null; + } + + return new SyncProgressImpl(internalProgress); + } + + statusForPriority(priority: number): SyncPriorityStatus | undefined { + const coreStatus = this.core?.priority_status; + if (coreStatus == null) return undefined; + + // priorityStatusEntries are sorted by ascending priorities (so higher numbers to lower numbers). + for (const known of coreStatus) { + // We look for the first entry that doesn't have a higher priority. + if (known.priority >= priority) { + return priorityToJs(known); + } + } + + // If we get to this point, no priority entry exists so we never synced a priority that low. + return { + priority, + lastSyncedAt: undefined, + hasSynced: false + }; + } + + isEqual(status: SyncStatus) { + /** + * By default Error object are serialized to an empty object. + * This replaces Errors with more useful information before serialization. + */ + const replacer = (_: string, value: any) => { + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: value.stack + }; + } + return value; + }; + + const options = { core: this.core, dataFlow: this.dataFlowStatus }; + const otherStatus = status as unknown as SyncStatusSnapshot; + const otherOptions = { + core: otherStatus.core, + dataFlow: otherStatus.dataFlowStatus + }; + + return JSON.stringify(options, replacer) == JSON.stringify(otherOptions, replacer); + } + + getMessage() { + const dataFlow = this.dataFlowStatus; + return `SyncStatus`; + } + + /** + * Serializes the SyncStatus instance to a plain object. + * + * @returns A plain object representation of the sync status + */ + toJSON(): unknown { + return { + connected: this.connected, + connecting: this.connecting, + dataFlow: { + ...this.dataFlowStatus, + uploadError: this.serializeError(this.dataFlowStatus.uploadError), + downloadError: this.serializeError(this.dataFlowStatus.downloadError) + }, + lastSyncedAt: this.lastSyncedAt, + hasSynced: this.hasSynced, + priorityStatusEntries: this.priorityStatusEntries + }; + } + + /** + * Not all errors are serializable over a MessagePort. E.g. some `DomExceptions` fail to be passed across workers. + * This explicitly serializes errors in the SyncStatus. + */ + protected serializeError(error?: Error) { + if (typeof error == 'undefined') { + return undefined; + } + return { + name: error.name, + message: error.message, + stack: error.stack + }; + } + + private static comparePriorities(a: SyncPriorityStatus, b: SyncPriorityStatus) { + return b.priority - a.priority; // Reverse because higher priorities have lower numbers + } +} + +function coreTimestampToDate(time: number | null): Date | undefined { + return time == null ? undefined : new Date(time * 1000); +} + +function priorityToJs(status: CoreSyncPriorityStatus): SyncPriorityStatus { + return { + priority: status.priority, + hasSynced: status.has_synced ?? undefined, + lastSyncedAt: coreTimestampToDate(status.last_synced_at) + }; +} + +class SyncStreamStatusView implements SyncStreamStatus { + subscription: SyncSubscriptionDescription; + + constructor( + private status: SyncStatusSnapshot, + private core: CoreStreamSubscription + ) { + this.subscription = { + name: core.name, + parameters: core.parameters, + active: core.active, + isDefault: core.is_default, + hasExplicitSubscription: core.has_explicit_subscription, + expiresAt: core.expires_at != null ? new Date(core.expires_at * 1000) : null, + hasSynced: core.last_synced_at != null, + lastSyncedAt: core.last_synced_at != null ? new Date(core.last_synced_at * 1000) : null + }; + } + + get progress() { + if (this.status.core?.downloading == null) { + // Don't make download progress public if we're not currently downloading. + return null; + } + + const { total, downloaded } = this.core.progress; + const progress = total == 0 ? 0.0 : downloaded / total; + + return { totalOperations: total, downloadedOperations: downloaded, downloadedFraction: progress }; + } + + get priority() { + return this.core.priority; + } +} diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts new file mode 100644 index 000000000..9ad44580e --- /dev/null +++ b/packages/shared-internals/src/index.ts @@ -0,0 +1,17 @@ +export * from './client/AbstractPowerSyncDatabase.js'; +export * from './client/sync/bucket/BucketStorageAdapter.js'; +export * from './client/sync/bucket/SqliteBucketStorage.js'; +export * from './client/sync/stream/AbstractRemote.js'; +export * from './client/sync/stream/AbstractStreamingSyncImplementation.js'; + +export * from './client/ConnectionManager.js'; + +export { MEMORY_TRIGGER_CLAIM_MANAGER } from './client/triggers/MemoryTriggerClaimManager.js'; +export { TriggerManagerImpl } from './client/triggers/TriggerManagerImpl.js'; +export * from './client/watched/DifferentialQueryProcessor.js'; +export * from './client/watched/OnChangeQueryProcessor.js'; + +export * from './utils/AbortOperation.js'; +export * from './utils/BaseObserver.js'; +export * from './utils/mutex.js'; +export type { SimpleAsyncIterator } from './utils/stream_transform.js'; diff --git a/packages/shared-internals/src/reexports.ts b/packages/shared-internals/src/reexports.ts new file mode 100644 index 000000000..f34f807ad --- /dev/null +++ b/packages/shared-internals/src/reexports.ts @@ -0,0 +1 @@ +// Declarations that all PowerSync SDKs should re-export. diff --git a/packages/common/src/utils/AbortOperation.ts b/packages/shared-internals/src/utils/AbortOperation.ts similarity index 100% rename from packages/common/src/utils/AbortOperation.ts rename to packages/shared-internals/src/utils/AbortOperation.ts diff --git a/packages/shared-internals/src/utils/BaseObserver.ts b/packages/shared-internals/src/utils/BaseObserver.ts new file mode 100644 index 000000000..dde41d0ee --- /dev/null +++ b/packages/shared-internals/src/utils/BaseObserver.ts @@ -0,0 +1,33 @@ +import { BaseListener, BaseObserverInterface } from '@powersync/common'; + +export class BaseObserver implements BaseObserverInterface { + protected listeners = new Set>(); + + constructor() {} + + dispose(): void { + this.listeners.clear(); + } + + /** + * Register a listener for updates to the PowerSync client. + */ + registerListener(listener: Partial): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + iterateListeners(cb: (listener: Partial) => any) { + for (const listener of this.listeners) { + cb(listener); + } + } + + async iterateAsyncListeners(cb: (listener: Partial) => Promise) { + for (let i of Array.from(this.listeners.values())) { + await cb(i); + } + } +} diff --git a/packages/common/src/utils/ControlledExecutor.ts b/packages/shared-internals/src/utils/ControlledExecutor.ts similarity index 100% rename from packages/common/src/utils/ControlledExecutor.ts rename to packages/shared-internals/src/utils/ControlledExecutor.ts diff --git a/packages/shared-internals/src/utils/MetaBaseObserver.ts b/packages/shared-internals/src/utils/MetaBaseObserver.ts new file mode 100644 index 000000000..fec979ef9 --- /dev/null +++ b/packages/shared-internals/src/utils/MetaBaseObserver.ts @@ -0,0 +1,65 @@ +import { + BaseListener, + ListenerCounts, + ListenerMetaManager, + MetaBaseObserverInterface, + MetaListener +} from '@powersync/common'; +import { BaseObserver } from './BaseObserver.js'; + +/** + * A BaseObserver that tracks the counts of listeners for each event type. + */ +export class MetaBaseObserver + extends BaseObserver + implements MetaBaseObserverInterface +{ + protected get listenerCounts(): ListenerCounts { + const counts = {} as Partial>; + let total = 0; + for (const listener of this.listeners) { + for (const key in listener) { + if (listener[key]) { + counts[key] = (counts[key] ?? 0) + 1; + total++; + } + } + } + return { + ...counts, + total + }; + } + + get listenerMeta(): ListenerMetaManager { + return { + counts: this.listenerCounts, + // Allows registering a meta listener that will be notified of changes in listener counts + registerListener: (listener: Partial>) => { + return this.metaListener.registerListener(listener); + } + }; + } + + protected metaListener: BaseObserver>; + + constructor() { + super(); + this.metaListener = new BaseObserver>(); + } + + registerListener(listener: Partial): () => void { + const dispose = super.registerListener(listener); + const updatedCount = this.listenerCounts; + this.metaListener.iterateListeners((l) => { + l.listenersChanged?.(updatedCount); + }); + return () => { + dispose(); + const updatedCount = this.listenerCounts; + this.metaListener.iterateListeners((l) => { + l.listenersChanged?.(updatedCount); + }); + }; + } +} diff --git a/packages/common/src/utils/async.ts b/packages/shared-internals/src/utils/async.ts similarity index 100% rename from packages/common/src/utils/async.ts rename to packages/shared-internals/src/utils/async.ts diff --git a/packages/shared-internals/src/utils/mutex.ts b/packages/shared-internals/src/utils/mutex.ts new file mode 100644 index 000000000..24d271023 --- /dev/null +++ b/packages/shared-internals/src/utils/mutex.ts @@ -0,0 +1,186 @@ +import { Queue } from './queue.js'; + +export type UnlockFn = () => void; + +/** + * An asynchronous semaphore implementation with associated items per lease. + */ +export class Semaphore { + // Available items that are not currently assigned to a waiter. + private readonly available: Queue; + + readonly size: number; + // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing + // aborted waiters from the middle of the list efficiently. + private firstWaiter?: SemaphoreWaitNode; + private lastWaiter?: SemaphoreWaitNode; + + constructor(elements: Iterable) { + this.available = new Queue(elements); + this.size = this.available.length; + } + + private addWaiter(requestedItems: number, onAcquire: () => void): SemaphoreWaitNode { + const node: SemaphoreWaitNode = { + isActive: true, + acquiredItems: [], + remainingItems: requestedItems, + onAcquire, + prev: this.lastWaiter + }; + if (this.lastWaiter) { + this.lastWaiter.next = node; + this.lastWaiter = node; + } else { + // First waiter + this.lastWaiter = this.firstWaiter = node; + } + + return node; + } + + private deactivateWaiter(waiter: SemaphoreWaitNode) { + const { prev, next } = waiter; + waiter.isActive = false; + + if (prev) prev.next = next; + if (next) next.prev = prev; + if (waiter == this.firstWaiter) this.firstWaiter = next; + if (waiter == this.lastWaiter) this.lastWaiter = prev; + } + + private requestPermits(amount: number, abort?: AbortSignal): Promise<{ items: T[]; release: UnlockFn }> { + if (amount <= 0 || amount > this.size) { + throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`); + } + + return new Promise((resolve, reject) => { + function rejectAborted() { + reject(abort?.reason ?? new Error('Semaphore acquire aborted')); + } + if (abort?.aborted) { + return rejectAborted(); + } + + let waiter: SemaphoreWaitNode; + + const markCompleted = () => { + const items = waiter.acquiredItems; + waiter.acquiredItems = []; // Avoid releasing items twice. + + for (const element of items) { + // Give to next waiter, if possible. + const nextWaiter = this.firstWaiter; + if (nextWaiter) { + nextWaiter.acquiredItems.push(element); + nextWaiter.remainingItems--; + if (nextWaiter.remainingItems == 0) { + nextWaiter.onAcquire(); + } + } else { + // No pending waiter, return lease into pool. + this.available.addLast(element); + } + } + }; + + const onAbort = () => { + abort?.removeEventListener('abort', onAbort); + + if (waiter.isActive) { + this.deactivateWaiter(waiter); + rejectAborted(); + } + }; + + const resolvePromise = () => { + this.deactivateWaiter(waiter); + abort?.removeEventListener('abort', onAbort); + + const items = waiter.acquiredItems; + resolve({ items, release: markCompleted }); + }; + + waiter = this.addWaiter(amount, resolvePromise); + + // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is + // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter). + while (!this.available.isEmpty && waiter.remainingItems > 0) { + waiter.acquiredItems.push(this.available.removeFirst()); + waiter.remainingItems--; + } + + if (waiter.remainingItems == 0) { + return resolvePromise(); + } + + abort?.addEventListener('abort', onAbort); + }); + } + + /** + * Requests a single item from the pool. + * + * The returned `release` callback must be invoked to return the item into the pool. + */ + async requestOne(abort?: AbortSignal): Promise<{ item: T; release: UnlockFn }> { + const { items, release } = await this.requestPermits(1, abort); + return { release, item: items[0] }; + } + + /** + * Requests access to all items from the pool. + * + * The returned `release` callback must be invoked to return items into the pool. + */ + requestAll(abort?: AbortSignal): Promise<{ items: T[]; release: UnlockFn }> { + return this.requestPermits(this.size, abort); + } +} + +interface SemaphoreWaitNode { + /** + * Whether the waiter is currently active (not aborted and not fullfilled). + */ + isActive: boolean; + acquiredItems: T[]; + remainingItems: number; + onAcquire: () => void; + prev?: SemaphoreWaitNode; + next?: SemaphoreWaitNode; +} + +/** + * An asynchronous mutex implementation. + */ +export class Mutex { + private inner = new Semaphore([null]); + + async acquire(abort?: AbortSignal): Promise { + const { release } = await this.inner.requestOne(abort); + return release; + } + + async runExclusive(fn: () => PromiseLike | T, abort?: AbortSignal): Promise { + const returnMutex = await this.acquire(abort); + + try { + return await fn(); + } finally { + returnMutex(); + } + } +} + +export function timeoutSignal(timeout: number): AbortSignal; + +export function timeoutSignal(timeout?: number): AbortSignal | undefined; + +export function timeoutSignal(timeout?: number): AbortSignal | undefined { + if (timeout == null) return; + if ('timeout' in AbortSignal) return AbortSignal.timeout(timeout); + + const controller = new AbortController(); + setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout); + return controller.signal; +} diff --git a/packages/common/src/utils/queue.ts b/packages/shared-internals/src/utils/queue.ts similarity index 100% rename from packages/common/src/utils/queue.ts rename to packages/shared-internals/src/utils/queue.ts diff --git a/packages/common/src/utils/stream_transform.ts b/packages/shared-internals/src/utils/stream_transform.ts similarity index 100% rename from packages/common/src/utils/stream_transform.ts rename to packages/shared-internals/src/utils/stream_transform.ts diff --git a/packages/common/tests/utils/async.test.ts b/packages/shared-internals/tests/utils/async.test.ts similarity index 100% rename from packages/common/tests/utils/async.test.ts rename to packages/shared-internals/tests/utils/async.test.ts diff --git a/packages/common/tests/utils/mutex.test.ts b/packages/shared-internals/tests/utils/mutex.test.ts similarity index 100% rename from packages/common/tests/utils/mutex.test.ts rename to packages/shared-internals/tests/utils/mutex.test.ts diff --git a/packages/common/tests/utils/queue.test.ts b/packages/shared-internals/tests/utils/queue.test.ts similarity index 100% rename from packages/common/tests/utils/queue.test.ts rename to packages/shared-internals/tests/utils/queue.test.ts diff --git a/packages/common/tests/utils/stream_transform.test.ts b/packages/shared-internals/tests/utils/stream_transform.test.ts similarity index 100% rename from packages/common/tests/utils/stream_transform.test.ts rename to packages/shared-internals/tests/utils/stream_transform.test.ts diff --git a/packages/shared-internals/tsconfig.json b/packages/shared-internals/tsconfig.json new file mode 100644 index 000000000..b48695af7 --- /dev/null +++ b/packages/shared-internals/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "types": ["node"], + "rootDir": "src", + "outDir": "./lib", + "lib": ["esnext"], + "declaration": true, + "module": "NodeNext", + "moduleResolution": "nodenext", + "preserveConstEnums": true, + "skipLibCheck": false, + "strictNullChecks": true + }, + "include": ["src/**/*"], + "references": [ + {"path": "../common"} + ] +} diff --git a/packages/web/package.json b/packages/web/package.json index 02dee0262..0382e8e50 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -71,6 +71,7 @@ }, "dependencies": { "@powersync/common": "workspace:*", + "@powersync/shared-internals": "workspace:*", "comlink": "catalog:", "commander": "^12.1.0" }, diff --git a/packages/web/src/db/PowerSyncDatabase.ts b/packages/web/src/db/PowerSyncDatabase.ts index dea2cf38a..c83a223f9 100644 --- a/packages/web/src/db/PowerSyncDatabase.ts +++ b/packages/web/src/db/PowerSyncDatabase.ts @@ -3,7 +3,6 @@ import { SqliteBucketStorage, StreamingSyncImplementation, TriggerManagerConfig, - Mutex, type BucketStorageAdapter, type PowerSyncBackendConnector, type PowerSyncCloseOptions, @@ -14,6 +13,7 @@ import { openDatabase, DBAdapter } from '@powersync/common'; +import { Mutex } from '@powersync/shared-internals'; import { getNavigatorLocks } from '../shared/navigator.js'; import { NAVIGATOR_TRIGGER_CLAIM_MANAGER } from './NavigatorTriggerClaimManager.js'; import { WebDBAdapter } from './adapters/WebDBAdapter.js'; diff --git a/packages/web/src/db/adapters/AsyncWebAdapter.ts b/packages/web/src/db/adapters/AsyncWebAdapter.ts index b667a9f36..626e2378a 100644 --- a/packages/web/src/db/adapters/AsyncWebAdapter.ts +++ b/packages/web/src/db/adapters/AsyncWebAdapter.ts @@ -3,11 +3,9 @@ import { DBAdapterDefaultMixin, DBAdapterListener, DBLockOptions, - LockContext, - Mutex, - Semaphore, - UnlockFn + LockContext } from '@powersync/common'; +import { Mutex, Semaphore, UnlockFn } from '@powersync/shared-internals'; import { SharedConnectionWorker, WebDBAdapter, WebDBAdapterConfiguration } from './WebDBAdapter.js'; import { DatabaseClient } from './wa-sqlite/DatabaseClient.js'; diff --git a/packages/web/src/db/adapters/SSRDBAdapter.ts b/packages/web/src/db/adapters/SSRDBAdapter.ts index 57e734ad4..c4a74ef34 100644 --- a/packages/web/src/db/adapters/SSRDBAdapter.ts +++ b/packages/web/src/db/adapters/SSRDBAdapter.ts @@ -5,10 +5,9 @@ import { DBLockOptions, LockContext, QueryResult, - Transaction, - Mutex, - timeoutSignal + Transaction } from '@powersync/common'; +import { Mutex, timeoutSignal } from '@powersync/shared-internals'; const MOCK_QUERY_RESPONSE: QueryResult = { rowsAffected: 0 diff --git a/packages/web/src/db/adapters/wa-sqlite/ConcurrentConnection.ts b/packages/web/src/db/adapters/wa-sqlite/ConcurrentConnection.ts index 97e91293c..8e5d2e1d7 100644 --- a/packages/web/src/db/adapters/wa-sqlite/ConcurrentConnection.ts +++ b/packages/web/src/db/adapters/wa-sqlite/ConcurrentConnection.ts @@ -1,4 +1,4 @@ -import { Mutex, UnlockFn } from '@powersync/common'; +import { Mutex, UnlockFn } from '@powersync/shared-internals'; import { RawSqliteConnection, RawWaSqliteDatabaseOptions } from './RawSqliteConnection.js'; /** diff --git a/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts index 499cb87eb..382105a94 100644 --- a/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts @@ -3,11 +3,11 @@ import { BaseObserver, LockOptions, LockType, - Mutex, StreamingSyncImplementation, SyncStatus, SyncStatusOptions } from '@powersync/common'; +import { Mutex } from '@powersync/shared-internals'; export class SSRStreamingSyncImplementation extends BaseObserver implements StreamingSyncImplementation { syncMutex: Mutex; diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 34f49cf3b..31d52b18c 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -12,13 +12,13 @@ import { SqliteBucketStorage, SubscribedStream, SyncStatus, - Mutex, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions, LogLevels, ResolvedSyncOptions } from '@powersync/common'; +import { Mutex } from '@powersync/shared-internals'; import * as Comlink from 'comlink'; import { WebRemote } from '../../db/sync/WebRemote.js'; import { diff --git a/packages/web/tests/mocks/MockWebRemote.ts b/packages/web/tests/mocks/MockWebRemote.ts index 25112b060..f4620a274 100644 --- a/packages/web/tests/mocks/MockWebRemote.ts +++ b/packages/web/tests/mocks/MockWebRemote.ts @@ -5,9 +5,9 @@ import { FetchImplementationProvider, PowerSyncLogger, RemoteConnector, - SimpleAsyncIterator, SocketSyncStreamOptions } from '@powersync/common'; +import { SimpleAsyncIterator } from '@powersync/shared-internals'; import { type BSON } from 'bson'; import { MockSyncService, setupMockServiceMessageHandler } from '../utils/MockSyncServiceWorker.js'; diff --git a/packages/web/tests/utils/MockStreamOpenFactory.ts b/packages/web/tests/utils/MockStreamOpenFactory.ts index fc2ecfc80..61e9c293c 100644 --- a/packages/web/tests/utils/MockStreamOpenFactory.ts +++ b/packages/web/tests/utils/MockStreamOpenFactory.ts @@ -8,9 +8,9 @@ import { PowerSyncDatabaseOptions, PowerSyncLogger, RemoteConnector, - SimpleAsyncIterator, SyncStreamOptions } from '@powersync/common'; +import { SimpleAsyncIterator } from '@powersync/shared-internals'; import { StreamingSyncLine } from '@powersync/common/internal/sync_protocol'; import { PowerSyncDatabase, WebPowerSyncDatabaseOptions, WebStreamingSyncImplementation } from '@powersync/web'; import { MockedFunction, vi } from 'vitest'; diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json index febc16cf8..333ec58b9 100644 --- a/packages/web/tsconfig.json +++ b/packages/web/tsconfig.json @@ -18,6 +18,9 @@ "references": [ { "path": "../common" + }, + { + "path": "../shared-internals" } ], "include": ["src/**/*", "tests/**/*", "package.json"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edb6a4025..965f71c91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,6 +251,9 @@ importers: '@powersync/common': specifier: workspace:^ version: link:../common + '@powersync/shared-internals': + specifier: workspace:* + version: link:../shared-internals devDependencies: '@powersync/sql-js': specifier: 0.0.9 @@ -346,6 +349,9 @@ importers: packages/capacitor: dependencies: + '@powersync/shared-internals': + specifier: workspace:* + version: link:../shared-internals '@powersync/web': specifier: workspace:^1.38.3 version: link:../web @@ -411,48 +417,12 @@ importers: '@microsoft/api-extractor': specifier: 'catalog:' version: 7.58.7(@types/node@24.10.13) - '@rollup/plugin-commonjs': - specifier: 'catalog:' - version: 29.0.2(rollup@4.59.0) - '@rollup/plugin-inject': - specifier: 'catalog:' - version: 5.0.5(rollup@4.59.0) - '@rollup/plugin-json': - specifier: 'catalog:' - version: 6.1.0(rollup@4.59.0) - '@rollup/plugin-node-resolve': - specifier: 'catalog:' - version: 16.0.3(rollup@4.59.0) '@types/node': specifier: 'catalog:' version: 24.10.13 '@types/uuid': specifier: 'catalog:' version: 9.0.8 - buffer: - specifier: ^6.0.3 - version: 6.0.3 - cross-fetch: - specifier: ^4.1.0 - version: 4.1.0(encoding@0.1.13) - estree-walker: - specifier: ^3.0.3 - version: 3.0.3 - magic-string: - specifier: ^0.30.21 - version: 0.30.21 - rollup: - specifier: 'catalog:' - version: 4.59.0 - rollup-plugin-dts: - specifier: 'catalog:' - version: 6.3.0(rollup@4.59.0)(typescript@6.0.3) - rsocket-core: - specifier: 1.0.0-alpha.3 - version: 1.0.0-alpha.3 - rsocket-websocket-client: - specifier: 1.0.0-alpha.3 - version: 1.0.0-alpha.3 packages/drizzle-driver: dependencies: @@ -536,6 +506,9 @@ importers: '@powersync/common': specifier: workspace:* version: link:../common + '@powersync/shared-internals': + specifier: workspace:* + version: link:../shared-internals comlink: specifier: 'catalog:' version: 4.4.2 @@ -661,6 +634,9 @@ importers: '@powersync/common': specifier: workspace:* version: link:../common + '@powersync/shared-internals': + specifier: workspace:* + version: link:../shared-internals devDependencies: '@op-engineering/op-sqlite': specifier: 'catalog:' @@ -749,6 +725,9 @@ importers: '@powersync/react': specifier: workspace:* version: link:../react + '@powersync/shared-internals': + specifier: workspace:* + version: link:../shared-internals devDependencies: '@craftzdog/react-native-buffer': specifier: ^6.0.5 @@ -799,6 +778,58 @@ importers: specifier: 3.2.1 version: 3.2.1 + packages/shared-internals: + dependencies: + event-iterator: + specifier: ^2.0.0 + version: 2.0.0 + devDependencies: + '@powersync/common': + specifier: workspace:* + version: link:../common + '@rollup/plugin-commonjs': + specifier: 'catalog:' + version: 29.0.2(rollup@4.59.0) + '@rollup/plugin-inject': + specifier: 'catalog:' + version: 5.0.5(rollup@4.59.0) + '@rollup/plugin-json': + specifier: 'catalog:' + version: 6.1.0(rollup@4.59.0) + '@rollup/plugin-node-resolve': + specifier: 'catalog:' + version: 16.0.3(rollup@4.59.0) + '@types/node': + specifier: 'catalog:' + version: 24.10.13 + '@types/uuid': + specifier: 'catalog:' + version: 9.0.8 + buffer: + specifier: ^6.0.3 + version: 6.0.3 + cross-fetch: + specifier: ^4.1.0 + version: 4.1.0(encoding@0.1.13) + estree-walker: + specifier: ^3.0.3 + version: 3.0.3 + magic-string: + specifier: ^0.30.21 + version: 0.30.21 + rollup: + specifier: 'catalog:' + version: 4.59.0 + rollup-plugin-dts: + specifier: 'catalog:' + version: 6.3.0(rollup@4.59.0)(typescript@6.0.3) + rsocket-core: + specifier: 1.0.0-alpha.3 + version: 1.0.0-alpha.3 + rsocket-websocket-client: + specifier: 1.0.0-alpha.3 + version: 1.0.0-alpha.3 + packages/tanstack-react-query: dependencies: '@powersync/common': @@ -877,6 +908,9 @@ importers: '@powersync/common': specifier: workspace:* version: link:../common + '@powersync/shared-internals': + specifier: workspace:* + version: link:../shared-internals comlink: specifier: 'catalog:' version: 4.4.2 diff --git a/tsconfig.json b/tsconfig.json index ef1cea6c3..ca9a62610 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,9 +25,9 @@ { "path": "./packages/node" }, - { - "path": "./packages/nuxt" - }, + // { + // "path": "./packages/nuxt" + // }, { "path": "./packages/powersync-op-sqlite" }, @@ -37,6 +37,9 @@ { "path": "./packages/react-native" }, + { + "path": "./packages/shared-internals" + }, { "path": "./packages/tanstack-react-query" }, From 12acd80aac51542b6cec4a980461c6ed9225f30b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 14:33:28 +0200 Subject: [PATCH 02/12] Fix more build errors --- .changeset/funny-deers-explode.md | 16 ++++ packages/adapter-sql-js/src/SQLJSAdapter.ts | 4 +- packages/capacitor/src/PowerSyncDatabase.ts | 38 ++++---- .../src/adapter/CapacitorSQLiteAdapter.ts | 3 +- .../src/sync/CapacitorSyncImplementation.ts | 2 +- .../src/client/CommonPowerSyncDatabase.ts | 5 ++ packages/common/src/db/crud/SyncStatus.ts | 10 +-- packages/node/src/db/PowerSyncDatabase.ts | 3 + packages/node/tests/sync.test.ts | 25 +++--- packages/node/tests/utils.ts | 13 +-- packages/nuxt/package.json | 3 +- .../usePowerSyncInspectorDiagnostics.ts | 21 ++--- .../src/runtime/utils/DynamicSchemaManager.ts | 2 +- .../runtime/utils/NuxtPowerSyncDatabase.ts | 67 +++++++------- .../runtime/utils/RustClientInterceptor.ts | 4 +- packages/nuxt/tsconfig.json | 5 +- .../src/db/OPSQLiteConnection.ts | 2 +- .../src/db/OPSqliteAdapter.ts | 3 +- .../react-native/src/db/PowerSyncDatabase.ts | 46 ++++++---- .../RNQSDBAdapter.ts | 2 +- .../bucket/ReactNativeBucketStorageAdapter.ts | 2 +- .../src/sync/stream/ReactNativeRemote.ts | 5 +- .../ReactNativeStreamingSyncImplementation.ts | 2 +- .../AbstractStreamingSyncImplementation.ts | 1 - .../src/db/crud/SyncStatus.ts | 28 +++--- packages/shared-internals/src/index.ts | 7 +- packages/tauri/guest-js/command.ts | 5 +- packages/tauri/guest-js/database.ts | 20 +++-- packages/tauri/guest-js/pool.ts | 2 +- packages/tauri/package.json | 3 +- packages/tauri/src/database.rs | 59 ++++++++----- packages/tauri/tsconfig.json | 3 + packages/vue/src/composables/powerSync.ts | 8 +- .../vue/src/composables/usePowerSyncStatus.ts | 2 +- packages/vue/src/composables/useStatus.ts | 4 +- .../src/db/NavigatorTriggerClaimManager.ts | 2 +- packages/web/src/db/PowerSyncDatabase.ts | 58 +++++++----- packages/web/src/db/adapters/SSRDBAdapter.ts | 12 +-- .../db/adapters/wa-sqlite/DatabaseClient.ts | 2 +- .../sync/SSRWebStreamingSyncImplementation.ts | 21 ++--- .../SharedWebStreamingSyncImplementation.ts | 15 ++-- packages/web/src/db/sync/WebRemote.ts | 5 +- .../db/sync/WebStreamingSyncImplementation.ts | 6 +- .../sync/AbstractSharedSyncClientProvider.ts | 5 +- .../worker/sync/SharedSyncImplementation.ts | 50 +++++------ packages/web/src/worker/sync/WorkerClient.ts | 3 +- packages/web/tests/crud.test.ts | 31 +++---- .../web/tests/mockSyncServiceExample.test.ts | 2 +- packages/web/tests/mocks/MockWebRemote.ts | 5 +- packages/web/tests/multiple_instances.test.ts | 4 +- packages/web/tests/offline.test.ts | 6 +- packages/web/tests/on_change.test.ts | 4 +- packages/web/tests/open.test.ts | 4 +- packages/web/tests/performance.test.ts | 4 +- packages/web/tests/schema.test.ts | 4 +- packages/web/tests/schemav2.test.ts | 4 +- packages/web/tests/stream.test.ts | 8 +- .../web/tests/utils/MockStreamOpenFactory.ts | 24 ++--- .../web/tests/utils/MockSyncServiceClient.ts | 2 +- packages/web/tests/watch.test.ts | 4 +- packages/web/tests/watchSchemaChange.test.ts | 6 +- pnpm-lock.yaml | 9 ++ tools/diagnostics-app/package.json | 1 + .../src/app/views/sync-diagnostics.tsx | 29 +++--- .../library/powersync/ConnectionManager.ts | 5 +- .../library/powersync/DynamicSchemaManager.ts | 10 +-- .../powersync/RustClientInterceptor.ts | 10 +-- .../src/routes/_authenticated.tsx | 88 ++++++++----------- tools/diagnostics-app/tsconfig.json | 7 +- .../powersynctests/src/tests/queries.test.ts | 4 +- tsconfig.json | 6 +- 71 files changed, 470 insertions(+), 415 deletions(-) create mode 100644 .changeset/funny-deers-explode.md diff --git a/.changeset/funny-deers-explode.md b/.changeset/funny-deers-explode.md new file mode 100644 index 000000000..4e8206ef5 --- /dev/null +++ b/.changeset/funny-deers-explode.md @@ -0,0 +1,16 @@ +--- +'@powersync/react-native': major +'@powersync/web': major +'@powersync/capacitor': minor +'@powersync/common': minor +'@powersync/tauri-plugin': minor +'@powersync/node': minor +'@powersync/nuxt': minor +'@powersync/vue': minor +--- + +Rename `AbstractPowerSyncDatabase` to `CommonPowerSyncDatabase`, make it a TypeScript interface. + +`CrudEntry` is now a TypeScript interface, remove it's constructor and `CrudEntry.fromRow`. + +Remove `DataFlowStatus.downloading`. Use `SyncStatus.downloading` instead. diff --git a/packages/adapter-sql-js/src/SQLJSAdapter.ts b/packages/adapter-sql-js/src/SQLJSAdapter.ts index d276cea44..a1aa919d2 100644 --- a/packages/adapter-sql-js/src/SQLJSAdapter.ts +++ b/packages/adapter-sql-js/src/SQLJSAdapter.ts @@ -1,9 +1,7 @@ import { BaseListener, - BaseObserver, BatchedUpdateNotification, ConnectionPool, - ControlledExecutor, createConsoleLogger, DBAdapter, DBAdapterDefaultMixin, @@ -19,7 +17,7 @@ import { SQLOpenOptions, Transaction } from '@powersync/common'; -import { Mutex, timeoutSignal } from '@powersync/shared-internals'; +import { Mutex, timeoutSignal, BaseObserver, ControlledExecutor } from '@powersync/shared-internals'; // This uses a pure JS version which avoids the need for WebAssembly, which is not supported in React Native. import SQLJs from '@powersync/sql-js/dist/sql-asm.js'; diff --git a/packages/capacitor/src/PowerSyncDatabase.ts b/packages/capacitor/src/PowerSyncDatabase.ts index e675c838c..3b89f72de 100644 --- a/packages/capacitor/src/PowerSyncDatabase.ts +++ b/packages/capacitor/src/PowerSyncDatabase.ts @@ -1,31 +1,27 @@ import { Capacitor } from '@capacitor/core'; import { - CreateSyncImplementationOptions, + CommonPowerSyncDatabase, DBAdapter, LogLevels, - MEMORY_TRIGGER_CLAIM_MANAGER, openDatabase, PowerSyncBackendConnector, - StreamingSyncImplementation, + PowerSyncDatabaseConstructor, SyncOptions, SyncStreamConnectionMethod, - TriggerManagerConfig, - PowerSyncDatabase as WebPowerSyncDatabase, - WebSQLOpenOptions + WebPowerSyncDatabase, + WebPowerSyncDatabaseOptions } from '@powersync/web'; import { CapacitorSQLiteAdapter } from './adapter/CapacitorSQLiteAdapter.js'; import { CapacitorRemote } from './sync/CapacitorRemote.js'; import { CapacitorStreamingSyncImplementation } from './sync/CapacitorSyncImplementation.js'; +import { + CreateSyncImplementationOptions, + MEMORY_TRIGGER_CLAIM_MANAGER, + StreamingSyncImplementation, + TriggerManagerConfig +} from '@powersync/shared-internals'; -/** - * PowerSyncDatabase class for managing database connections and sync implementations. - * This extends the WebPowerSyncDatabase to provide platform-specific implementations - * for Capacitor environments (iOS and Android). - * - * @experimental - * @alpha - */ -export class PowerSyncDatabase extends WebPowerSyncDatabase { +class CapacitorPowerSyncDatabase extends WebPowerSyncDatabase { /** * Connects to stream of events from the PowerSync instance. * {@link PowerSyncConnectionOptions#connectionMethod} defaults to WebSocket connection on Web platforms @@ -137,3 +133,15 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase { } } } + +/** + * PowerSyncDatabase class for managing database connections and sync implementations. + * This extends the WebPowerSyncDatabase to provide platform-specific implementations + * for Capacitor environments (iOS and Android). + * + * @experimental + * @alpha + */ +export const PowerSyncDatabase: PowerSyncDatabaseConstructor = CapacitorPowerSyncDatabase; + +export interface PowerSyncDatabase extends CommonPowerSyncDatabase {} diff --git a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts index cc1f94984..5f3442554 100644 --- a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts +++ b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts @@ -2,7 +2,6 @@ import { CapacitorSQLite, SQLiteConnection, SQLiteDBConnection } from '@capacito import { Capacitor } from '@capacitor/core'; import { - BaseObserver, BatchedUpdateNotification, ConnectionPool, DBAdapter, @@ -12,7 +11,7 @@ import { LockContext, QueryResult } from '@powersync/web'; -import { Mutex, timeoutSignal } from '@powersync/shared-internals'; +import { BaseObserver, Mutex, timeoutSignal } from '@powersync/shared-internals'; import { PowerSyncCore } from '../plugin/PowerSyncCore.js'; import { messageForErrorCode } from '../plugin/PowerSyncPlugin.js'; import { CapacitorSQLiteOpenFactoryOptions, DEFAULT_SQLITE_OPTIONS } from './CapacitorSQLiteOpenFactory.js'; diff --git a/packages/capacitor/src/sync/CapacitorSyncImplementation.ts b/packages/capacitor/src/sync/CapacitorSyncImplementation.ts index c9696397e..535d631df 100644 --- a/packages/capacitor/src/sync/CapacitorSyncImplementation.ts +++ b/packages/capacitor/src/sync/CapacitorSyncImplementation.ts @@ -1,4 +1,4 @@ -import { AbstractStreamingSyncImplementation, LockOptions, LockType } from '@powersync/web'; +import { AbstractStreamingSyncImplementation, LockOptions, LockType } from '@powersync/shared-internals'; import { Mutex } from '@powersync/shared-internals'; type MutexMap = { diff --git a/packages/common/src/client/CommonPowerSyncDatabase.ts b/packages/common/src/client/CommonPowerSyncDatabase.ts index 5c76bd251..40273f783 100644 --- a/packages/common/src/client/CommonPowerSyncDatabase.ts +++ b/packages/common/src/client/CommonPowerSyncDatabase.ts @@ -126,6 +126,11 @@ export interface PowerSyncDatabaseConstructor { new (options: Options): CommonPowerSyncDatabase; } +/** + * @deprecated Use {@link CommonPowerSyncDatabase} instead. + */ +export type AbstractPowerSyncDatabase = CommonPowerSyncDatabase; + /** * @public */ diff --git a/packages/common/src/db/crud/SyncStatus.ts b/packages/common/src/db/crud/SyncStatus.ts index 8cad9271a..8336066fc 100644 --- a/packages/common/src/db/crud/SyncStatus.ts +++ b/packages/common/src/db/crud/SyncStatus.ts @@ -5,11 +5,6 @@ import { ProgressWithOperations, SyncProgress } from './SyncProgress.js'; * @public */ export type SyncDataFlowStatus = Partial<{ - /** - * true if actively downloading changes. - * This is only true when {@link connected} is also true. - */ - downloading: boolean; /** * true if uploading changes. */ @@ -54,6 +49,11 @@ export interface SyncStatus { */ get connecting(): boolean; + /** + * Whether the PowerSync SDK is currently downloading data from the connected PowerSync service. + */ + get downloading(): boolean; + /** * Time that a last sync has fully completed, if any. * This timestamp is reset to null after a restart of the PowerSync service. diff --git a/packages/node/src/db/PowerSyncDatabase.ts b/packages/node/src/db/PowerSyncDatabase.ts index 5b6535f72..a342b0cd1 100644 --- a/packages/node/src/db/PowerSyncDatabase.ts +++ b/packages/node/src/db/PowerSyncDatabase.ts @@ -1,5 +1,6 @@ import { BasePowerSyncDatabaseOptions, + CommonPowerSyncDatabase, DatabaseSource, DBAdapter, openDatabase, @@ -85,3 +86,5 @@ class NodePowerSyncDatabase extends AbstractPowerSyncDatabase = NodePowerSyncDatabase; + +export interface PowerSyncDatabase extends CommonPowerSyncDatabase {} diff --git a/packages/node/tests/sync.test.ts b/packages/node/tests/sync.test.ts index ff30ccc4b..c38337c74 100644 --- a/packages/node/tests/sync.test.ts +++ b/packages/node/tests/sync.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, vi } from 'vitest'; import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, PowerSyncLogger, ProgressWithOperations, Schema, @@ -16,7 +16,8 @@ import { TestConnector, waitForSyncStatus } from './utils.js'; -import { BucketChecksum, OplogEntryJSON } from '@powersync/common/internal/sync_protocol'; +import { BucketChecksum, OplogEntryJSON } from '@powersync/shared-internals/internal/sync_protocol'; +import { AbstractPowerSyncDatabase } from '@powersync/shared-internals'; const defaultConnectOptions: SyncOptions = { // This might help with test stability/timeouts if a retry is needed. @@ -104,7 +105,7 @@ describe('Sync', () => { // Replicate what we'd see on the web when switching connections in the shared sync worker: The sync client would // suddenly see a database without an active sync iteration. await database.execute('SELECT powersync_control(?, null)', ['stop']); - database.syncStreamImplementation!.markConnectionMayHaveChanged(); + (database as AbstractPowerSyncDatabase).syncStreamImplementation!.markConnectionMayHaveChanged(); await database.waitForStatus((s) => !s.connected); await vi.waitFor(() => expect(syncService.connectedListeners).toHaveLength(1)); }); @@ -152,7 +153,7 @@ function defineSyncTests(bson: boolean) { await vi.waitFor(() => expect(syncService.connectedListeners).toHaveLength(1)); // We want connected: true once we have a connection await vi.waitFor(() => connectCompleted); - expect(database.currentStatus.dataFlowStatus.downloading).toBeFalsy(); + expect(database.currentStatus.downloading).toBeFalsy(); syncService.pushLine({ checkpoint: { @@ -161,7 +162,7 @@ function defineSyncTests(bson: boolean) { } }); - await vi.waitFor(() => expect(database.currentStatus.dataFlowStatus.downloading).toBeTruthy()); + await vi.waitFor(() => expect(database.currentStatus.downloading).toBeTruthy()); }); mockSyncServiceTest('does not set uploading status without local writes', async ({ syncService }) => { @@ -181,7 +182,7 @@ function defineSyncTests(bson: boolean) { buckets: [bucket('a', 10)] } }); - await vi.waitFor(() => expect(database.currentStatus.dataFlowStatus.downloading).toBeTruthy()); + await vi.waitFor(() => expect(database.currentStatus.downloading).toBeTruthy()); }); describe('reports progress', () => { @@ -738,7 +739,7 @@ function defineSyncTests(bson: boolean) { } }); - await powersync.syncStreamImplementation!.waitUntilStatusMatches((status) => { + await powersync.waitForStatus((status) => { return status.statusForPriority(prio).hasSynced === true; }); await new Promise((r) => setTimeout(r)); @@ -836,7 +837,7 @@ function defineSyncTests(bson: boolean) { buckets: [bucket('a', 2)] } }); - await vi.waitFor(() => powersync.currentStatus.dataFlowStatus.downloading == true); + await vi.waitFor(() => powersync.currentStatus.downloading == true); syncService.pushLine({ data: { bucket: 'a', @@ -852,7 +853,7 @@ function defineSyncTests(bson: boolean) { } }); syncService.pushLine({ checkpoint_complete: { last_op_id: '2' } }); - await vi.waitFor(() => powersync.currentStatus.dataFlowStatus.downloading == false); + await vi.waitFor(() => powersync.currentStatus.downloading == false); expect((await query.next()).value.rows._array).toStrictEqual([]); }); @@ -915,7 +916,7 @@ function defineSyncTests(bson: boolean) { buckets: [bucket('a', 2)] } }); - await vi.waitFor(() => powersync.currentStatus.dataFlowStatus.downloading == true); + await vi.waitFor(() => powersync.currentStatus.downloading == true); syncService.pushLine({ data: { bucket: 'a', @@ -931,7 +932,7 @@ function defineSyncTests(bson: boolean) { } }); syncService.pushLine({ checkpoint_complete: { last_op_id: '2' } }); - await vi.waitFor(() => powersync.currentStatus.dataFlowStatus.downloading == false); + await vi.waitFor(() => powersync.currentStatus.downloading == false); expect((await query.next()).value.rows._array).toStrictEqual([]); }); @@ -1042,7 +1043,7 @@ function defineSyncTests(bson: boolean) { } async function waitForProgress( - database: AbstractPowerSyncDatabase, + database: CommonPowerSyncDatabase, total: [number, number], forPriorities: [number, [number, number]][] = [] ) { diff --git a/packages/node/tests/utils.ts b/packages/node/tests/utils.ts index 13649ea84..81cffeb3f 100644 --- a/packages/node/tests/utils.ts +++ b/packages/node/tests/utils.ts @@ -3,10 +3,13 @@ import os from 'node:os'; import path from 'node:path'; import { ReadableStream, TransformStream } from 'node:stream/web'; -import { BucketChecksum, StreamingSyncCheckpoint, StreamingSyncLine } from '@powersync/common/internal/sync_protocol'; +import { + BucketChecksum, + StreamingSyncCheckpoint, + StreamingSyncLine +} from '@powersync/shared-internals/internal/sync_protocol'; import { onTestFinished, test } from 'vitest'; import { - AbstractPowerSyncDatabase, NodePowerSyncDatabaseOptions, PowerSyncBackendConnector, PowerSyncCredentials, @@ -17,7 +20,7 @@ import { column } from '../lib/index.js'; import { BSON } from 'bson'; -import { createConsoleLogger, LogLevels } from '@powersync/common'; +import { CommonPowerSyncDatabase, createConsoleLogger, LogLevels } from '@powersync/common'; import { NodeSQLOpenOptions } from '../src/db/options.js'; export async function createTempDir() { @@ -214,7 +217,7 @@ export class TestConnector implements PowerSyncBackendConnector { token: 'test' }; } - async uploadData(database: AbstractPowerSyncDatabase): Promise { + async uploadData(database: CommonPowerSyncDatabase): Promise { const tx = await database.getNextCrudTransaction(); await tx?.complete(); this.uploadDataInvocations++; @@ -222,7 +225,7 @@ export class TestConnector implements PowerSyncBackendConnector { } export function waitForSyncStatus( - database: AbstractPowerSyncDatabase, + database: CommonPowerSyncDatabase, matcher: (status: SyncStatus) => boolean ): Promise { return new Promise((resolve, reject) => { diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index c824ce1ac..cc7a59bea 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -67,7 +67,8 @@ "@journeyapps/wa-sqlite": "^1.7.0", "@powersync/kysely-driver": "workspace:^1.3.3", "@powersync/vue": "workspace:^0.5.1", - "@powersync/web": "workspace:^1.38.3" + "@powersync/web": "workspace:^1.38.3", + "@powersync/shared-internals": "workspace:^1.0.0" }, "peerDependenciesMeta": { "@powersync/kysely-driver": { diff --git a/packages/nuxt/src/runtime/composables/usePowerSyncInspectorDiagnostics.ts b/packages/nuxt/src/runtime/composables/usePowerSyncInspectorDiagnostics.ts index a4588524e..1762a9dcf 100644 --- a/packages/nuxt/src/runtime/composables/usePowerSyncInspectorDiagnostics.ts +++ b/packages/nuxt/src/runtime/composables/usePowerSyncInspectorDiagnostics.ts @@ -1,6 +1,6 @@ import { usePowerSync, useStatus } from '@powersync/vue'; import type { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, PowerSyncBackendConnector, SyncOptions, SyncStatus, @@ -10,6 +10,7 @@ import type { ComputedRef, Ref } from 'vue'; import { ref, computed, readonly, onMounted, onUnmounted } from 'vue'; import { computedAsync } from '@vueuse/core'; import { usePowerSyncInspector } from './usePowerSyncInspector'; +import type { NuxtDatabaseImplementation } from '../utils/NuxtPowerSyncDatabase'; // queries const BUCKETS_QUERY = ` @@ -105,7 +106,7 @@ type ReadonlyRef = Readonly>; * Uses named types so the signature stays readable in docs and IDE. */ export interface UsePowerSyncInspectorDiagnosticsReturn { - db: Ref | undefined; + db: Ref | undefined; connector: ComputedRef; connectionOptions: ComputedRef; isDiagnosticSchemaSetup: ReadonlyRef; @@ -235,7 +236,7 @@ function formatBytes(bytes: number, decimals = 2) { * ``` */ export function usePowerSyncInspectorDiagnostics(): UsePowerSyncInspectorDiagnosticsReturn { - const db = usePowerSync(); + const db = usePowerSync() as Ref; const syncStatus = useStatus(); const { getCurrentSchemaManager } = usePowerSyncInspector(); @@ -247,12 +248,12 @@ export function usePowerSyncInspectorDiagnostics(): UsePowerSyncInspectorDiagnos const hasSynced = ref(syncStatus.value?.hasSynced || false); const isConnected = ref(syncStatus.value?.connected || false); const isSyncing = ref(false); - const isDownloading = ref(syncStatus.value?.dataFlowStatus.downloading || false); + const isDownloading = ref(syncStatus.value?.downloading || false); const isUploading = ref(syncStatus.value?.dataFlowStatus.uploading || false); const lastSyncedAt = ref(syncStatus.value?.lastSyncedAt || null); const uploadError = ref(syncStatus.value?.dataFlowStatus.uploadError || null); const downloadError = ref(syncStatus.value?.dataFlowStatus.downloadError || null); - const downloadProgressDetails = ref(syncStatus.value?.dataFlowStatus.downloadProgress || null); + const downloadProgressDetails = ref(syncStatus.value?.downloadProgress || null); const bucketRows = ref(null); const tableRows = ref(null); @@ -295,7 +296,7 @@ export function usePowerSyncInspectorDiagnostics(): UsePowerSyncInspectorDiagnos // functions const clearData = async () => { - await db.value?.syncStreamImplementation?.disconnect(); + await db.value?.disconnect(); const connector = db.value.connector; const connectionOptions = db.value.connectionOptions as SyncOptions; await db.value?.disconnectAndClear(); @@ -313,7 +314,7 @@ export function usePowerSyncInspectorDiagnostics(): UsePowerSyncInspectorDiagnos uploadQueueStats.value = await db.value?.getUploadQueueStats(true); - if (synced_at != null && !syncStatus.value?.dataFlowStatus.downloading) { + if (synced_at != null && !syncStatus.value?.downloading) { // These are potentially expensive queries - do not run during initial sync bucketRows.value = await db.value.getAll(BUCKETS_QUERY); tableRows.value = await db.value.getAll(TABLES_QUERY); @@ -342,12 +343,12 @@ export function usePowerSyncInspectorDiagnostics(): UsePowerSyncInspectorDiagnos // Update reactive status hasSynced.value = !!newStatus.hasSynced; isConnected.value = !!newStatus.connected; - isDownloading.value = !!newStatus.dataFlowStatus.downloading; + isDownloading.value = !!newStatus.downloading; isUploading.value = !!newStatus.dataFlowStatus.uploading; lastSyncedAt.value = newStatus.lastSyncedAt || null; uploadError.value = newStatus.dataFlowStatus.uploadError || null; downloadError.value = newStatus.dataFlowStatus.downloadError || null; - downloadProgressDetails.value = newStatus.dataFlowStatus.downloadProgress || null; + downloadProgressDetails.value = newStatus.downloadProgress || null; if ( newStatus?.hasSynced === undefined || @@ -356,7 +357,7 @@ export function usePowerSyncInspectorDiagnostics(): UsePowerSyncInspectorDiagnos hasSynced.value = newStatus?.priorityStatusEntries.every((entry) => entry.hasSynced) ?? false; } - if (newStatus?.dataFlowStatus.downloading || newStatus?.dataFlowStatus.uploading) { + if (newStatus?.downloading || newStatus?.dataFlowStatus.uploading) { isSyncing.value = true; } else { isSyncing.value = false; diff --git a/packages/nuxt/src/runtime/utils/DynamicSchemaManager.ts b/packages/nuxt/src/runtime/utils/DynamicSchemaManager.ts index 8e9fed972..e4899191f 100644 --- a/packages/nuxt/src/runtime/utils/DynamicSchemaManager.ts +++ b/packages/nuxt/src/runtime/utils/DynamicSchemaManager.ts @@ -2,7 +2,7 @@ import type { DBAdapter } from '@powersync/web'; import { Column, ColumnType, Schema, Table } from '@powersync/web'; import { DiagnosticsAppSchema as AppSchema } from './AppSchema'; import { JsSchemaGenerator } from './JsSchemaGenerator'; -import type { SyncDataBucketJSON } from '@powersync/common/internal/sync_protocol'; +import type { SyncDataBucketJSON } from '@powersync/shared-internals/internal/sync_protocol'; /** * Record fields from downloaded data, then build a schema from it. diff --git a/packages/nuxt/src/runtime/utils/NuxtPowerSyncDatabase.ts b/packages/nuxt/src/runtime/utils/NuxtPowerSyncDatabase.ts index d25ed3580..f4e28f618 100644 --- a/packages/nuxt/src/runtime/utils/NuxtPowerSyncDatabase.ts +++ b/packages/nuxt/src/runtime/utils/NuxtPowerSyncDatabase.ts @@ -1,18 +1,20 @@ import { - PowerSyncDatabase, + WebPowerSyncDatabase, Schema, SharedWebStreamingSyncImplementation, WebRemote, WebStreamingSyncImplementation, type DisconnectAndClearOptions, type PowerSyncBackendConnector, - type StreamingSyncImplementation, type WebPowerSyncDatabaseOptions, type WebDBAdapter, LogLevels, - type CreateSyncImplementationOptions, - type SyncOptions + type SyncOptions, + type CommonPowerSyncDatabase, + type PowerSyncDatabaseConstructor, + PowerSyncDatabase } from '@powersync/web'; +import { type StreamingSyncImplementation, type CreateSyncImplementationOptions } from '@powersync/shared-internals'; import type { DynamicSchemaManager } from './DynamicSchemaManager'; import { usePowerSyncInspector } from '../composables/usePowerSyncInspector'; import { useDiagnosticsLogger } from '../composables/useDiagnosticsLogger'; @@ -21,32 +23,7 @@ import { shallowRef, type ShallowRef } from 'vue'; import { useRuntimeConfig } from '#app'; import { RustClientInterceptor } from './RustClientInterceptor'; -/** - * An extended PowerSync database class that includes diagnostic capabilities for use with the PowerSync Inspector. - * - * This class automatically configures diagnostics when `useDiagnostics: true` is set in the module configuration. - * It provides enhanced VFS support, schema management, and logging capabilities for the inspector. - * - * @example - * ```typescript - * import { NuxtPowerSyncDatabase } from '@powersync/nuxt' - * - * const db = new NuxtPowerSyncDatabase({ - * database: { - * dbFilename: 'your-db-filename.sqlite', - * }, - * schema: yourSchema, - * }) - * ``` - * - * @remarks - * - When diagnostics are enabled, automatically uses cooperative sync VFS for improved compatibility - * - Stores connector internally for inspector access - * - Integrates with dynamic schema management for inspector features - * - Automatically configures logging when diagnostics are enabled - * - When diagnostics are disabled, behaves like a standard `PowerSyncDatabase` - */ -export class NuxtPowerSyncDatabase extends PowerSyncDatabase { +export class NuxtDatabaseImplementation extends WebPowerSyncDatabase { private schemaManager!: DynamicSchemaManager; private _connector: PowerSyncBackendConnector | null = null; private useDiagnostics: boolean = false; @@ -159,3 +136,33 @@ export class NuxtPowerSyncDatabase extends PowerSyncDatabase { await super.disconnectAndClear(options); } } + +/** + * An extended PowerSync database class that includes diagnostic capabilities for use with the PowerSync Inspector. + * + * This class automatically configures diagnostics when `useDiagnostics: true` is set in the module configuration. + * It provides enhanced VFS support, schema management, and logging capabilities for the inspector. + * + * @example + * ```typescript + * import { NuxtPowerSyncDatabase } from '@powersync/nuxt' + * + * const db = new NuxtPowerSyncDatabase({ + * database: { + * dbFilename: 'your-db-filename.sqlite', + * }, + * schema: yourSchema, + * }) + * ``` + * + * @remarks + * - When diagnostics are enabled, automatically uses cooperative sync VFS for improved compatibility + * - Stores connector internally for inspector access + * - Integrates with dynamic schema management for inspector features + * - Automatically configures logging when diagnostics are enabled + * - When diagnostics are disabled, behaves like a standard `PowerSyncDatabase` + */ +export const NuxtPowerSyncDatabase: PowerSyncDatabaseConstructor = + NuxtDatabaseImplementation; + +export interface NuxtPowerSyncDatabase extends CommonPowerSyncDatabase {} diff --git a/packages/nuxt/src/runtime/utils/RustClientInterceptor.ts b/packages/nuxt/src/runtime/utils/RustClientInterceptor.ts index 2602c92da..7559a3c23 100644 --- a/packages/nuxt/src/runtime/utils/RustClientInterceptor.ts +++ b/packages/nuxt/src/runtime/utils/RustClientInterceptor.ts @@ -1,9 +1,9 @@ import type { ColumnType, PowerSyncDatabase, DBAdapter } from '@powersync/web'; import { type BSON } from 'bson'; -import { AbstractPowerSyncDatabase, PowerSyncControlCommand, SqliteBucketStorage } from '@powersync/web'; +import { AbstractPowerSyncDatabase, PowerSyncControlCommand, SqliteBucketStorage } from '@powersync/shared-internals'; import type { DynamicSchemaManager } from './DynamicSchemaManager'; import type { ShallowRef } from 'vue'; -import type { BucketChecksum, Checkpoint, StreamingSyncLine } from '@powersync/common/internal/sync_protocol'; +import type { BucketChecksum, Checkpoint, StreamingSyncLine } from '@powersync/shared-internals/internal/sync_protocol'; /** * Tracks per-byte and per-operation progress for the Rust client. diff --git a/packages/nuxt/tsconfig.json b/packages/nuxt/tsconfig.json index a160ef0bf..c8cea65d1 100644 --- a/packages/nuxt/tsconfig.json +++ b/packages/nuxt/tsconfig.json @@ -17,6 +17,9 @@ }, { "path": "../web" - } + }, + { + "path": "../shared-internals" + }, ] } \ No newline at end of file diff --git a/packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts b/packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts index 80c249aee..6f60bb563 100644 --- a/packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts +++ b/packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts @@ -1,6 +1,5 @@ import { DB, SQLBatchTuple, UpdateHookOperation } from '@op-engineering/op-sqlite'; import { - BaseObserver, BatchedUpdateNotification, DBAdapterListener, DBGetUtilsDefaultMixin, @@ -10,6 +9,7 @@ import { SqlExecutor, UpdateNotification } from '@powersync/common'; +import { BaseObserver } from '@powersync/shared-internals'; export type OPSQLiteConnectionOptions = { baseDB: DB; diff --git a/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts b/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts index 7fe9c87f0..28d238bdb 100644 --- a/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts +++ b/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts @@ -1,6 +1,5 @@ import { getDylibPath, open, type DB } from '@op-engineering/op-sqlite'; import { - BaseObserver, ConnectionPool, DBAdapter, DBAdapterDefaultMixin, @@ -9,7 +8,7 @@ import { QueryResult, Transaction } from '@powersync/common'; -import { timeoutSignal, Semaphore } from '@powersync/shared-internals'; +import { timeoutSignal, Semaphore, BaseObserver } from '@powersync/shared-internals'; import { Platform } from 'react-native'; import { OPSQLiteConnection } from './OPSQLiteConnection'; import { SqliteOptions } from './SqliteOptions'; diff --git a/packages/react-native/src/db/PowerSyncDatabase.ts b/packages/react-native/src/db/PowerSyncDatabase.ts index 8db272c04..7e32c27df 100644 --- a/packages/react-native/src/db/PowerSyncDatabase.ts +++ b/packages/react-native/src/db/PowerSyncDatabase.ts @@ -1,33 +1,23 @@ import { - AbstractPowerSyncDatabase, - AbstractStreamingSyncImplementation, - BucketStorageAdapter, - CreateSyncImplementationOptions, + CommonPowerSyncDatabase, DBAdapter, openDatabase, PowerSyncBackendConnector, + PowerSyncDatabaseConstructor, PowerSyncDatabaseOptions } from '@powersync/common'; +import { + AbstractPowerSyncDatabase, + AbstractStreamingSyncImplementation, + BucketStorageAdapter, + CreateSyncImplementationOptions +} from '@powersync/shared-internals'; import { ReactNativeRemote } from '../sync/stream/ReactNativeRemote'; import { ReactNativeStreamingSyncImplementation } from '../sync/stream/ReactNativeStreamingSyncImplementation'; import { ReactNativeBucketStorageAdapter } from './../sync/bucket/ReactNativeBucketStorageAdapter'; import { ReactNativeQuickSqliteOpenFactory } from './adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory'; -/** - * A PowerSync database which provides SQLite functionality - * which is automatically synced. - * - * @example - * ```typescript - * export const db = new PowerSyncDatabase({ - * schema: AppSchema, - * database: { - * dbFilename: 'example.db' - * } - * }); - * ``` - */ -export class PowerSyncDatabase extends AbstractPowerSyncDatabase { +class ReactNativePowerSyncDatabase extends AbstractPowerSyncDatabase { constructor(options: PowerSyncDatabaseOptions) { super(options); } @@ -64,3 +54,21 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase = ReactNativePowerSyncDatabase; + +export interface PowerSyncDatabase extends CommonPowerSyncDatabase {} diff --git a/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts b/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts index 1df89bbb9..81d8e7006 100644 --- a/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts +++ b/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts @@ -1,5 +1,4 @@ import { - BaseObserver, DBAdapter, DBAdapterListener, LockContext as PowerSyncLockContext, @@ -12,6 +11,7 @@ import { } from '@powersync/common'; import type { QuickSQLiteConnection, LockContext as RNQSLockContext } from '@journeyapps/react-native-quick-sqlite'; import { QueryResult, SqlExecutor } from '@powersync/common'; +import { BaseObserver } from '@powersync/shared-internals'; class RNQSConnectionPool extends BaseObserver implements ConnectionPool { constructor( diff --git a/packages/react-native/src/sync/bucket/ReactNativeBucketStorageAdapter.ts b/packages/react-native/src/sync/bucket/ReactNativeBucketStorageAdapter.ts index e3d470269..a62d4166d 100644 --- a/packages/react-native/src/sync/bucket/ReactNativeBucketStorageAdapter.ts +++ b/packages/react-native/src/sync/bucket/ReactNativeBucketStorageAdapter.ts @@ -1,4 +1,4 @@ -import { PowerSyncControlCommand, SqliteBucketStorage } from '@powersync/common'; +import { PowerSyncControlCommand, SqliteBucketStorage } from '@powersync/shared-internals'; export class ReactNativeBucketStorageAdapter extends SqliteBucketStorage { control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise { diff --git a/packages/react-native/src/sync/stream/ReactNativeRemote.ts b/packages/react-native/src/sync/stream/ReactNativeRemote.ts index 14d2c2f83..1528d4482 100644 --- a/packages/react-native/src/sync/stream/ReactNativeRemote.ts +++ b/packages/react-native/src/sync/stream/ReactNativeRemote.ts @@ -1,13 +1,12 @@ +import { LogLevels, PowerSyncLogger } from '@powersync/common'; import { AbstractRemote, AbstractRemoteOptions, FetchImplementation, FetchImplementationProvider, - LogLevels, - PowerSyncLogger, RemoteConnector, SyncStreamOptions -} from '@powersync/common'; +} from '@powersync/shared-internals'; import { Platform } from 'react-native'; // @ts-expect-error import { TextDecoder } from 'text-encoding'; diff --git a/packages/react-native/src/sync/stream/ReactNativeStreamingSyncImplementation.ts b/packages/react-native/src/sync/stream/ReactNativeStreamingSyncImplementation.ts index babbdbdba..e042839cb 100644 --- a/packages/react-native/src/sync/stream/ReactNativeStreamingSyncImplementation.ts +++ b/packages/react-native/src/sync/stream/ReactNativeStreamingSyncImplementation.ts @@ -3,7 +3,7 @@ import { AbstractStreamingSyncImplementationOptions, LockOptions, LockType -} from '@powersync/common'; +} from '@powersync/shared-internals'; import { Mutex } from '@powersync/shared-internals'; /** diff --git a/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index 1046d9aff..1446c7fa9 100644 --- a/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -96,7 +96,6 @@ export interface StreamingSyncImplementation disconnect(): Promise; getWriteCheckpoint: () => Promise; isConnected: boolean; - syncStatus: SyncStatus; triggerCrudUpload: () => void; waitForReady(): Promise; waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise; diff --git a/packages/shared-internals/src/db/crud/SyncStatus.ts b/packages/shared-internals/src/db/crud/SyncStatus.ts index 40ccc4e8a..229d27bd9 100644 --- a/packages/shared-internals/src/db/crud/SyncStatus.ts +++ b/packages/shared-internals/src/db/crud/SyncStatus.ts @@ -21,8 +21,7 @@ export class SyncStatusSnapshot implements SyncStatus { ) { this.dataFlowStatus = { uploading: false, - ...dataFlow, - downloading: core?.downloading != null + ...dataFlow }; } @@ -34,6 +33,10 @@ export class SyncStatusSnapshot implements SyncStatus { return this.core?.connecting ?? false; } + get downloading(): boolean { + return this.core?.downloading != null; + } + get lastSyncedAt(): Date | undefined { return this.statusForPriority(FULL_SYNC_PRIORITY)?.lastSyncedAt; } @@ -114,7 +117,7 @@ export class SyncStatusSnapshot implements SyncStatus { getMessage() { const dataFlow = this.dataFlowStatus; - return `SyncStatus`; + return `SyncStatus`; } /** @@ -122,18 +125,14 @@ export class SyncStatusSnapshot implements SyncStatus { * * @returns A plain object representation of the sync status */ - toJSON(): unknown { + toJSON(): SyncStatusJson { return { - connected: this.connected, - connecting: this.connecting, + core: this.core, dataFlow: { ...this.dataFlowStatus, uploadError: this.serializeError(this.dataFlowStatus.uploadError), downloadError: this.serializeError(this.dataFlowStatus.downloadError) - }, - lastSyncedAt: this.lastSyncedAt, - hasSynced: this.hasSynced, - priorityStatusEntries: this.priorityStatusEntries + } }; } @@ -151,10 +150,6 @@ export class SyncStatusSnapshot implements SyncStatus { stack: error.stack }; } - - private static comparePriorities(a: SyncPriorityStatus, b: SyncPriorityStatus) { - return b.priority - a.priority; // Reverse because higher priorities have lower numbers - } } function coreTimestampToDate(time: number | null): Date | undefined { @@ -169,6 +164,11 @@ function priorityToJs(status: CoreSyncPriorityStatus): SyncPriorityStatus { }; } +export interface SyncStatusJson { + core: CoreSyncStatus | null; + dataFlow: SyncDataFlowStatus; +} + class SyncStreamStatusView implements SyncStreamStatus { subscription: SyncSubscriptionDescription; diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts index 9ad44580e..07253e1fd 100644 --- a/packages/shared-internals/src/index.ts +++ b/packages/shared-internals/src/index.ts @@ -1,17 +1,20 @@ export * from './client/AbstractPowerSyncDatabase.js'; export * from './client/sync/bucket/BucketStorageAdapter.js'; +export * from './client/sync/bucket/CrudEntry.js'; export * from './client/sync/bucket/SqliteBucketStorage.js'; export * from './client/sync/stream/AbstractRemote.js'; export * from './client/sync/stream/AbstractStreamingSyncImplementation.js'; - +export * from './client/sync/stream/core-instruction.js'; +export * from './db/crud/SyncStatus.js'; export * from './client/ConnectionManager.js'; export { MEMORY_TRIGGER_CLAIM_MANAGER } from './client/triggers/MemoryTriggerClaimManager.js'; -export { TriggerManagerImpl } from './client/triggers/TriggerManagerImpl.js'; +export * from './client/triggers/TriggerManagerImpl.js'; export * from './client/watched/DifferentialQueryProcessor.js'; export * from './client/watched/OnChangeQueryProcessor.js'; export * from './utils/AbortOperation.js'; export * from './utils/BaseObserver.js'; +export * from './utils/ControlledExecutor.js'; export * from './utils/mutex.js'; export type { SimpleAsyncIterator } from './utils/stream_transform.js'; diff --git a/packages/tauri/guest-js/command.ts b/packages/tauri/guest-js/command.ts index 5935510b0..6fb4e5dd7 100644 --- a/packages/tauri/guest-js/command.ts +++ b/packages/tauri/guest-js/command.ts @@ -1,4 +1,5 @@ -import { SyncStatusOptions, SyncStreamDescription, SyncStreamSubscribeOptions } from '@powersync/common'; +import { SyncStreamDescription, SyncStreamSubscribeOptions } from '@powersync/common'; +import { SyncStatusJson } from '@powersync/shared-internals'; import { invoke } from '@tauri-apps/api/core'; export interface OpenDatabase { @@ -67,7 +68,7 @@ export type CommandResult = | { CreatedHandle: number } | { ExecuteSqlResult: ExecuteSqlResult } | { ExecuteBatchResult: ExecuteBatchResult } - | { SyncStatus: SyncStatusOptions } + | { SyncStatus: SyncStatusJson } | 'Void'; export async function powersyncCommand(command: Command): Promise { diff --git a/packages/tauri/guest-js/database.ts b/packages/tauri/guest-js/database.ts index 0dfefeb48..6e63f9f72 100644 --- a/packages/tauri/guest-js/database.ts +++ b/packages/tauri/guest-js/database.ts @@ -1,13 +1,8 @@ import { - AbstractPowerSyncDatabase, BasePowerSyncDatabaseOptions, - BucketStorageAdapter, DBAdapter, PowerSyncCloseOptions, SQLOpenOptions, - StreamingSyncImplementation, - SyncStatus, - SyncStatusOptions, SyncStream, SyncStreamSubscribeOptions, SyncStreamSubscription @@ -16,6 +11,13 @@ import { LateHandle, RustDatabaseAdapter } from './pool'; import { CreatedDatabase, powersyncCommand } from './command'; import { listen, UnlistenFn } from '@tauri-apps/api/event'; import { join } from '@tauri-apps/api/path'; +import { + AbstractPowerSyncDatabase, + BucketStorageAdapter, + StreamingSyncImplementation, + SyncStatusJson, + SyncStatusSnapshot +} from '@powersync/shared-internals'; export interface TauriPowerSyncOpenOptions extends BasePowerSyncDatabaseOptions { database: TauriSQLOpenOptions; @@ -151,15 +153,15 @@ export class PowerSyncTauriDatabase extends AbstractPowerSyncDatabase l.statusChanged?.(this.currentStatus)); } protected async resolveOfflineSyncStatus(): Promise { const result = await powersyncCommand({ GetSyncStatus: this.rustHandle }); - const status = (result as any).SyncStatus as SyncStatusOptions; + const status = (result as any).SyncStatus as SyncStatusJson; this.updateSyncStatusFromRust(status); } @@ -182,7 +184,7 @@ export class PowerSyncTauriDatabase extends AbstractPowerSyncDatabase(`sync-status:${event_key}`, (event) => { + this.syncStatusListener = await listen(`sync-status:${event_key}`, (event) => { this.updateSyncStatusFromRust(event.payload); }); diff --git a/packages/tauri/guest-js/pool.ts b/packages/tauri/guest-js/pool.ts index 916236e2e..8d27423bf 100644 --- a/packages/tauri/guest-js/pool.ts +++ b/packages/tauri/guest-js/pool.ts @@ -1,5 +1,4 @@ import { - BaseObserver, ConnectionPool, DBAdapterDefaultMixin, DBAdapterListener, @@ -9,6 +8,7 @@ import { QueryResult, SqlExecutor } from '@powersync/common'; +import { BaseObserver } from '@powersync/shared-internals'; import { ExecuteBatchResult, ExecuteSqlResult, powersyncCommand, SqliteValue } from './command'; /** diff --git a/packages/tauri/package.json b/packages/tauri/package.json index 6ae3d9d16..3cd33220e 100644 --- a/packages/tauri/package.json +++ b/packages/tauri/package.json @@ -35,7 +35,8 @@ }, "dependencies": { "@tauri-apps/api": "^2.0.0", - "@powersync/common": "workspace:" + "@powersync/common": "workspace:", + "@powersync/shared-internals": "workspace:" }, "devDependencies": { "typescript": "catalog:", diff --git a/packages/tauri/src/database.rs b/packages/tauri/src/database.rs index e9edb96fe..0dbbae080 100644 --- a/packages/tauri/src/database.rs +++ b/packages/tauri/src/database.rs @@ -2,6 +2,7 @@ use powersync::error::PowerSyncError; use powersync::{PowerSyncDatabase, SyncStatusData}; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; +use std::collections::HashMap; use std::sync::Arc; use tauri::{AppHandle, Emitter, Runtime}; use tokio::task::JoinHandle; @@ -88,14 +89,43 @@ impl Serialize for SerializableSyncStatus { S: Serializer, { let status = self.0.as_ref(); - // Note: This must match SyncStatusOptions in common/src/db/crud/SyncStatus.ts - let mut inner = serializer.serialize_struct("SyncStatusOptions", 4)?; - inner.serialize_field("connected", &status.is_connected())?; - inner.serialize_field("connecting", &status.is_connecting())?; + // Note: This must match SyncStatusJson in shared-internals/src/db/crud/SyncStatus.ts + let mut inner = serializer.serialize_struct("SyncStatusJson", 2)?; + inner.serialize_field("core", &SerializableCoreSyncStatus(status))?; inner.serialize_field("dataFlow", &SerializableDataFlowStatus(status))?; - // TODO: lastSyncedAt, hasSynced and priorityStatusEntries are not available from the Rust - // SDK since it's centered around Sync Streams. - inner.serialize_field("clientImplementation", "RUST")?; + + inner.end() + } +} + +struct SerializableCoreSyncStatus<'a>(&'a SyncStatusData); + +impl Serialize for SerializableCoreSyncStatus<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize, Default)] + struct EmptyDownloadProgress { + buckets: HashMap, + } + + let status = self.0; + // Note: This must match CoreSyncStatus in shared-internals/src/client/sync/stream/core-instruction.ts + let mut inner = serializer.serialize_struct("CoreSyncStatus", 5)?; + inner.serialize_field("connected", &status.is_connected())?; + inner.serialize_field("connecting", &&status.is_connecting())?; + + // Note: Priority status and download progress is not available outside of streams. + inner.serialize_field::<[()]>("priority_status", &[])?; + if status.is_downloading() { + let progress = Some(EmptyDownloadProgress::default()); + inner.serialize_field("downloading", &progress) + } else { + inner.serialize_field("downloading", &None::<()>) + }?; + + inner.serialize_field("streams", status.internal_streams())?; inner.end() } @@ -108,13 +138,9 @@ impl Serialize for SerializableDataFlowStatus<'_> { where S: Serializer, { - #[derive(Serialize)] - struct EmptyDownloadProgress {} - let status = self.0; // Note: This must match SyncDataFlowStatus in common/src/db/crud/SyncStatus.ts - let mut inner = serializer.serialize_struct("DataFlowOptions", 10)?; - inner.serialize_field("downloading", &status.is_downloading())?; + let mut inner = serializer.serialize_struct("DataFlowOptions", 3)?; inner.serialize_field("uploading", &status.is_uploading())?; inner.serialize_field( "downloadError", @@ -124,15 +150,6 @@ impl Serialize for SerializableDataFlowStatus<'_> { "uploadError", &status.upload_error().map(SerializeAsJavaScriptError), )?; - inner.serialize_field( - "downloadProgress", - if status.is_downloading() { - &Some(EmptyDownloadProgress {}) - } else { - &None - }, - )?; - inner.serialize_field("internalStreamSubscriptions", status.internal_streams())?; inner.end() } diff --git a/packages/tauri/tsconfig.json b/packages/tauri/tsconfig.json index 97825bf5c..2f09f7173 100644 --- a/packages/tauri/tsconfig.json +++ b/packages/tauri/tsconfig.json @@ -15,6 +15,9 @@ "references": [ { "path": "../common" + }, + { + "path": "../shared-internals" } ] } diff --git a/packages/vue/src/composables/powerSync.ts b/packages/vue/src/composables/powerSync.ts index 88df46924..06f6503e4 100644 --- a/packages/vue/src/composables/powerSync.ts +++ b/packages/vue/src/composables/powerSync.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase } from '@powersync/common'; +import { CommonPowerSyncDatabase } from '@powersync/common'; import { App, MaybeRef, Ref, hasInjectionContext, inject, provide, shallowRef, toRaw, toValue } from 'vue'; import { setupTopLevelWarningMessage } from './messages.js'; @@ -11,7 +11,7 @@ const POWERSYNC_KEY = Symbol('POWERSYNC_KEY'); * * Needs to be installed on a Vue instance using `app.use()`. */ -export function createPowerSyncPlugin(powerSyncPluginOptions: { database: MaybeRef }) { +export function createPowerSyncPlugin(powerSyncPluginOptions: { database: MaybeRef }) { const install = (app: App) => { app.provide(POWERSYNC_KEY, shallowRef(toRaw(toValue(powerSyncPluginOptions.database)))); }; @@ -25,7 +25,7 @@ export function createPowerSyncPlugin(powerSyncPluginOptions: { database: MaybeR * * If the key parameter is provided, the client will be provided under that key instead of the default PowerSync key. */ -export function providePowerSync(database: MaybeRef, key: string | undefined = undefined) { +export function providePowerSync(database: MaybeRef, key: string | undefined = undefined) { provide(key || POWERSYNC_KEY, shallowRef(toRaw(toValue(database)))); } @@ -42,7 +42,7 @@ export const usePowerSync = (key: string | undefined = undefined) => { if (!hasInjectionContext()) { throw setupTopLevelWarningMessage; } - const powerSync = inject | undefined>(key || POWERSYNC_KEY); + const powerSync = inject | undefined>(key || POWERSYNC_KEY); if (!powerSync) { console.warn('[PowerSync warn]: No PowerSync client found.'); diff --git a/packages/vue/src/composables/usePowerSyncStatus.ts b/packages/vue/src/composables/usePowerSyncStatus.ts index 1b70ae6cb..33d6954f7 100644 --- a/packages/vue/src/composables/usePowerSyncStatus.ts +++ b/packages/vue/src/composables/usePowerSyncStatus.ts @@ -7,7 +7,7 @@ import { usePowerSync } from './powerSync.js'; */ export const usePowerSyncStatus = (): Ref => { const powerSync = usePowerSync(); - const status = ref(new SyncStatus({})) as Ref; + const status = ref(); if (!powerSync) { return status; diff --git a/packages/vue/src/composables/useStatus.ts b/packages/vue/src/composables/useStatus.ts index 06c85529d..c9f350482 100644 --- a/packages/vue/src/composables/useStatus.ts +++ b/packages/vue/src/composables/useStatus.ts @@ -16,7 +16,7 @@ import { usePowerSync } from './powerSync.js'; */ export const useStatus = (): Ref => { const powerSync = usePowerSync(); - const status = ref(new SyncStatus({})); + const status = ref(); if (!powerSync) { return status as any; @@ -37,5 +37,5 @@ export const useStatus = (): Ref => { }); }); - return status as any; + return status; }; diff --git a/packages/web/src/db/NavigatorTriggerClaimManager.ts b/packages/web/src/db/NavigatorTriggerClaimManager.ts index e8259e411..f390b8cb6 100644 --- a/packages/web/src/db/NavigatorTriggerClaimManager.ts +++ b/packages/web/src/db/NavigatorTriggerClaimManager.ts @@ -1,4 +1,4 @@ -import { TriggerClaimManager } from '@powersync/common'; +import { TriggerClaimManager } from '@powersync/shared-internals'; import { getNavigatorLocks } from '../shared/navigator.js'; /** diff --git a/packages/web/src/db/PowerSyncDatabase.ts b/packages/web/src/db/PowerSyncDatabase.ts index c83a223f9..6b4286bba 100644 --- a/packages/web/src/db/PowerSyncDatabase.ts +++ b/packages/web/src/db/PowerSyncDatabase.ts @@ -1,19 +1,23 @@ import { - AbstractPowerSyncDatabase, - SqliteBucketStorage, - StreamingSyncImplementation, - TriggerManagerConfig, - type BucketStorageAdapter, type PowerSyncBackendConnector, type PowerSyncCloseOptions, LogLevels, - CreateSyncImplementationOptions, BasePowerSyncDatabaseOptions, DatabaseSource, openDatabase, - DBAdapter + DBAdapter, + PowerSyncDatabaseConstructor, + CommonPowerSyncDatabase } from '@powersync/common'; -import { Mutex } from '@powersync/shared-internals'; +import { + AbstractPowerSyncDatabase, + BucketStorageAdapter, + CreateSyncImplementationOptions, + Mutex, + SqliteBucketStorage, + StreamingSyncImplementation, + TriggerManagerConfig +} from '@powersync/shared-internals'; import { getNavigatorLocks } from '../shared/navigator.js'; import { NAVIGATOR_TRIGGER_CLAIM_MANAGER } from './NavigatorTriggerClaimManager.js'; import { WebDBAdapter } from './adapters/WebDBAdapter.js'; @@ -61,26 +65,15 @@ export interface WebSyncOptions { } /** - * A PowerSync database which provides SQLite functionality - * which is automatically synced. - * - * @example - * ```typescript - * export const db = new PowerSyncDatabase({ - * schema: AppSchema, - * database: { - * dbFilename: 'example.db' - * } - * }); - * ``` + * @internal Use {@link PowerSyncDatabase} instead, this class is only used by other SDKs also needing web support. */ -export class PowerSyncDatabase extends AbstractPowerSyncDatabase { +export class WebPowerSyncDatabase extends AbstractPowerSyncDatabase { static SHARED_MUTEX = new Mutex(); protected resolvedOpenOptions: WebSpecificOpenOptions; protected enableBroadcastLogs: boolean; - constructor(options: WebPowerSyncDatabaseOptions, database?: () => DBAdapter) { + constructor(options: WebPowerSyncDatabaseOptions) { const resolvedOpenOptions = resolveAndValidateOptions('database' in options ? options.database : {}); super(options); @@ -158,7 +151,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase(cb: () => Promise) { if (this.resolvedOpenOptions.ssrMode) { - return PowerSyncDatabase.SHARED_MUTEX.runExclusive(cb); + return WebPowerSyncDatabase.SHARED_MUTEX.runExclusive(cb); } return getNavigatorLocks().request(`lock-${this.database.name}`, cb); } @@ -183,7 +176,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase = WebPowerSyncDatabase; +export interface PowerSyncDatabase extends CommonPowerSyncDatabase {} diff --git a/packages/web/src/db/adapters/SSRDBAdapter.ts b/packages/web/src/db/adapters/SSRDBAdapter.ts index c4a74ef34..dd9a18cbc 100644 --- a/packages/web/src/db/adapters/SSRDBAdapter.ts +++ b/packages/web/src/db/adapters/SSRDBAdapter.ts @@ -1,13 +1,5 @@ -import { - BaseObserver, - DBAdapterListener, - DBAdapter, - DBLockOptions, - LockContext, - QueryResult, - Transaction -} from '@powersync/common'; -import { Mutex, timeoutSignal } from '@powersync/shared-internals'; +import { DBAdapterListener, DBAdapter, DBLockOptions, LockContext, QueryResult, Transaction } from '@powersync/common'; +import { BaseObserver, Mutex, timeoutSignal } from '@powersync/shared-internals'; const MOCK_QUERY_RESPONSE: QueryResult = { rowsAffected: 0 diff --git a/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts b/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts index 7b62858a1..1a91c31e0 100644 --- a/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts +++ b/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts @@ -7,7 +7,6 @@ import { SqlExecutor, DBGetUtilsDefaultMixin, BatchedUpdateNotification, - BaseObserver, ConnectionClosedError, SQLOpenOptions } from '@powersync/common'; @@ -16,6 +15,7 @@ import { ClientConnectionView } from './DatabaseServer.js'; import { RawQueryResult } from './RawSqliteConnection.js'; import * as Comlink from 'comlink'; import type { ConnectToMultiDatabaseServerOptions } from '../../../worker/db/MultiDatabaseServer.js'; +import { BaseObserver } from '@powersync/shared-internals'; export interface OpenWorkerConnection { connect(config: ConnectToMultiDatabaseServerOptions): Promise; diff --git a/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts index 382105a94..fa90ba2bb 100644 --- a/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts @@ -1,13 +1,13 @@ +import { SyncStatus } from '@powersync/common'; import { AbstractStreamingSyncImplementationOptions, BaseObserver, + Mutex, + StreamingSyncImplementation, LockOptions, LockType, - StreamingSyncImplementation, - SyncStatus, - SyncStatusOptions -} from '@powersync/common'; -import { Mutex } from '@powersync/shared-internals'; + SyncStatusSnapshot +} from '@powersync/shared-internals'; export class SSRStreamingSyncImplementation extends BaseObserver implements StreamingSyncImplementation { syncMutex: Mutex; @@ -15,13 +15,11 @@ export class SSRStreamingSyncImplementation extends BaseObserver implements Stre isConnected: boolean; lastSyncedAt?: Date | undefined; - syncStatus: SyncStatus; - constructor(options: AbstractStreamingSyncImplementationOptions) { + constructor() { super(); this.syncMutex = new Mutex(); this.crudMutex = new Mutex(); - this.syncStatus = new SyncStatus({}); this.isConnected = false; } @@ -47,13 +45,6 @@ export class SSRStreamingSyncImplementation extends BaseObserver implements Stre */ async waitForReady() {} - /** - * This will never resolve in SSR Mode. - */ - async waitForStatus(status: SyncStatusOptions) { - return this.waitUntilStatusMatches(() => false); - } - /** * This will never resolve in SSR Mode. */ diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 350c13c1f..51d65cb07 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -1,10 +1,4 @@ -import { - LogRecord, - PowerSyncCredentials, - ResolvedSyncOptions, - SubscribedStream, - SyncStatusOptions -} from '@powersync/common'; +import { LogRecord, PowerSyncCredentials, ResolvedSyncOptions } from '@powersync/common'; import * as Comlink from 'comlink'; import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider.js'; import { ManualSharedSyncPayload, SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation.js'; @@ -15,6 +9,7 @@ import { WebStreamingSyncImplementationOptions } from './WebStreamingSyncImplementation.js'; import { generateTabCloseSignal } from '../../shared/tab_close_signal.js'; +import { SubscribedStream, SyncStatusJson } from '@powersync/shared-internals'; /** * The shared worker will trigger methods on this side of the message port @@ -23,7 +18,7 @@ import { generateTabCloseSignal } from '../../shared/tab_close_signal.js'; class SharedSyncClientProvider extends AbstractSharedSyncClientProvider { constructor( protected options: WebStreamingSyncImplementationOptions, - public statusChanged: (status: SyncStatusOptions) => void, + public statusChanged: (status: SyncStatusJson) => void, protected webDB: WebDBAdapter ) { super(); @@ -124,8 +119,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem */ this.clientProvider = new SharedSyncClientProvider( this.webOptions, - (status) => { - this.updateSyncStatus(status); + ({ core, dataFlow }) => { + this.updateSyncStatus(core, dataFlow); }, options.db ); diff --git a/packages/web/src/db/sync/WebRemote.ts b/packages/web/src/db/sync/WebRemote.ts index aee30ea75..f9378ac95 100644 --- a/packages/web/src/db/sync/WebRemote.ts +++ b/packages/web/src/db/sync/WebRemote.ts @@ -1,12 +1,11 @@ +import { LogLevels, PowerSyncLogger } from '@powersync/common'; import { AbstractRemote, AbstractRemoteOptions, FetchImplementation, FetchImplementationProvider, - LogLevels, - PowerSyncLogger, RemoteConnector -} from '@powersync/common'; +} from '@powersync/shared-internals'; import { getUserAgentInfo } from './userAgent.js'; diff --git a/packages/web/src/db/sync/WebStreamingSyncImplementation.ts b/packages/web/src/db/sync/WebStreamingSyncImplementation.ts index d36bc5ef3..c474d31ac 100644 --- a/packages/web/src/db/sync/WebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/WebStreamingSyncImplementation.ts @@ -1,10 +1,10 @@ +import { LogLevels } from '@powersync/common'; import { AbstractStreamingSyncImplementation, AbstractStreamingSyncImplementationOptions, LockOptions, - LockType, - LogLevels -} from '@powersync/common'; + LockType +} from '@powersync/shared-internals'; import { getNavigatorLocks } from '../../shared/navigator.js'; export interface WebStreamingSyncImplementationOptions extends AbstractStreamingSyncImplementationOptions { diff --git a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts index be4dfabc5..9caae095d 100644 --- a/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts +++ b/packages/web/src/worker/sync/AbstractSharedSyncClientProvider.ts @@ -1,4 +1,5 @@ -import type { LogRecord, PowerSyncCredentials, SyncStatusOptions } from '@powersync/common'; +import type { LogRecord, PowerSyncCredentials } from '@powersync/common'; +import { SyncStatusJson } from '@powersync/shared-internals'; /** * The client side port should provide these methods. @@ -7,7 +8,7 @@ export abstract class AbstractSharedSyncClientProvider { abstract fetchCredentials(): Promise; abstract invalidateCredentials(): void; abstract uploadCrud(): Promise; - abstract statusChanged(status: SyncStatusOptions): void; + abstract statusChanged(status: SyncStatusJson): void; abstract getDBWorkerPort(): Promise; abstract log(record: LogRecord): void; diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 31d52b18c..86d8cbbcc 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -1,7 +1,4 @@ import { - AbortOperation, - BaseObserver, - ConnectionManager, ConnectionPool, DBAdapter, DBAdapterDefaultMixin, @@ -9,16 +6,24 @@ import { DBLockOptions, LockContext, PowerSyncBackendConnector, + SyncStatus, + LogLevels, + ResolvedSyncOptions, + SyncDataFlowStatus +} from '@powersync/common'; +import { + AbortOperation, + BaseObserver, + ConnectionManager, SqliteBucketStorage, SubscribedStream, - SyncStatus, type StreamingSyncImplementation, type StreamingSyncImplementationListener, - type SyncStatusOptions, - LogLevels, - ResolvedSyncOptions -} from '@powersync/common'; -import { Mutex } from '@powersync/shared-internals'; + Mutex, + CoreSyncStatus, + SyncStatusSnapshot, + SyncStatusJson +} from '@powersync/shared-internals'; import * as Comlink from 'comlink'; import { WebRemote } from '../../db/sync/WebRemote.js'; import { @@ -119,7 +124,7 @@ export class SharedSyncImplementation extends BaseObserver { - this.updateAllStatuses(status.toJSON()); + statusChanged: (status, dataFlow) => { + const snapshot = new SyncStatusSnapshot(status, dataFlow); + this.syncStatus = snapshot; + const json = snapshot.toJSON(); + this.ports.forEach((p) => p.clientProvider.statusChanged(json)); } }); @@ -192,12 +199,6 @@ export class SharedSyncImplementation extends BaseObserver { - return this.withSyncImplementation(async (sync) => { - return sync.waitForStatus(status); - }); - } - async waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise { return this.withSyncImplementation(async (sync) => { return sync.waitUntilStatusMatches(predicate); @@ -302,7 +303,7 @@ export class SharedSyncImplementation extends BaseObserver p.clientProvider.statusChanged(status)); - } } /** diff --git a/packages/web/src/worker/sync/WorkerClient.ts b/packages/web/src/worker/sync/WorkerClient.ts index 78b26bfea..19d0fc816 100644 --- a/packages/web/src/worker/sync/WorkerClient.ts +++ b/packages/web/src/worker/sync/WorkerClient.ts @@ -1,4 +1,4 @@ -import { ResolvedSyncOptions, SubscribedStream } from '@powersync/common'; +import { ResolvedSyncOptions } from '@powersync/common'; import * as Comlink from 'comlink'; import { getNavigatorLocks } from '../../shared/navigator.js'; import { @@ -8,6 +8,7 @@ import { SharedSyncInitOptions, WrappedSyncPort } from './SharedSyncImplementation.js'; +import { SubscribedStream } from '@powersync/shared-internals'; /** * A client to the shared sync worker. diff --git a/packages/web/tests/crud.test.ts b/packages/web/tests/crud.test.ts index 795903aeb..aae9dbb31 100644 --- a/packages/web/tests/crud.test.ts +++ b/packages/web/tests/crud.test.ts @@ -1,8 +1,9 @@ -import { Column, ColumnType, CrudEntry, Schema, Table, UpdateType } from '@powersync/common'; +import { Column, ColumnType, Schema, Table, UpdateType } from '@powersync/common'; import pDefer from 'p-defer'; import { v4 as uuid } from 'uuid'; import { describe, expect, it } from 'vitest'; import { generateTestDb } from './utils/testDb.js'; +import { CrudEntryImpl } from '@powersync/shared-internals'; const testId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42'; @@ -22,7 +23,7 @@ describe('CRUD Tests', { sequential: true }, () => { const tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId).equals(1); - const expectedCrudEntry = new CrudEntry(1, UpdateType.PUT, 'assets', testId, 1, { description: 'test' }); + const expectedCrudEntry = new CrudEntryImpl(1, UpdateType.PUT, 'assets', testId, 1, { description: 'test' }); expect(tx.crud[0].equals(expectedCrudEntry)).true; }); @@ -48,8 +49,8 @@ describe('CRUD Tests', { sequential: true }, () => { const crudBatch = (await powersync.getCrudBatch(2))!; expect(crudBatch.crud.length).equals(2); - const expectedCrudEntry = new CrudEntry(1, UpdateType.PUT, 'assets', testId, 1, { description: 'test' }); - const expectedCrudEntry2 = new CrudEntry(2, UpdateType.PUT, 'assets', 'mockId', 1, { description: 'test1' }); + const expectedCrudEntry = new CrudEntryImpl(1, UpdateType.PUT, 'assets', testId, 1, { description: 'test' }); + const expectedCrudEntry2 = new CrudEntryImpl(2, UpdateType.PUT, 'assets', 'mockId', 1, { description: 'test1' }); expect(crudBatch.crud[0].equals(expectedCrudEntry)).true; expect(crudBatch.crud[1].equals(expectedCrudEntry2)).true; }); @@ -95,7 +96,7 @@ describe('CRUD Tests', { sequential: true }, () => { const tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId).equals(2); - const expectedCrudEntry = new CrudEntry(2, UpdateType.PATCH, 'assets', testId, 2, { description: 'test2' }); + const expectedCrudEntry = new CrudEntryImpl(2, UpdateType.PATCH, 'assets', testId, 2, { description: 'test2' }); expect(tx.crud[0].equals(expectedCrudEntry)).true; }); @@ -122,11 +123,11 @@ describe('CRUD Tests', { sequential: true }, () => { const crudBatch = (await powersync.getCrudBatch(2))!; expect(crudBatch.crud.length).equals(2); - const expectedCrudEntry = new CrudEntry(3, UpdateType.PATCH, 'assets', testId, 2, { + const expectedCrudEntry = new CrudEntryImpl(3, UpdateType.PATCH, 'assets', testId, 2, { description: 'test2', make: 'make2' }); - const expectedCrudEntry2 = new CrudEntry(4, UpdateType.PATCH, 'assets', 'mockId', 2, { + const expectedCrudEntry2 = new CrudEntryImpl(4, UpdateType.PATCH, 'assets', 'mockId', 2, { description: 'test2', make: 'make2' }); @@ -148,7 +149,7 @@ describe('CRUD Tests', { sequential: true }, () => { const tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId).equals(2); - const expectedCrudEntry = new CrudEntry(2, UpdateType.DELETE, 'assets', testId, 2); + const expectedCrudEntry = new CrudEntryImpl(2, UpdateType.DELETE, 'assets', testId, 2); expect(tx.crud[0].equals(expectedCrudEntry)).true; }); @@ -202,7 +203,7 @@ describe('CRUD Tests', { sequential: true }, () => { const tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId).equals(1); - const expectedCrudEntry = new CrudEntry(1, UpdateType.PUT, 'logs', testId, 1, { + const expectedCrudEntry = new CrudEntryImpl(1, UpdateType.PUT, 'logs', testId, 1, { content: 'test log', level: 'INFO' }); @@ -227,9 +228,9 @@ describe('CRUD Tests', { sequential: true }, () => { const tx = (await powersync.getNextCrudTransaction())!; expect(tx.transactionId).equals(1); - expect(tx.crud[0].equals(new CrudEntry(1, UpdateType.PUT, 'assets', testId, 1, { quantity: bigNumber }))).equals( - true - ); + expect( + tx.crud[0].equals(new CrudEntryImpl(1, UpdateType.PUT, 'assets', testId, 1, { quantity: bigNumber })) + ).equals(true); }); it('big numbers - text', async () => { @@ -280,8 +281,8 @@ describe('CRUD Tests', { sequential: true }, () => { const tx1 = (await powersync.getNextCrudTransaction())!; expect(tx1.transactionId).equals(1); const expectedCrudEntries = [ - new CrudEntry(1, UpdateType.PUT, 'assets', testId, 1, { description: 'test1' }), - new CrudEntry(2, UpdateType.PUT, 'assets', 'test2', 1, { description: 'test2' }) + new CrudEntryImpl(1, UpdateType.PUT, 'assets', testId, 1, { description: 'test1' }), + new CrudEntryImpl(2, UpdateType.PUT, 'assets', 'test2', 1, { description: 'test2' }) ]; expect(tx1.crud.map((entry, index) => entry.equals(expectedCrudEntries[index]))).deep.equals([true, true]); @@ -289,7 +290,7 @@ describe('CRUD Tests', { sequential: true }, () => { const tx2 = (await powersync.getNextCrudTransaction())!; expect(tx2.transactionId).equals(2); - const expectedCrudEntry2 = new CrudEntry(3, UpdateType.PATCH, 'assets', testId, 2, { description: 'updated' }); + const expectedCrudEntry2 = new CrudEntryImpl(3, UpdateType.PATCH, 'assets', testId, 2, { description: 'updated' }); expect(tx2.crud[0].equals(expectedCrudEntry2)).true; await tx2.complete(); expect(await powersync.getNextCrudTransaction()).equals(null); diff --git a/packages/web/tests/mockSyncServiceExample.test.ts b/packages/web/tests/mockSyncServiceExample.test.ts index aa72a521f..56016ee0f 100644 --- a/packages/web/tests/mockSyncServiceExample.test.ts +++ b/packages/web/tests/mockSyncServiceExample.test.ts @@ -11,7 +11,7 @@ import { describe, expect, vi } from 'vitest'; import { sharedMockSyncServiceTest } from './utils/mockSyncServiceTest.js'; -import { StreamingSyncCheckpoint } from '@powersync/common/internal/sync_protocol'; +import { StreamingSyncCheckpoint } from '@powersync/shared-internals/internal/sync_protocol'; describe('Mock Sync Service Example', { timeout: 100000 }, () => { sharedMockSyncServiceTest( diff --git a/packages/web/tests/mocks/MockWebRemote.ts b/packages/web/tests/mocks/MockWebRemote.ts index f4620a274..bc9ee9005 100644 --- a/packages/web/tests/mocks/MockWebRemote.ts +++ b/packages/web/tests/mocks/MockWebRemote.ts @@ -1,12 +1,13 @@ +import { PowerSyncLogger } from '@powersync/common'; import { AbstractRemote, AbstractRemoteOptions, FetchImplementation, FetchImplementationProvider, - PowerSyncLogger, RemoteConnector, SocketSyncStreamOptions -} from '@powersync/common'; +} from '@powersync/shared-internals'; + import { SimpleAsyncIterator } from '@powersync/shared-internals'; import { type BSON } from 'bson'; import { MockSyncService, setupMockServiceMessageHandler } from '../utils/MockSyncServiceWorker.js'; diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index a52a5b830..05c2aee93 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -1,5 +1,5 @@ import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, createConsoleLogger, DBAdapterDefaultMixin, LogLevels, @@ -27,7 +27,7 @@ describe('Multiple Instances', { sequential: true }, () => { schema: TEST_SCHEMA }); - function createAsset(powersync: AbstractPowerSyncDatabase) { + function createAsset(powersync: CommonPowerSyncDatabase) { return powersync.execute('INSERT INTO assets(id, description) VALUES(uuid(), ?)', ['test']); } diff --git a/packages/web/tests/offline.test.ts b/packages/web/tests/offline.test.ts index 384e1f81c..86b386350 100644 --- a/packages/web/tests/offline.test.ts +++ b/packages/web/tests/offline.test.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase } from '@powersync/common'; +import { CommonPowerSyncDatabase } from '@powersync/common'; import { PowerSyncDatabase } from '@powersync/web'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { makeOptionalSyncSchema } from './utils/optionalSyncTestSchema.js'; @@ -8,7 +8,7 @@ const userId = '3390de4f-0488-4e50-abed-f8e8eb1d0b42'; const customerId = '4490de4f-0488-4e50-abed-f8e8eb1d0b42'; describe('Schema Tests', { sequential: true }, () => { - let db: AbstractPowerSyncDatabase; + let db: CommonPowerSyncDatabase; beforeEach(async () => { db = new PowerSyncDatabase({ @@ -100,7 +100,7 @@ describe('Schema Tests', { sequential: true }, () => { }); }); -export async function getSourceTables(db: AbstractPowerSyncDatabase, sql: string, parameters: Array = []) { +export async function getSourceTables(db: CommonPowerSyncDatabase, sql: string, parameters: Array = []) { const rows = await db.getAll<{ opcode: string; p3: number; p2: string }>(`EXPLAIN ${sql}`, parameters); const rootpages: number[] = []; diff --git a/packages/web/tests/on_change.test.ts b/packages/web/tests/on_change.test.ts index 74417b87a..c5f5ce69b 100644 --- a/packages/web/tests/on_change.test.ts +++ b/packages/web/tests/on_change.test.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, WatchOnChangeEvent } from '@powersync/common'; +import { CommonPowerSyncDatabase, WatchOnChangeEvent } from '@powersync/common'; import { PowerSyncDatabase } from '@powersync/web'; import { v4 as uuid } from 'uuid'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -7,7 +7,7 @@ import { TEST_SCHEMA } from './utils/test-schema.js'; const UPLOAD_TIMEOUT_MS = 3000; describe('OnChange Tests', { sequential: true }, () => { - let powersync: AbstractPowerSyncDatabase; + let powersync: CommonPowerSyncDatabase; beforeEach(async () => { powersync = new PowerSyncDatabase({ diff --git a/packages/web/tests/open.test.ts b/packages/web/tests/open.test.ts index 4fa08d492..dacaeba3f 100644 --- a/packages/web/tests/open.test.ts +++ b/packages/web/tests/open.test.ts @@ -1,5 +1,5 @@ import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, createConsoleLogger, DBAdapterDefaultMixin, LogLevels, @@ -21,7 +21,7 @@ import { defaultLogLevel, defaultTestLogger } from './utils/logger.js'; const testId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42'; -export const basicTest = async (db: AbstractPowerSyncDatabase) => { +export const basicTest = async (db: CommonPowerSyncDatabase) => { await db.execute('INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test']); expect(await db.getAll('SELECT * FROM assets')).length.gt(0); await db.disconnectAndClear(); diff --git a/packages/web/tests/performance.test.ts b/packages/web/tests/performance.test.ts index 2334b31f7..3efc2ba81 100644 --- a/packages/web/tests/performance.test.ts +++ b/packages/web/tests/performance.test.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, Schema, Table, column } from '@powersync/common'; +import { CommonPowerSyncDatabase, Schema, Table, column } from '@powersync/common'; import { PowerSyncDatabase } from '@powersync/web'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -8,7 +8,7 @@ describe('Basic', { sequential: true }, () => { email: column.text }); - let db: AbstractPowerSyncDatabase; + let db: CommonPowerSyncDatabase; beforeEach(() => { db = new PowerSyncDatabase({ diff --git a/packages/web/tests/schema.test.ts b/packages/web/tests/schema.test.ts index c2229b215..8e7cd1c8c 100644 --- a/packages/web/tests/schema.test.ts +++ b/packages/web/tests/schema.test.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, Column, ColumnType, Index, IndexedColumn, Schema, Table } from '@powersync/common'; +import { CommonPowerSyncDatabase, Column, ColumnType, Index, IndexedColumn, Schema, Table } from '@powersync/common'; import { PowerSyncDatabase } from '@powersync/web'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -75,7 +75,7 @@ const generateSchemaTables = (assetsTableGenerator: () => Table = generateAssets const schema = new Schema(generateSchemaTables()); describe('Schema Tests', { sequential: true }, () => { - let powersync: AbstractPowerSyncDatabase; + let powersync: CommonPowerSyncDatabase; beforeEach(async () => { powersync = new PowerSyncDatabase({ diff --git a/packages/web/tests/schemav2.test.ts b/packages/web/tests/schemav2.test.ts index 0541a1b19..6915fc125 100644 --- a/packages/web/tests/schemav2.test.ts +++ b/packages/web/tests/schemav2.test.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, Schema, TableV2, column } from '@powersync/common'; +import { CommonPowerSyncDatabase, Schema, TableV2, column } from '@powersync/common'; import { PowerSyncDatabase } from '@powersync/web'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -57,7 +57,7 @@ const aliased = new TableV2({ name: column.text }, { viewName: 'test1' }); const schema = new Schema({ assets, customers, logs, credentials, aliased }); describe('Schema Tests', { sequential: true }, () => { - let powersync: AbstractPowerSyncDatabase; + let powersync: CommonPowerSyncDatabase; beforeEach(async () => { powersync = new PowerSyncDatabase({ diff --git a/packages/web/tests/stream.test.ts b/packages/web/tests/stream.test.ts index ae97b9a63..a49b08473 100644 --- a/packages/web/tests/stream.test.ts +++ b/packages/web/tests/stream.test.ts @@ -4,15 +4,15 @@ import { Schema, SyncOptions, SyncStreamConnectionMethod, - SyncStreamOptions, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { describe, expect, it, onTestFinished, vi } from 'vitest'; import { TestConnector } from './utils/MockStreamOpenFactory.js'; import { ConnectedDatabaseUtils, generateConnectedDatabase } from './utils/generateConnectedDatabase.js'; -import { BucketChecksum } from '@powersync/common/internal/sync_protocol'; +import { BucketChecksum } from '@powersync/shared-internals/internal/sync_protocol'; import { defaultLogLevel, defaultTestLogger } from './utils/logger.js'; +import { SyncStreamOptions } from '@powersync/shared-internals'; const UPLOAD_TIMEOUT_MS = 3000; @@ -244,7 +244,7 @@ describe('Streaming', { sequential: true }, () => { buckets: [bucket('a', 2)] } }); - await vi.waitFor(() => powersync.currentStatus.dataFlowStatus.downloading == true); + await vi.waitFor(() => powersync.currentStatus.downloading == true); remote.enqueueLine({ data: { bucket: 'a', @@ -260,7 +260,7 @@ describe('Streaming', { sequential: true }, () => { } }); remote.enqueueLine({ checkpoint_complete: { last_op_id: '2' } }); - await vi.waitFor(() => powersync.currentStatus.dataFlowStatus.downloading == false); + await vi.waitFor(() => powersync.currentStatus.downloading == false); console.log('has second sync, should update list'); expect((await query.next()).value.rows._array).toStrictEqual([]); diff --git a/packages/web/tests/utils/MockStreamOpenFactory.ts b/packages/web/tests/utils/MockStreamOpenFactory.ts index 61e9c293c..a6169e639 100644 --- a/packages/web/tests/utils/MockStreamOpenFactory.ts +++ b/packages/web/tests/utils/MockStreamOpenFactory.ts @@ -1,17 +1,19 @@ import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, + PowerSyncBackendConnector, + PowerSyncCredentials, + PowerSyncLogger +} from '@powersync/common'; +import { AbstractRemote, AbstractStreamingSyncImplementation, CreateSyncImplementationOptions, - PowerSyncBackendConnector, - PowerSyncCredentials, - PowerSyncDatabaseOptions, - PowerSyncLogger, + SimpleAsyncIterator, RemoteConnector, - SyncStreamOptions -} from '@powersync/common'; -import { SimpleAsyncIterator } from '@powersync/shared-internals'; -import { StreamingSyncLine } from '@powersync/common/internal/sync_protocol'; + SyncStreamOptions, + SqliteBucketStorage +} from '@powersync/shared-internals'; +import { StreamingSyncLine } from '@powersync/shared-internals/internal/sync_protocol'; import { PowerSyncDatabase, WebPowerSyncDatabaseOptions, WebStreamingSyncImplementation } from '@powersync/web'; import { MockedFunction, vi } from 'vitest'; @@ -22,7 +24,7 @@ export class TestConnector implements PowerSyncBackendConnector { token: '' }; } - async uploadData(database: AbstractPowerSyncDatabase): Promise { + async uploadData(database: CommonPowerSyncDatabase): Promise { const tx = await database.getNextCrudTransaction(); await tx?.complete(); } @@ -134,7 +136,7 @@ export class MockedStreamPowerSync extends PowerSyncDatabase { ): AbstractStreamingSyncImplementation { return new WebStreamingSyncImplementation({ logger: this.logger, - adapter: this.bucketStorageAdapter, + adapter: new SqliteBucketStorage(this.database, this.logger), remote: this.remote, uploadCrud: async () => { await this.waitForReady(); diff --git a/packages/web/tests/utils/MockSyncServiceClient.ts b/packages/web/tests/utils/MockSyncServiceClient.ts index 3096c8edd..da93ecfab 100644 --- a/packages/web/tests/utils/MockSyncServiceClient.ts +++ b/packages/web/tests/utils/MockSyncServiceClient.ts @@ -1,4 +1,4 @@ -import { StreamingSyncLine } from '@powersync/common/internal/sync_protocol'; +import { StreamingSyncLine } from '@powersync/shared-internals/internal/sync_protocol'; import type { AutomaticResponseConfig, MockSyncServiceMessage, diff --git a/packages/web/tests/watch.test.ts b/packages/web/tests/watch.test.ts index 4cde9f8a7..6e3eabee7 100644 --- a/packages/web/tests/watch.test.ts +++ b/packages/web/tests/watch.test.ts @@ -1,5 +1,5 @@ import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, ArrayComparator, GetAllQuery, QueryResult, @@ -23,7 +23,7 @@ vi.useRealTimers(); const throttleDuration = 1000; describe('Watch Tests', { sequential: true }, () => { - let powersync: AbstractPowerSyncDatabase; + let powersync: CommonPowerSyncDatabase; beforeEach(async () => { powersync = new PowerSyncDatabase({ diff --git a/packages/web/tests/watchSchemaChange.test.ts b/packages/web/tests/watchSchemaChange.test.ts index 05a4cf972..0b30c910b 100644 --- a/packages/web/tests/watchSchemaChange.test.ts +++ b/packages/web/tests/watchSchemaChange.test.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, QueryResult } from '@powersync/common'; +import { CommonPowerSyncDatabase, QueryResult } from '@powersync/common'; import { PowerSyncDatabase } from '@powersync/web'; import { v4 as uuid } from 'uuid'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -16,7 +16,7 @@ vi.useRealTimers(); const throttleDuration = 1000; describe('Watch With Schema Change Tests', { sequential: true }, () => { - let powersync: AbstractPowerSyncDatabase; + let powersync: CommonPowerSyncDatabase; beforeEach(async () => { powersync = new PowerSyncDatabase({ @@ -144,7 +144,7 @@ describe('Watch With Schema Change Tests', { sequential: true }, () => { }); }); -export async function getSourceTables(db: AbstractPowerSyncDatabase, sql: string, parameters: Array = []) { +export async function getSourceTables(db: CommonPowerSyncDatabase, sql: string, parameters: Array = []) { const rows = await db.getAll<{ opcode: string; p3: number; p2: string }>(`EXPLAIN ${sql}`, parameters); const rootpages: number[] = []; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 965f71c91..858b865da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -555,6 +555,9 @@ importers: '@nuxt/kit': specifier: ^4.3.1 version: 4.3.1(magicast@0.5.2) + '@powersync/shared-internals': + specifier: workspace:^1.0.0 + version: link:../shared-internals '@tanstack/vue-table': specifier: ^8.21.3 version: 8.21.3(vue@3.5.30(typescript@6.0.3)) @@ -866,6 +869,9 @@ importers: '@powersync/common': specifier: 'workspace:' version: link:../common + '@powersync/shared-internals': + specifier: 'workspace:' + version: link:../shared-internals '@tauri-apps/api': specifier: ^2.0.0 version: 2.10.1 @@ -966,6 +972,9 @@ importers: '@powersync/react': specifier: workspace:* version: link:../../packages/react + '@powersync/shared-internals': + specifier: workspace:* + version: link:../../packages/shared-internals '@powersync/web': specifier: workspace:* version: link:../../packages/web diff --git a/tools/diagnostics-app/package.json b/tools/diagnostics-app/package.json index d77c843f2..5e8f267ed 100644 --- a/tools/diagnostics-app/package.json +++ b/tools/diagnostics-app/package.json @@ -14,6 +14,7 @@ "@monaco-editor/react": "^4.7.0", "@powersync/react": "workspace:*", "@powersync/web": "workspace:*", + "@powersync/shared-internals": "workspace:*", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", diff --git a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx index 79725f940..b315bf4ff 100644 --- a/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx +++ b/tools/diagnostics-app/src/app/views/sync-diagnostics.tsx @@ -112,7 +112,7 @@ async function fetchSyncStats(): Promise { const { synced_at } = await db.get<{ synced_at: string | null }>('SELECT powersync_last_synced_at() as synced_at'); const lastSyncedAt = synced_at ? new Date(synced_at + 'Z') : null; - if (synced_at != null && !sync?.syncStatus.dataFlowStatus.downloading) { + if (synced_at != null && !sync?.syncStatus.downloading) { const bucketRows = await db.getAll(BUCKETS_QUERY); const tableRows = await db.getAll(TABLES_SIZE_QUERY); return { bucketRows, tableRows, lastSyncedAt }; @@ -303,8 +303,7 @@ export default function SyncDiagnosticsPage() { href="https://docs.powersync.com/sync/rules/parameter-queries" target="_blank" rel="noopener noreferrer" - className="underline hover:text-foreground" - > + className="underline hover:text-foreground"> Sync Rules {' '} or{' '} @@ -312,8 +311,7 @@ export default function SyncDiagnosticsPage() { href="https://docs.powersync.com/sync/streams/parameters" target="_blank" rel="noopener noreferrer" - className="underline hover:text-foreground" - > + className="underline hover:text-foreground"> Sync Streams ) count toward both, but are de-duplicated client-side, so the server's parameter result count @@ -338,8 +336,7 @@ export default function SyncDiagnosticsPage() { = 900 ? 'text-destructive' : totals.buckets >= 800 ? 'text-amber-600' : '' - )} - > + )}> {totals.buckets.toLocaleString()} @@ -369,8 +366,7 @@ export default function SyncDiagnosticsPage() { = 900 ? 'text-destructive' : totals.buckets >= 800 ? 'text-amber-600' : '' - )} - > + )}> {totals.buckets.toLocaleString()} @@ -438,8 +434,7 @@ export default function SyncDiagnosticsPage() { variant="ghost" size="sm" className="h-7 gap-1.5 text-muted-foreground" - onClick={() => setShowTokenDialog(true)} - > + onClick={() => setShowTokenDialog(true)}> View Token @@ -482,8 +477,7 @@ export default function SyncDiagnosticsPage() { variant="outline" onClick={() => { clearData(); - }} - > + }}> Clear & Redownload @@ -502,8 +496,7 @@ export default function SyncDiagnosticsPage() { href="https://docs.powersync.com/maintenance-ops/compacting-buckets" target="_blank" rel="noopener noreferrer" - className="underline hover:text-foreground" - > + className="underline hover:text-foreground"> Learn about compacting @@ -524,8 +517,7 @@ export default function SyncDiagnosticsPage() { href="https://docs.powersync.com/debugging/troubleshooting#too-many-buckets-psync_s2305" target="_blank" rel="noopener noreferrer" - className="underline hover:text-foreground" - > + className="underline hover:text-foreground"> For troubleshooting steps, see the docs @@ -616,8 +608,7 @@ function TruncatedTablesList({ tables }: { tables: string }) { e.stopPropagation(); setExpanded(!expanded); }} - className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors" - > + className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"> {expanded ? ( <> diff --git a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts index 8ced18275..764cefd36 100644 --- a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts +++ b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts @@ -18,6 +18,7 @@ import { ClientParameterRow, localStateDb } from './LocalStateManager'; import { DynamicSchemaManager } from './DynamicSchemaManager'; import { RustClientInterceptor } from './RustClientInterceptor'; import { TokenConnector } from './TokenConnector'; +import { SyncStatusSnapshot } from '@powersync/shared-internals'; const baseLogger = createConsoleLogger({ minLevel: LogLevels.debug }); @@ -200,8 +201,8 @@ export function useSyncStatus() { setCurrent(sync.syncStatus); const l = sync.registerListener({ - statusChanged: (status) => { - setCurrent(status); + statusChanged: (core, dataFlow) => { + setCurrent(new SyncStatusSnapshot(core, dataFlow)); } }); return () => l?.(); diff --git a/tools/diagnostics-app/src/library/powersync/DynamicSchemaManager.ts b/tools/diagnostics-app/src/library/powersync/DynamicSchemaManager.ts index 6655b1d8e..22257b0b6 100644 --- a/tools/diagnostics-app/src/library/powersync/DynamicSchemaManager.ts +++ b/tools/diagnostics-app/src/library/powersync/DynamicSchemaManager.ts @@ -1,5 +1,5 @@ -import { AbstractPowerSyncDatabase, Column, ColumnType, Schema, Table } from '@powersync/web'; -import type { SyncDataBucketJSON } from '@powersync/common/internal/sync_protocol'; +import { CommonPowerSyncDatabase, Column, ColumnType, Schema, Table } from '@powersync/web'; +import type { SyncDataBucketJSON } from '@powersync/shared-internals/internal/sync_protocol'; import { AppSchema } from './AppSchema'; import { JsSchemaGenerator } from './JsSchemaGenerator'; import { localStateDb } from './LocalStateManager'; @@ -14,7 +14,7 @@ export class DynamicSchemaManager { private tables: Record> = {}; private dirty = false; private refreshTimeout: ReturnType | null = null; - private pendingDb: AbstractPowerSyncDatabase | null = null; + private pendingDb: CommonPowerSyncDatabase | null = null; /** * Load dynamic schema from local DB. Call after localStateDb is initialized (e.g. before connect). @@ -88,7 +88,7 @@ export class DynamicSchemaManager { * Apply schema to db. Debounced so we don't call db.updateSchema() on every sync batch * (updateFromOperations can fire many times per second during sync). */ - async refreshSchema(db: AbstractPowerSyncDatabase) { + async refreshSchema(db: CommonPowerSyncDatabase) { if (!this.dirty) return; this.pendingDb = db; if (this.refreshTimeout != null) return; @@ -105,7 +105,7 @@ export class DynamicSchemaManager { } /** Call when refresh must run immediately (e.g. after connect). */ - async refreshSchemaNow(db: AbstractPowerSyncDatabase) { + async refreshSchemaNow(db: CommonPowerSyncDatabase) { if (this.refreshTimeout != null) { clearTimeout(this.refreshTimeout); this.refreshTimeout = null; diff --git a/tools/diagnostics-app/src/library/powersync/RustClientInterceptor.ts b/tools/diagnostics-app/src/library/powersync/RustClientInterceptor.ts index a986b76dc..8969f4004 100644 --- a/tools/diagnostics-app/src/library/powersync/RustClientInterceptor.ts +++ b/tools/diagnostics-app/src/library/powersync/RustClientInterceptor.ts @@ -1,12 +1,12 @@ +import { ColumnType, CommonPowerSyncDatabase } from '@powersync/web'; import { AbstractPowerSyncDatabase, AbstractRemote, - ColumnType, PowerSyncControlCommand, SqliteBucketStorage -} from '@powersync/web'; +} from '@powersync/shared-internals'; import { type BSON } from 'bson'; -import type { BucketChecksum, Checkpoint, StreamingSyncLine } from '@powersync/common/internal/sync_protocol'; +import type { BucketChecksum, Checkpoint, StreamingSyncLine } from '@powersync/shared-internals/internal/sync_protocol'; import { DynamicSchemaManager } from './DynamicSchemaManager'; /** @@ -18,13 +18,13 @@ import { DynamicSchemaManager } from './DynamicSchemaManager'; */ export class RustClientInterceptor extends SqliteBucketStorage { private bson?: typeof BSON; - private rdb: AbstractPowerSyncDatabase; + private rdb: CommonPowerSyncDatabase; private lastStartedCheckpoint: Checkpoint | null = null; public tables: Record> = {}; constructor( - db: AbstractPowerSyncDatabase, + db: CommonPowerSyncDatabase, private remote: AbstractRemote, private schemaManager: DynamicSchemaManager ) { diff --git a/tools/diagnostics-app/src/routes/_authenticated.tsx b/tools/diagnostics-app/src/routes/_authenticated.tsx index c966dfa73..ba44e4438 100644 --- a/tools/diagnostics-app/src/routes/_authenticated.tsx +++ b/tools/diagnostics-app/src/routes/_authenticated.tsx @@ -77,17 +77,9 @@ function AppSidebar() { {open ? ( - PowerSync Logo + PowerSync Logo ) : ( - PowerSync + PowerSync )} @@ -97,8 +89,7 @@ function AppSidebar() { {NAVIGATION_ITEMS.map((item) => { const Icon = item.icon; - const isActive = - location.pathname === item.path || location.pathname.startsWith(item.path + '/'); + const isActive = location.pathname === item.path || location.pathname.startsWith(item.path + '/'); return ( - setShowLogoutDialog(true)}> + setShowLogoutDialog(true)}> Sign Out @@ -166,44 +155,41 @@ function AuthenticatedLayout() { - {/* Top Bar */} -
-
-

{title}

-
-
- +
+

{title}

+
+
+ + + {syncStatus?.connected ? ( + + ) : ( + + + )} - /> - - {syncStatus?.connected ? ( - - ) : ( - - - - )} -
-
+ + - {/* Main Content Area - min-w-0 so wide table content scrolls inside DataTable, not the whole page */} -
- {syncError ? ( - - Sync error detected: {syncError.message} - - ) : null} - -
-
+ {/* Main Content Area - min-w-0 so wide table content scrolls inside DataTable, not the whole page */} +
+ {syncError ? ( + + Sync error detected: {syncError.message} + + ) : null} + +
+ ); diff --git a/tools/diagnostics-app/tsconfig.json b/tools/diagnostics-app/tsconfig.json index db2329f9a..a7adfe039 100644 --- a/tools/diagnostics-app/tsconfig.json +++ b/tools/diagnostics-app/tsconfig.json @@ -19,5 +19,10 @@ "@types/react": ["./node_modules/@types/react"] } }, - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "references": [ + {"path": "../../packages/common" }, + {"path": "../../packages/shared-internals" }, + {"path": "../../packages/web" } + ] } diff --git a/tools/powersynctests/src/tests/queries.test.ts b/tools/powersynctests/src/tests/queries.test.ts index 6a025593e..7fa675ecc 100644 --- a/tools/powersynctests/src/tests/queries.test.ts +++ b/tools/powersynctests/src/tests/queries.test.ts @@ -1,6 +1,6 @@ import { OPSqliteOpenFactory } from '@powersync/op-sqlite'; import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, column, LockContext, PowerSyncDatabase, @@ -54,7 +54,7 @@ const createDatabase = () => { }); }; -let db: AbstractPowerSyncDatabase; +let db: CommonPowerSyncDatabase; export function registerBaseTests() { describe('Raw queries', () => { diff --git a/tsconfig.json b/tsconfig.json index ca9a62610..09f415868 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,9 +25,9 @@ { "path": "./packages/node" }, - // { - // "path": "./packages/nuxt" - // }, + { + "path": "./packages/nuxt" + }, { "path": "./packages/powersync-op-sqlite" }, From 1ad1b52003611992245e4d5b96959b414a602cb0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 15:11:00 +0200 Subject: [PATCH 03/12] Make more things internal --- packages/capacitor/src/PowerSyncDatabase.ts | 4 +- packages/common/etc/common.api.md | 1154 ++--------------- packages/common/package.json | 3 +- .../src/client/CommonPowerSyncDatabase.ts | 25 +- packages/common/src/client/SQLOpenFactory.ts | 22 - .../connection/PowerSyncBackendConnector.ts | 2 +- packages/common/src/client/sync/options.ts | 20 - .../common/src/client/sync/sync-streams.ts | 2 +- .../src/client/triggers/TriggerManager.ts | 12 +- .../processors/DifferentialQueryProcessor.ts | 1 - packages/common/src/db/schema/Column.ts | 8 - packages/common/src/db/schema/Index.ts | 5 +- .../common/src/db/schema/IndexedColumn.ts | 5 +- packages/common/src/db/schema/Table.ts | 25 +- packages/common/src/index.ts | 14 +- packages/common/src/utils/MetaBaseObserver.ts | 10 + packages/node/src/db/PowerSyncDatabase.ts | 5 +- packages/node/src/db/RemoteConnection.ts | 9 +- .../react-native/src/db/PowerSyncDatabase.ts | 4 +- .../src/client/ConnectionManager.ts | 3 +- .../src/client/sync/options.ts | 21 + .../AbstractStreamingSyncImplementation.ts | 2 +- .../src/db/ConnectionClosedError.ts | 0 .../shared-internals/src/db/openDatabase.ts | 23 + packages/shared-internals/src/index.ts | 4 + .../src/utils/parseQuery.ts | 2 +- .../tests/utils/parseQuery.test.ts | 2 +- packages/tanstack-react-query/package.json | 1 + .../src/hooks/usePowerSyncQueries.ts | 3 +- packages/tanstack-react-query/tsconfig.json | 3 + packages/vue/package.json | 3 +- .../vue/src/composables/useSingleQuery.ts | 3 +- .../vue/src/composables/useWatchedQuery.ts | 3 +- packages/vue/tsconfig.json | 3 + packages/web/src/db/PowerSyncDatabase.ts | 4 +- .../db/adapters/wa-sqlite/DatabaseClient.ts | 3 +- .../SharedWebStreamingSyncImplementation.ts | 4 +- .../worker/sync/SharedSyncImplementation.ts | 9 +- packages/web/src/worker/sync/WorkerClient.ts | 3 +- pnpm-lock.yaml | 7 + .../library/powersync/ConnectionManager.ts | 2 +- 41 files changed, 265 insertions(+), 1173 deletions(-) rename packages/{common => shared-internals}/src/db/ConnectionClosedError.ts (100%) create mode 100644 packages/shared-internals/src/db/openDatabase.ts rename packages/{common => shared-internals}/src/utils/parseQuery.ts (92%) rename packages/{common => shared-internals}/tests/utils/parseQuery.test.ts (97%) diff --git a/packages/capacitor/src/PowerSyncDatabase.ts b/packages/capacitor/src/PowerSyncDatabase.ts index 3b89f72de..eebab5658 100644 --- a/packages/capacitor/src/PowerSyncDatabase.ts +++ b/packages/capacitor/src/PowerSyncDatabase.ts @@ -3,7 +3,6 @@ import { CommonPowerSyncDatabase, DBAdapter, LogLevels, - openDatabase, PowerSyncBackendConnector, PowerSyncDatabaseConstructor, SyncOptions, @@ -18,7 +17,8 @@ import { CreateSyncImplementationOptions, MEMORY_TRIGGER_CLAIM_MANAGER, StreamingSyncImplementation, - TriggerManagerConfig + TriggerManagerConfig, + openDatabase } from '@powersync/shared-internals'; class CapacitorPowerSyncDatabase extends WebPowerSyncDatabase { diff --git a/packages/common/etc/common.api.md b/packages/common/etc/common.api.md index fee63def7..90e54ae52 100644 --- a/packages/common/etc/common.api.md +++ b/packages/common/etc/common.api.md @@ -4,305 +4,8 @@ ```ts -import { fetch as fetch_2 } from 'cross-fetch'; - -// Warning: (ae-internal-missing-underscore) The name "AbortOperation" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class AbortOperation extends Error { - constructor(reason: string); - // (undocumented) - protected reason: string; -} - -// Warning: (ae-incompatible-release-tags) The symbol "AbstractPowerSyncDatabase" is marked as @public, but its signature references "BaseObserver" which is marked as @internal -// -// @public (undocumented) -export abstract class AbstractPowerSyncDatabase extends BaseObserver { - constructor(options: Options); - // Warning: (ae-incompatible-release-tags) The symbol "bucketStorageAdapter" is marked as @public, but its signature references "BucketStorageAdapter" which is marked as @internal - // - // (undocumented) - protected bucketStorageAdapter: BucketStorageAdapter; - close(options?: PowerSyncCloseOptions): Promise; - closed: boolean; - connect(connector: PowerSyncBackendConnector, options?: SyncOptions): Promise; - get connected(): boolean; - // (undocumented) - get connecting(): boolean; - // Warning: (ae-incompatible-release-tags) The symbol "connectionManager" is marked as @public, but its signature references "ConnectionManager" which is marked as @internal - // - // (undocumented) - protected connectionManager: ConnectionManager; - get connectionOptions(): Required | null; - get connector(): PowerSyncBackendConnector | null; - currentStatus: SyncStatus; - customQuery(query: WatchCompatibleQuery): Query; - get database(): DBAdapter; - disconnect(): Promise; - disconnectAndClear(options?: DisconnectAndClearOptions): Promise; - // @deprecated (undocumented) - dispose(): void; - execute(sql: string, parameters?: any[]): Promise; - executeBatch(sql: string, parameters?: any[][]): Promise; - executeRaw(sql: string, parameters?: any[]): Promise; - // Warning: (ae-incompatible-release-tags) The symbol "generateBucketStorageAdapter" is marked as @public, but its signature references "BucketStorageAdapter" which is marked as @internal - // - // (undocumented) - protected abstract generateBucketStorageAdapter(): BucketStorageAdapter; - // Warning: (ae-incompatible-release-tags) The symbol "generateSyncStreamImplementation" is marked as @public, but its signature references "CreateSyncImplementationOptions" which is marked as @internal - // Warning: (ae-incompatible-release-tags) The symbol "generateSyncStreamImplementation" is marked as @public, but its signature references "StreamingSyncImplementation" which is marked as @internal - // - // (undocumented) - protected abstract generateSyncStreamImplementation(connector: PowerSyncBackendConnector, options: CreateSyncImplementationOptions): StreamingSyncImplementation; - // Warning: (ae-incompatible-release-tags) The symbol "generateTriggerManagerConfig" is marked as @public, but its signature references "TriggerManagerConfig" which is marked as @internal - protected generateTriggerManagerConfig(): TriggerManagerConfig; - get(sql: string, parameters?: any[]): Promise; - getAll(sql: string, parameters?: any[]): Promise; - getClientId(): Promise; - getCrudBatch(limit?: number): Promise; - getCrudTransactions(): AsyncIterable; - getNextCrudTransaction(): Promise; - getOptional(sql: string, parameters?: any[]): Promise; - getUploadQueueStats(includeSize?: boolean): Promise; - init(): Promise; - protected initialize(): Promise; - abstract _initialize(): Promise; - // (undocumented) - protected _isReadyPromise: Promise; - // (undocumented) - protected loadVersion(): Promise; - // (undocumented) - logger: PowerSyncLogger; - onChange(options?: SQLOnChangeOptions): AsyncIterable; - onChange(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void; - onChangeWithAsyncGenerator(options?: SQLWatchOptions): AsyncIterable; - onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void; - protected abstract openDBAdapter(): DBAdapter; - // (undocumented) - protected options: Options; - query(query: ArrayQueryDefinition): Query; - readLock(callback: (db: LockContext) => Promise): Promise; - readTransaction(callback: (tx: Transaction) => Promise, lockTimeout?: number): Promise; - // (undocumented) - ready: boolean; - // (undocumented) - protected resolveOfflineSyncStatus(): Promise; - resolveTables(sql: string, parameters?: any[], options?: SQLWatchOptions): Promise; - protected runExclusive(callback: () => Promise): Promise; - // Warning: (ae-incompatible-release-tags) The symbol "runExclusiveMutex" is marked as @public, but its signature references "Mutex" which is marked as @internal - // - // (undocumented) - protected runExclusiveMutex: Mutex; - get schema(): Schema<{ - [x: string]: Table; - }>; - // (undocumented) - protected _schema: Schema; - // (undocumented) - sdkVersion: string; - syncStream(name: string, params?: Record): SyncStream; - // Warning: (ae-incompatible-release-tags) The symbol "syncStreamImplementation" is marked as @public, but its signature references "StreamingSyncImplementation" which is marked as @internal - // - // (undocumented) - get syncStreamImplementation(): StreamingSyncImplementation | null; - // Warning: (ae-incompatible-release-tags) The symbol "triggers" is marked as @public, but its signature references "TriggerManager" which is marked as @alpha - readonly triggers: TriggerManager; - // Warning: (ae-incompatible-release-tags) The symbol "triggersImpl" is marked as @public, but its signature references "TriggerManagerImpl" which is marked as @internal - // - // (undocumented) - protected triggersImpl: TriggerManagerImpl; - updateSchema(schema: Schema): Promise; - waitForFirstSync(request?: AbortSignal | { - signal?: AbortSignal; - priority?: number; - }): Promise; - // (undocumented) - waitForReady(): Promise; - waitForStatus(predicate: (status: SyncStatus) => any, signal?: AbortSignal): Promise; - watch(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable; - watch(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void; - watchWithAsyncGenerator(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable; - watchWithCallback(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void; - writeLock(callback: (db: LockContext) => Promise): Promise; - writeTransaction(callback: (tx: Transaction) => Promise, lockTimeout?: number): Promise; -} - -// Warning: (ae-forgotten-export) The symbol "MetaBaseObserver" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "WatchedQueryProcessorListener" needs to be exported by the entry point index.d.ts -// Warning: (ae-internal-missing-underscore) The name "AbstractQueryProcessor" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export abstract class AbstractQueryProcessor extends MetaBaseObserver> implements WatchedQuery { - constructor(options: AbstractQueryProcessorOptions); - // (undocumented) - protected abortController: AbortController; - // (undocumented) - close(): Promise; - // (undocumented) - get closed(): boolean; - // (undocumented) - protected _closed: boolean; - // (undocumented) - protected constructInitialState(): WatchedQueryState; - // (undocumented) - protected disposeListeners: (() => void) | null; - protected init(signal: AbortSignal): Promise; - // (undocumented) - protected initialized: Promise; - protected iterateAsyncListenersWithError(callback: (listener: Partial>) => Promise | void): Promise; - protected abstract linkQuery(options: LinkQueryOptions): Promise; - // (undocumented) - protected options: AbstractQueryProcessorOptions; - // (undocumented) - protected get reportFetching(): boolean; - protected runWithReporting(callback: () => Promise): Promise; - // (undocumented) - readonly state: WatchedQueryState; - updateSettings(settings: Settings): Promise; - // (undocumented) - protected updateSettingsInternal(settings: Settings, signal: AbortSignal): Promise; - // (undocumented) - protected updateState(update: Partial>): Promise; -} - -// Warning: (ae-internal-missing-underscore) The name "AbstractQueryProcessorOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface AbstractQueryProcessorOptions { - // (undocumented) - db: AbstractPowerSyncDatabase; - // (undocumented) - placeholderData: Data; - // (undocumented) - watchOptions: Settings; -} - -// Warning: (ae-internal-missing-underscore) The name "AbstractRemote" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export abstract class AbstractRemote { - constructor(connector: RemoteConnector, logger: PowerSyncLogger, options?: Partial); - // (undocumented) - protected buildRequest(path: string): Promise<{ - url: string; - headers: { - 'content-type': string; - Authorization: string; - 'x-user-agent': string; - }; - }>; - // (undocumented) - protected connector: RemoteConnector; - // (undocumented) - protected createSocket(url: string): WebSocket; - // (undocumented) - createTextDecoder(): TextDecoder; - // (undocumented) - protected credentials: PowerSyncCredentials | null; - // (undocumented) - get fetch(): FetchImplementation; - fetchCredentials(): Promise; - fetchStream(options: SyncStreamOptions): Promise>; - protected fetchStreamRaw(options: SyncStreamOptions): Promise<{ - isBson: boolean; - stream: SimpleAsyncIterator; - }>; - // (undocumented) - get(path: string, headers?: Record): Promise; - getCredentials(): Promise; - // (undocumented) - getUserAgent(): string; - invalidateCredentials(): void; - // (undocumented) - protected logger: PowerSyncLogger; - // (undocumented) - protected options: AbstractRemoteOptions; - // (undocumented) - post(path: string, data: any, headers?: Record): Promise; - prefetchCredentials(): Promise; - socketStreamRaw(options: SocketSyncStreamOptions): Promise>; - // (undocumented) - protected get supportsStreamingBinaryResponses(): boolean; -} - -// Warning: (ae-internal-missing-underscore) The name "AbstractRemoteOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export type AbstractRemoteOptions = { - socketUrlTransformer: (url: string) => string; - fetchImplementation: FetchImplementation | FetchImplementationProvider; - fetchOptions?: {}; -}; - -// Warning: (ae-internal-missing-underscore) The name "AbstractStreamingSyncImplementation" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export abstract class AbstractStreamingSyncImplementation extends BaseObserver implements StreamingSyncImplementation { - constructor(options: AbstractStreamingSyncImplementationOptions); - // (undocumented) - protected abortController: AbortController | null; - // (undocumented) - connect(options: ResolvedSyncOptions): Promise; - // (undocumented) - protected crudUpdateListener?: () => void; - // (undocumented) - disconnect(): Promise; - // (undocumented) - dispose(): Promise; - // (undocumented) - getWriteCheckpoint(): Promise; - // (undocumented) - get isConnected(): boolean; - // (undocumented) - get lastSyncedAt(): Date | undefined; - // (undocumented) - protected logger: PowerSyncLogger; - // (undocumented) - markConnectionMayHaveChanged(): void; - // (undocumented) - abstract obtainLock(lockOptions: LockOptions_2): Promise; - // (undocumented) - protected options: AbstractStreamingSyncImplementationOptions; - // Warning: (ae-forgotten-export) The symbol "RustIterationResult" needs to be exported by the entry point index.d.ts - // - // (undocumented) - protected streamingSyncIteration(signal: AbortSignal, options: ResolvedSyncOptions): Promise; - // (undocumented) - protected streamingSyncPromise?: Promise<[void, void]>; - // (undocumented) - syncStatus: SyncStatus; - // (undocumented) - triggerCrudUpload(): void; - // (undocumented) - updateSubscriptions(subscriptions: SubscribedStream[]): void; - // (undocumented) - protected updateSyncStatus(options: SyncStatusOptions): void; - // (undocumented) - waitForReady(): Promise; - // (undocumented) - waitForStatus(status: SyncStatusOptions): Promise; - // (undocumented) - waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise; -} - -// Warning: (ae-internal-missing-underscore) The name "AbstractStreamingSyncImplementationOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface AbstractStreamingSyncImplementationOptions { - // (undocumented) - adapter: BucketStorageAdapter; - identifier?: string; - // (undocumented) - logger: PowerSyncLogger; - // (undocumented) - remote: AbstractRemote; - serializedSchema: any; - // (undocumented) - subscriptions: SubscribedStream[]; - // (undocumented) - uploadCrud: () => Promise; -} +// @public @deprecated (undocumented) +export type AbstractPowerSyncDatabase = CommonPowerSyncDatabase; // @public export class ArrayComparator implements WatchedQueryComparator { @@ -332,11 +35,11 @@ export const ATTACHMENT_TABLE = "attachments"; // @alpha export class AttachmentContext { - constructor(db: AbstractPowerSyncDatabase, tableName: string | undefined, logger: PowerSyncLogger, archivedCacheLimit: number); + constructor(db: CommonPowerSyncDatabase, tableName: string | undefined, logger: PowerSyncLogger, archivedCacheLimit: number); readonly archivedCacheLimit: number; // (undocumented) clearQueue(): Promise; - readonly db: AbstractPowerSyncDatabase; + readonly db: CommonPowerSyncDatabase; // (undocumented) deleteArchivedAttachments(callback?: (attachments: AttachmentRecord[]) => Promise): Promise; deleteAttachment(attachmentId: string): Promise; @@ -367,7 +70,7 @@ export function attachmentFromSql(row: any): AttachmentRecord; // @alpha export class AttachmentQueue implements AttachmentQueue { constructor(input: { - db: AbstractPowerSyncDatabase; + db: CommonPowerSyncDatabase; remoteStorage: RemoteStorageAdapter; localStorage: LocalStorageAdapter; watchAttachments: (onUpdate: (attachment: WatchedAttachmentItem[]) => Promise, signal: AbortSignal) => void; @@ -406,7 +109,7 @@ export class AttachmentQueue implements AttachmentQueue { stopSync(): Promise; readonly syncIntervalMs: number; syncStorage(): Promise; - readonly syncThrottleDuration: number; + readonly syncThrottleDuration?: number; readonly tableName: string; verifyAttachments(): Promise; withAttachmentContext(callback: (context: AttachmentContext) => Promise): Promise; @@ -434,17 +137,6 @@ export interface AttachmentRecord { timestamp?: number; } -// Warning: (ae-internal-missing-underscore) The name "AttachmentService" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class AttachmentService { - constructor(db: AbstractPowerSyncDatabase, logger: PowerSyncLogger, tableName?: string, archivedCacheLimit?: number); - watchActiveAttachments(input?: { - throttleMs?: number; - }): DifferentialWatchedQuery; - withContext(callback: (context: AttachmentContext) => Promise): Promise; -} - // @alpha export enum AttachmentState { // (undocumented) @@ -473,25 +165,18 @@ export type BaseColumnType = { type: ColumnType; }; +// @alpha +export interface BaseCreateDiffTriggerOptions { + columns?: string[]; + hooks?: TriggerCreationHooks; + source: string; + useStorage?: boolean; + when: Partial>; +} + // @public (undocumented) export type BaseListener = Record any) | undefined>; -// Warning: (ae-internal-missing-underscore) The name "BaseObserver" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export class BaseObserver implements BaseObserverInterface { - constructor(); - // (undocumented) - dispose(): void; - // (undocumented) - iterateAsyncListeners(cb: (listener: Partial) => Promise): Promise; - // (undocumented) - iterateListeners(cb: (listener: Partial) => any): void; - // (undocumented) - protected listeners: Set>; - registerListener(listener: Partial): () => void; -} - // @public (undocumented) export interface BaseObserverInterface { // (undocumented) @@ -523,38 +208,6 @@ export interface BatchedUpdateNotification { tables: string[]; } -// Warning: (ae-internal-missing-underscore) The name "BucketStorageAdapter" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface BucketStorageAdapter extends BaseObserverInterface, Disposable_2 { - control(op: PowerSyncControlCommand, payload: string | Uint8Array | null): Promise; - getClientId(): Promise; - // (undocumented) - getCrudBatch(limit?: number): Promise; - // (undocumented) - getMaxOpId(): string; - // (undocumented) - hasCrud(): Promise; - // (undocumented) - hasMigratedSubkeys(): Promise; - // (undocumented) - init(): Promise; - // (undocumented) - migrateToFixedSubkeys(): Promise; - // (undocumented) - nextCrudItem(): Promise; - // (undocumented) - updateLocalTarget(cb: () => Promise): Promise; -} - -// Warning: (ae-internal-missing-underscore) The name "BucketStorageListener" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface BucketStorageListener extends BaseListener { - // (undocumented) - crudUpdate: () => void; -} - // @public (undocumented) export class Column { constructor(options: ColumnOptions); @@ -599,6 +252,67 @@ export enum ColumnType { TEXT = "TEXT" } +// @public (undocumented) +export interface CommonPowerSyncDatabase extends BaseObserverInterface { + close(options?: PowerSyncCloseOptions): Promise; + readonly closed: boolean; + connect(connector: PowerSyncBackendConnector, options?: SyncOptions): Promise; + get connected(): boolean; + // (undocumented) + get connecting(): boolean; + // @internal (undocumented) + createMutex(): Mutex; + readonly currentStatus: SyncStatus; + customQuery(query: WatchCompatibleQuery): Query; + get database(): DBAdapter; + disconnect(): Promise; + disconnectAndClear(options?: DisconnectAndClearOptions): Promise; + execute(sql: string, parameters?: any[]): Promise; + executeBatch(sql: string, parameters?: any[][]): Promise; + executeRaw(sql: string, parameters?: any[]): Promise; + get(sql: string, parameters?: any[]): Promise; + getAll(sql: string, parameters?: any[]): Promise; + getClientId(): Promise; + getCrudBatch(limit?: number): Promise; + getCrudTransactions(): AsyncIterable; + getNextCrudTransaction(): Promise; + getOptional(sql: string, parameters?: any[]): Promise; + getUploadQueueStats(includeSize?: boolean): Promise; + init(): Promise; + // (undocumented) + readonly logger: PowerSyncLogger; + onChange(options?: SQLOnChangeOptions): AsyncIterable; + onChange(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void; + onChangeWithAsyncGenerator(options?: SQLWatchOptions): AsyncIterable; + onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void; + query(query: ArrayQueryDefinition): Query; + readLock(callback: (db: LockContext) => Promise): Promise; + readTransaction(callback: (tx: Transaction) => Promise, lockTimeout?: number): Promise; + // (undocumented) + readonly ready: boolean; + resolveTables(sql: string, parameters?: any[], options?: SQLWatchOptions): Promise; + readonly schema: Schema; + // (undocumented) + readonly sdkVersion: string; + syncStream(name: string, params?: Record): SyncStream; + // @alpha + readonly triggers: TriggerManager; + updateSchema(schema: Schema): Promise; + waitForFirstSync(request?: AbortSignal | { + signal?: AbortSignal; + priority?: number; + }): Promise; + // (undocumented) + waitForReady(): Promise; + waitForStatus(predicate: (status: SyncStatus) => any, signal?: AbortSignal): Promise; + watch(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable; + watch(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void; + watchWithAsyncGenerator(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable; + watchWithCallback(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void; + writeLock(callback: (db: LockContext) => Promise): Promise; + writeTransaction(callback: (tx: Transaction) => Promise, lockTimeout?: number): Promise; +} + // @public (undocumented) export interface CompilableQuery { // (undocumented) @@ -608,7 +322,7 @@ export interface CompilableQuery { } // @public (undocumented) -export function compilableQueryWatch(db: AbstractPowerSyncDatabase, query: CompilableQuery, handler: CompilableQueryWatchHandler, options?: SQLWatchOptions): void; +export function compilableQueryWatch(db: CommonPowerSyncDatabase, query: CompilableQuery, handler: CompilableQueryWatchHandler, options?: SQLWatchOptions): void; // @public (undocumented) export interface CompilableQueryWatchHandler { @@ -626,84 +340,6 @@ export interface CompiledQuery { readonly sql: string; } -// Warning: (ae-internal-missing-underscore) The name "ConnectionClosedError" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class ConnectionClosedError extends Error { - constructor(message: string); - // (undocumented) - static MATCHES(input: any): boolean; - // (undocumented) - static NAME: string; -} - -// Warning: (ae-internal-missing-underscore) The name "ConnectionManager" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export class ConnectionManager extends BaseObserver { - constructor(options: ConnectionManagerOptions); - get activeStreams(): { - name: string; - params: Record | null; - }[]; - // (undocumented) - close(): Promise; - // (undocumented) - connect(connector: PowerSyncBackendConnector, options: SyncOptions, serializedSchema: any): Promise; - protected connectingPromise: Promise | null; - // (undocumented) - protected connectInternal(): Promise; - // (undocumented) - get connectionOptions(): Required | null; - // (undocumented) - get connector(): PowerSyncBackendConnector | null; - disconnect(): Promise; - protected disconnectingPromise: Promise | null; - // (undocumented) - protected disconnectInternal(): Promise; - // (undocumented) - get logger(): PowerSyncLogger; - // (undocumented) - protected options: ConnectionManagerOptions; - // Warning: (ae-forgotten-export) The symbol "StoredConnectionOptions" needs to be exported by the entry point index.d.ts - protected pendingConnectionOptions: StoredConnectionOptions | null; - // (undocumented) - protected performDisconnect(): Promise; - // (undocumented) - stream(adapter: InternalSubscriptionAdapter, name: string, parameters: Record | null): SyncStream; - protected syncDisposer: (() => Promise | void) | null; - // (undocumented) - syncStreamImplementation: StreamingSyncImplementation | null; - protected syncStreamInitPromise: Promise | null; -} - -// Warning: (ae-internal-missing-underscore) The name "ConnectionManagerListener" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface ConnectionManagerListener extends BaseListener { - // (undocumented) - syncStreamCreated: (sync: StreamingSyncImplementation) => void; -} - -// Warning: (ae-internal-missing-underscore) The name "ConnectionManagerOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface ConnectionManagerOptions { - // (undocumented) - createSyncImplementation(connector: PowerSyncBackendConnector, options: CreateSyncImplementationOptions): Promise; - // (undocumented) - logger: PowerSyncLogger; -} - -// Warning: (ae-internal-missing-underscore) The name "ConnectionManagerSyncImplementationResult" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface ConnectionManagerSyncImplementationResult { - onDispose: () => Promise | void; - // (undocumented) - sync: StreamingSyncImplementation; -} - // @public (undocumented) export interface ConnectionPool extends BaseObserverInterface { // (undocumented) @@ -717,29 +353,9 @@ export interface ConnectionPool extends BaseObserverInterface writeLock: (fn: (tx: LockContext) => Promise, options?: DBLockOptions) => Promise; } -// Warning: (ae-internal-missing-underscore) The name "ControlledExecutor" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export class ControlledExecutor { - constructor(task: (param: T) => Promise | void, options?: ControlledExecutorOptions); - // (undocumented) - dispose(): void; - // (undocumented) - schedule(param: T): void; -} - -// Warning: (ae-internal-missing-underscore) The name "ControlledExecutorOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface ControlledExecutorOptions { - throttleEnabled?: boolean; -} - // @public export function createConsoleLogger(options?: Partial): PowerSyncLogger & CreateLoggerOptions; -// Warning: (ae-forgotten-export) The symbol "BaseCreateDiffTriggerOptions" needs to be exported by the entry point index.d.ts -// // @alpha export interface CreateDiffTriggerOptions extends BaseCreateDiffTriggerOptions { destination: string; @@ -752,16 +368,6 @@ export interface CreateLoggerOptions { prefix: string; } -// Warning: (ae-internal-missing-underscore) The name "CreateSyncImplementationOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export interface CreateSyncImplementationOptions { - // (undocumented) - serializedSchema: any; - // (undocumented) - subscriptions: SubscribedStream[]; -} - // @public export class CrudBatch { constructor( @@ -774,26 +380,18 @@ export class CrudBatch { } // @public -export class CrudEntry { - constructor(clientId: number, op: UpdateType, table: string, id: string, transactionId?: number, opData?: Record, previousValues?: Record, metadata?: string); +export interface CrudEntry { clientId: number; // (undocumented) equals(entry: CrudEntry): boolean; - // Warning: (ae-forgotten-export) The symbol "CrudEntryJSON" needs to be exported by the entry point index.d.ts - // - // (undocumented) - static fromRow(dbRow: CrudEntryJSON): CrudEntry; - // @deprecated - hashCode(): string; id: string; metadata?: string; op: UpdateType; opData?: Record; previousValues?: Record; table: string; - toComparisonArray(): (string | number | Record | undefined)[]; - // Warning: (ae-forgotten-export) The symbol "CrudEntryOutputJSON" needs to be exported by the entry point index.d.ts - toJSON(): CrudEntryOutputJSON; + toComparisonArray(): unknown[]; + toJSON(): unknown; transactionId?: number; } @@ -873,91 +471,10 @@ export function DBGetUtilsDefaultMixin Omi }; } & TBase; -// @public (undocumented) -export interface DBLockOptions { - // (undocumented) - timeoutMs?: number; -} - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_INDEX_COLUMN_OPTIONS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const DEFAULT_INDEX_COLUMN_OPTIONS: Partial; - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_INDEX_OPTIONS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const DEFAULT_INDEX_OPTIONS: Partial; - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_LOCK_TIMEOUT_MS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export const DEFAULT_LOCK_TIMEOUT_MS = 120000; - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_POWERSYNC_CLOSE_OPTIONS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions; - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_REMOTE_OPTIONS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const DEFAULT_REMOTE_OPTIONS: AbstractRemoteOptions; - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_ROW_COMPARATOR" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export const DEFAULT_ROW_COMPARATOR: DifferentialWatchedQueryComparator; - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_TABLE_OPTIONS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const DEFAULT_TABLE_OPTIONS: { - indexes: never[]; - insertOnly: boolean; - localOnly: boolean; - trackPrevious: boolean; - trackMetadata: boolean; - ignoreEmptyUpdates: boolean; -}; - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_WATCH_QUERY_OPTIONS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions; - -// Warning: (ae-internal-missing-underscore) The name "DEFAULT_WATCH_THROTTLE_MS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const DEFAULT_WATCH_THROTTLE_MS = 30; - -// Warning: (ae-internal-missing-underscore) The name "DifferentialQueryProcessor" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class DifferentialQueryProcessor extends AbstractQueryProcessor>, DifferentialWatchedQuerySettings> implements DifferentialWatchedQuery { - constructor(options: DifferentialQueryProcessorOptions); - // (undocumented) - protected comparator: DifferentialWatchedQueryComparator; - // Warning: (ae-forgotten-export) The symbol "DataHashMap" needs to be exported by the entry point index.d.ts - // - // (undocumented) - protected differentiate(current: RowType[], previousMap: DataHashMap): { - diff: WatchedQueryDifferential; - map: DataHashMap; - hasChanged: boolean; - }; - // (undocumented) - protected linkQuery(options: LinkQueryOptions>): Promise; - // (undocumented) - protected options: DifferentialQueryProcessorOptions; -} - -// Warning: (ae-internal-missing-underscore) The name "DifferentialQueryProcessorOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface DifferentialQueryProcessorOptions extends AbstractQueryProcessorOptions> { +// @public (undocumented) +export interface DBLockOptions { // (undocumented) - rowComparator?: DifferentialWatchedQueryComparator; + timeoutMs?: number; } // @public (undocumented) @@ -1008,17 +525,6 @@ interface Disposable_2 { } export { Disposable_2 as Disposable } -// Warning: (ae-internal-missing-underscore) The name "EMPTY_DIFFERENTIAL" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export const EMPTY_DIFFERENTIAL: { - added: never[]; - all: never[]; - removed: never[]; - updated: never[]; - unchanged: never[]; -}; - // @alpha (undocumented) export enum EncodingType { // (undocumented) @@ -1045,19 +551,6 @@ export function extractTableUpdates(update: BatchedUpdateNotification | UpdateNo // @public export const FalsyComparator: WatchedQueryComparator; -// Warning: (ae-internal-missing-underscore) The name "FetchImplementation" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export type FetchImplementation = typeof fetch_2; - -// Warning: (ae-internal-missing-underscore) The name "FetchImplementationProvider" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class FetchImplementationProvider { - // (undocumented) - getFetch(): FetchImplementation; -} - // @public (undocumented) export enum FetchStrategy { Buffered = "buffered", @@ -1071,7 +564,7 @@ export class GetAllQuery implements WatchCompatibleQuery; // (undocumented) protected options: GetAllQueryOptions; @@ -1144,36 +637,20 @@ export interface IndexOptions { // @public (undocumented) export type IndexShorthand = Record; -// Warning: (ae-internal-missing-underscore) The name "InternalSubscriptionAdapter" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface InternalSubscriptionAdapter { - // (undocumented) - firstStatusMatching(predicate: (status: SyncStatus) => any, abort?: AbortSignal): Promise; - // (undocumented) - resolveOfflineSyncStatus(): Promise; - // (undocumented) - rustSubscriptionsCommand(payload: any): Promise; -} - -// Warning: (ae-internal-missing-underscore) The name "InvalidSQLCharacters" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const InvalidSQLCharacters: RegExp; - // Warning: (ae-internal-missing-underscore) The name "isBatchedUpdateNotification" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) export function isBatchedUpdateNotification(update: BatchedUpdateNotification | UpdateNotification): update is BatchedUpdateNotification; -// Warning: (ae-internal-missing-underscore) The name "LinkQueryOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface LinkQueryOptions { - // (undocumented) - abortSignal: AbortSignal; +// @public +export type ListenerCounts = Partial> & { + total: number; +}; + +// @public (undocumented) +export interface ListenerMetaManager extends BaseObserverInterface> { // (undocumented) - settings: Settings; + counts: ListenerCounts; } // @alpha @@ -1194,29 +671,6 @@ export interface LockContext extends SqlExecutor, DBGetUtils { connectionType?: 'writer' | 'queryOnly' | 'readOnly'; } -// @internal -interface LockOptions_2 { - // (undocumented) - callback: () => Promise; - // (undocumented) - signal?: AbortSignal; - // (undocumented) - type: LockType; -} - -// Warning: (ae-internal-missing-underscore) The name "LockOptions" should be prefixed with an underscore because the declaration is marked as @internal -export { LockOptions_2 as LockOptions } - -// Warning: (ae-internal-missing-underscore) The name "LockType" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export enum LockType { - // (undocumented) - CRUD = "crud", - // (undocumented) - SYNC = "sync" -} - // @public (undocumented) export const LogLevels: { readonly trace: 10; @@ -1233,83 +687,29 @@ export interface LogRecord { message: string; } -// Warning: (ae-internal-missing-underscore) The name "MAX_AMOUNT_OF_COLUMNS" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export const MAX_AMOUNT_OF_COLUMNS = 1999; - -// Warning: (ae-internal-missing-underscore) The name "MAX_OP_ID" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const MAX_OP_ID = "9223372036854775807"; - -// Warning: (ae-internal-missing-underscore) The name "MEMORY_TRIGGER_CLAIM_MANAGER" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const MEMORY_TRIGGER_CLAIM_MANAGER: TriggerClaimManager; - -// Warning: (ae-forgotten-export) The symbol "MutableDeep" needs to be exported by the entry point index.d.ts -// Warning: (ae-internal-missing-underscore) The name "MutableWatchedQueryState" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export type MutableWatchedQueryState = { - -readonly [P in keyof WatchedQueryState]: MutableDeep[P]>; -}; - -// Warning: (ae-internal-missing-underscore) The name "Mutex" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class Mutex { - // (undocumented) - acquire(abort?: AbortSignal): Promise; +// @public (undocumented) +export interface MetaBaseObserverInterface extends BaseObserverInterface { // (undocumented) - runExclusive(fn: () => PromiseLike | T, abort?: AbortSignal): Promise; + listenerMeta: ListenerMetaManager; } -// Warning: (ae-internal-missing-underscore) The name "OnChangeQueryProcessor" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class OnChangeQueryProcessor extends AbstractQueryProcessor> { - constructor(options: OnChangeQueryProcessorOptions); - // (undocumented) - protected checkEquality(current: Data, previous: Data): boolean; - // (undocumented) - protected linkQuery(options: LinkQueryOptions): Promise; +// @public +export interface MetaListener extends BaseListener { // (undocumented) - protected options: OnChangeQueryProcessorOptions; + listenersChanged?: (counts: ListenerCounts) => void; } -// Warning: (ae-internal-missing-underscore) The name "OnChangeQueryProcessorOptions" should be prefixed with an underscore because the declaration is marked as @internal +// Warning: (ae-internal-missing-underscore) The name "Mutex" should be prefixed with an underscore because the declaration is marked as @internal // -// @internal (undocumented) -export interface OnChangeQueryProcessorOptions extends AbstractQueryProcessorOptions> { +// @internal +export interface Mutex { // (undocumented) - comparator?: WatchedQueryComparator; + runExclusive(fn: () => PromiseLike | T, abort?: AbortSignal): Promise; } -// Warning: (ae-internal-missing-underscore) The name "openDatabase" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export function openDatabase(source: DatabaseSource, defaultFactory: (options: T) => DBAdapter): DBAdapter; - // @public export type OpId = string; -// Warning: (ae-internal-missing-underscore) The name "ParsedQuery" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface ParsedQuery { - // (undocumented) - parameters: any[]; - // (undocumented) - sqlStatement: string; -} - -// Warning: (ae-internal-missing-underscore) The name "parseQuery" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const parseQuery: (query: string | CompilableQuery, parameters: any[]) => ParsedQuery; - // @public export type PendingStatement = { sql: string; @@ -1324,7 +724,7 @@ export type PendingStatementParameter = 'Id' | { // @public (undocumented) export interface PowerSyncBackendConnector { fetchCredentials: () => Promise; - uploadData: (database: AbstractPowerSyncDatabase) => Promise; + uploadData: (database: CommonPowerSyncDatabase) => Promise; } // @public (undocumented) @@ -1332,27 +732,6 @@ export interface PowerSyncCloseOptions { disconnect?: boolean; } -// Warning: (ae-internal-missing-underscore) The name "PowerSyncControlCommand" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export enum PowerSyncControlCommand { - CONNECTION_STATE = "connection", - // (undocumented) - NOTIFY_CRUD_UPLOAD_COMPLETED = "completed_upload", - // (undocumented) - NOTIFY_TOKEN_REFRESHED = "refreshed_token", - // (undocumented) - PROCESS_BSON_LINE = "line_binary", - // (undocumented) - PROCESS_TEXT_LINE = "line_text", - // (undocumented) - START = "start", - // (undocumented) - STOP = "stop", - // (undocumented) - UPDATE_SUBSCRIPTIONS = "update_subscriptions" -} - // @public (undocumented) export interface PowerSyncCredentials { // (undocumented) @@ -1363,13 +742,17 @@ export interface PowerSyncCredentials { token: string; } +// @public (undocumented) +export interface PowerSyncDatabaseConstructor { + // (undocumented) + new (options: Options): CommonPowerSyncDatabase; +} + // @public (undocumented) export type PowerSyncDatabaseOptions = BasePowerSyncDatabaseOptions & DatabaseSource; -// Warning: (ae-incompatible-release-tags) The symbol "PowerSyncDBListener" is marked as @public, but its signature references "StreamingSyncImplementationListener" which is marked as @internal -// // @public (undocumented) -export interface PowerSyncDBListener extends StreamingSyncImplementationListener { +export interface PowerSyncDBListener extends BaseListener { // (undocumented) closed: () => Promise | void; // (undocumented) @@ -1378,6 +761,8 @@ export interface PowerSyncDBListener extends StreamingSyncImplementationListener initialized: () => void; // (undocumented) schemaChanged: (schema: Schema) => void; + // (undocumented) + statusChanged?: ((status: SyncStatus) => void) | undefined; } // @public @@ -1393,22 +778,6 @@ export interface ProgressWithOperations { totalOperations: number; } -// Warning: (ae-internal-missing-underscore) The name "PSInternalTable" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export enum PSInternalTable { - // (undocumented) - BUCKETS = "ps_buckets", - // (undocumented) - CRUD = "ps_crud", - // (undocumented) - DATA = "ps_data", - // (undocumented) - OPLOG = "ps_oplog", - // (undocumented) - UNTYPED = "ps_untyped" -} - // @public (undocumented) export interface Query { differentialWatch(options?: DifferentialWatchedQueryOptions): DifferentialWatchedQuery; @@ -1435,14 +804,6 @@ export type QueryResult = { // @public export type RawTableType = RawTableTypeWithStatements | InferredRawTableType; -// Warning: (ae-internal-missing-underscore) The name "RemoteConnector" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export type RemoteConnector = { - fetchCredentials: () => Promise; - invalidateCredentials?: () => void; -}; - // @alpha export interface RemoteStorageAdapter { deleteFile(attachment: AttachmentRecord): Promise; @@ -1450,16 +811,6 @@ export interface RemoteStorageAdapter { uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise; } -// Warning: (ae-internal-missing-underscore) The name "ResolvedSyncOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export type ResolvedSyncOptions = Required; - -// Warning: (ae-internal-missing-underscore) The name "resolveSyncOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export function resolveSyncOptions(options: SyncOptions): ResolvedSyncOptions; - // @public (undocumented) export type RowType> = { [K in keyof T['columnMap']]: ExtractColumnValueType; @@ -1477,11 +828,6 @@ export enum RowUpdateType { SQLITE_UPDATE = 23 } -// Warning: (ae-internal-missing-underscore) The name "runOnSchemaChange" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export function runOnSchemaChange(callback: (signal: AbortSignal) => void, db: AbstractPowerSyncDatabase, options?: SQLWatchOptions): void; - // @alpha export function sanitizeSQL(strings: TemplateStringsArray, ...values: any[]): string; @@ -1516,42 +862,6 @@ export type SchemaTableType = { [K in keyof S]: RowType; }; -// Warning: (ae-internal-missing-underscore) The name "Semaphore" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class Semaphore { - constructor(elements: Iterable); - requestAll(abort?: AbortSignal): Promise<{ - items: T[]; - release: UnlockFn; - }>; - requestOne(abort?: AbortSignal): Promise<{ - item: T; - release: UnlockFn; - }>; - // (undocumented) - readonly size: number; -} - -// Warning: (ae-internal-missing-underscore) The name "SimpleAsyncIterator" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export type SimpleAsyncIterator = Pick, 'next'>; - -// Warning: (ae-internal-missing-underscore) The name "SocketSyncStreamOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface SocketSyncStreamOptions { - // (undocumented) - abortSignal: AbortSignal; - // (undocumented) - data: unknown; - // (undocumented) - fetchStrategy: FetchStrategy; - // (undocumented) - path: string; -} - // @public (undocumented) export interface SqlExecutor { execute: (query: string, params?: any[] | undefined) => Promise; @@ -1560,44 +870,6 @@ export interface SqlExecutor { executeRaw: (query: string, params?: any[] | undefined) => Promise; } -// Warning: (ae-internal-missing-underscore) The name "SqliteBucketStorage" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export class SqliteBucketStorage extends BaseObserver implements BucketStorageAdapter { - constructor(db: DBAdapter, logger: PowerSyncLogger); - // (undocumented) - control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise; - // (undocumented) - dispose(): Promise; - // (undocumented) - getClientId(): Promise; - // (undocumented) - _getClientId(): Promise; - getCrudBatch(limit?: number): Promise; - // (undocumented) - getMaxOpId(): string; - // (undocumented) - hasCrud(): Promise; - // (undocumented) - hasMigratedSubkeys(): Promise; - // (undocumented) - init(): Promise; - // (undocumented) - migrateToFixedSubkeys(): Promise; - // (undocumented) - nextCrudItem(): Promise; - // (undocumented) - static _subkeyMigrationKey: string; - // (undocumented) - tableNames: Set; - // (undocumented) - updateLocalTarget(cb: () => Promise): Promise; - // (undocumented) - writeTransaction(callback: (tx: Transaction) => Promise, options?: { - timeoutMs: number; - }): Promise; -} - // @public (undocumented) export interface SQLOnChangeOptions { // @deprecated (undocumented) @@ -1636,75 +908,18 @@ export interface StandardWatchedQueryOptions extends WatchedQueryOption placeholderData?: RowType[]; } -// Warning: (ae-internal-missing-underscore) The name "StreamingSyncImplementation" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface StreamingSyncImplementation extends BaseObserverInterface, Disposable_2 { - connect(options: ResolvedSyncOptions): Promise; - disconnect(): Promise; - // (undocumented) - getWriteCheckpoint: () => Promise; - // (undocumented) - isConnected: boolean; - // (undocumented) - markConnectionMayHaveChanged(): void; - // (undocumented) - syncStatus: SyncStatus; - // (undocumented) - triggerCrudUpload: () => void; - // (undocumented) - updateSubscriptions(subscriptions: SubscribedStream[]): void; - // (undocumented) - waitForReady(): Promise; - // (undocumented) - waitForStatus(status: SyncStatusOptions): Promise; - // (undocumented) - waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise; -} - -// Warning: (ae-internal-missing-underscore) The name "StreamingSyncImplementationListener" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface StreamingSyncImplementationListener extends BaseListener { - statusChanged?: ((status: SyncStatus) => void) | undefined; - statusUpdated?: ((statusUpdate: SyncStatusOptions) => void) | undefined; -} - // Warning: (ae-forgotten-export) The symbol "JSONValue" needs to be exported by the entry point index.d.ts // // @public (undocumented) export type StreamingSyncRequestParameterType = JSONValue; -// Warning: (ae-internal-missing-underscore) The name "SubscribedStream" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export type SubscribedStream = { - name: string; - params: Record | null; -}; - // @public (undocumented) export type SyncDataFlowStatus = Partial<{ - downloading: boolean; uploading: boolean; downloadError?: Error; uploadError?: Error; - downloadProgress: InternalProgressInformation | null; - internalStreamSubscriptions: CoreStreamSubscription[] | null; }>; -// Warning: (ae-internal-missing-underscore) The name "SyncingService" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class SyncingService { - constructor(attachmentService: AttachmentService, localStorage: LocalStorageAdapter, remoteStorage: RemoteStorageAdapter, logger: PowerSyncLogger, errorHandler?: AttachmentErrorHandler); - deleteArchivedAttachments(context: AttachmentContext): Promise; - deleteAttachment(attachment: AttachmentRecord, context: AttachmentContext): Promise; - downloadAttachment(attachment: AttachmentRecord): Promise; - processAttachments(attachments: AttachmentRecord[], context: AttachmentContext): Promise; - uploadAttachment(attachment: AttachmentRecord): Promise; -} - // @public export interface SyncOptions { appMetadata?: Record; @@ -1728,60 +943,27 @@ export interface SyncPriorityStatus { } // @public -export class SyncProgress implements ProgressWithOperations { - constructor(internal: InternalProgressInformation); - // (undocumented) - downloadedFraction: number; - // (undocumented) - downloadedOperations: number; - // (undocumented) - protected internal: InternalProgressInformation; - // (undocumented) - totalOperations: number; +export interface SyncProgress extends ProgressWithOperations { untilPriority(priority: number): ProgressWithOperations; } // @public (undocumented) -export class SyncStatus { - // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "SyncStatusOptions" which is marked as @internal - constructor(options: SyncStatusOptions); +export interface SyncStatus { get connected(): boolean; get connecting(): boolean; get dataFlowStatus(): SyncDataFlowStatus; + get downloading(): boolean; get downloadProgress(): SyncProgress | null; forStream(stream: SyncStreamDescription): SyncStreamStatus | undefined; getMessage(): string; get hasSynced(): boolean | undefined; isEqual(status: SyncStatus): boolean; get lastSyncedAt(): Date | undefined; - // Warning: (ae-incompatible-release-tags) The symbol "options" is marked as @public, but its signature references "SyncStatusOptions" which is marked as @internal - // - // (undocumented) - protected options: SyncStatusOptions; - get priorityStatusEntries(): SyncPriorityStatus[]; - protected serializeError(error?: Error): { - name: string; - message: string; - stack: string | undefined; - } | undefined; - statusForPriority(priority: number): SyncPriorityStatus; + get priorityStatusEntries(): SyncPriorityStatus[] | undefined; + statusForPriority(priority: number): SyncPriorityStatus | undefined; get syncStreams(): SyncStreamStatus[] | undefined; - // Warning: (ae-incompatible-release-tags) The symbol "toJSON" is marked as @public, but its signature references "SyncStatusOptions" which is marked as @internal - toJSON(): SyncStatusOptions; } -// Warning: (ae-internal-missing-underscore) The name "SyncStatusOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export type SyncStatusOptions = { - connected?: boolean; - connecting?: boolean; - dataFlow?: SyncDataFlowStatus; - lastSyncedAt?: Date; - hasSynced?: boolean; - priorityStatusEntries?: SyncPriorityStatus[]; -}; - // @public export interface SyncStream extends SyncStreamDescription { subscribe(options?: SyncStreamSubscribeOptions): Promise; @@ -1802,17 +984,6 @@ export interface SyncStreamDescription { parameters: Record | null; } -// Warning: (ae-internal-missing-underscore) The name "SyncStreamOptions" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export type SyncStreamOptions = { - path: string; - data: unknown; - headers?: Record; - abortSignal: AbortSignal; - fetchOptions?: Request; -}; - // @public export interface SyncStreamStatus { // (undocumented) @@ -1959,14 +1130,6 @@ export interface TableV2Options extends SharedTableOptions { indexes?: IndexShorthand; } -// Warning: (ae-internal-missing-underscore) The name "timeoutSignal" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export function timeoutSignal(timeout: number): AbortSignal; - -// @internal (undocumented) -export function timeoutSignal(timeout?: number): AbortSignal | undefined; - // @alpha export interface TrackDiffOptions extends BaseCreateDiffTriggerOptions { onChange: (context: TriggerDiffHandlerContext) => Promise; @@ -1985,14 +1148,6 @@ export interface Transaction extends LockContext { rollback: () => Promise; } -// Warning: (ae-internal-missing-underscore) The name "TriggerClaimManager" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export interface TriggerClaimManager { - checkClaim: (identifier: string) => Promise; - obtainClaim: (identifier: string) => Promise<() => Promise>; -} - // @alpha export interface TriggerCreationHooks { beforeCreate?: (context: LockContext) => Promise; @@ -2036,51 +1191,6 @@ export interface TriggerManager { trackTableDiff(options: TrackDiffOptions): Promise; } -// Warning: (ae-internal-missing-underscore) The name "TriggerManagerConfig" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export interface TriggerManagerConfig { - // (undocumented) - claimManager: TriggerClaimManager; -} - -// Warning: (ae-internal-missing-underscore) The name "TriggerManagerImpl" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export class TriggerManagerImpl implements TriggerManager { - constructor(options: TriggerManagerImplOptions); - cleanupResources(): Promise; - // (undocumented) - protected cleanupTimeout: ReturnType | null; - // (undocumented) - createDiffTrigger(options: CreateDiffTriggerOptions): Promise<(options?: TriggerRemoveCallbackOptions) => Promise>; - // (undocumented) - protected get db(): AbstractPowerSyncDatabase; - // Warning: (ae-forgotten-export) The symbol "TriggerManagerImplConfiguration" needs to be exported by the entry point index.d.ts - // - // (undocumented) - protected defaultConfig: TriggerManagerImplConfiguration; - // (undocumented) - dispose(): void; - // (undocumented) - protected generateTriggerName(operation: DiffTriggerOperation, destinationTable: string, triggerId: string): string; - // (undocumented) - protected getUUID(ctx?: LockContext): Promise; - // (undocumented) - protected isDisposed: boolean; - // Warning: (ae-forgotten-export) The symbol "TriggerManagerImplOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) - protected options: TriggerManagerImplOptions; - // (undocumented) - protected removeTriggers(tx: LockContext, triggerIds: string[]): Promise; - // (undocumented) - protected schema: Schema; - // (undocumented) - trackTableDiff(options: TrackDiffOptions): Promise; - updateDefaults(config: TriggerManagerImplConfiguration): void; -} - // @alpha export type TriggerRemoveCallback = (options?: TriggerRemoveCallbackOptions) => Promise; @@ -2090,11 +1200,6 @@ export interface TriggerRemoveCallbackOptions { context?: LockContext; } -// Warning: (ae-internal-missing-underscore) The name "UnlockFn" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export type UnlockFn = () => void; - // @public export interface UpdateNotification extends TableUpdateOperation { // (undocumented) @@ -2142,10 +1247,12 @@ export type WatchedAttachmentItem = { mediaType?: string; }; -// Warning: (ae-forgotten-export) The symbol "MetaBaseObserverInterface" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -export interface WatchedQuery = WatchedQueryListener> extends MetaBaseObserverInterface { +export interface WatchedQuery< +Data = unknown, +Settings extends WatchedQueryOptions = WatchedQueryOptions, +Listener extends WatchedQueryListener = WatchedQueryListener +> extends MetaBaseObserverInterface { close(): Promise; // (undocumented) readonly closed: boolean; @@ -2190,15 +1297,15 @@ export interface WatchedQueryListener extends BaseListener { // @public (undocumented) export enum WatchedQueryListenerEvent { // (undocumented) - CLOSED = "closed", + CLOSED = 'closed', // (undocumented) - ON_DATA = "onData", + ON_DATA = 'onData', // (undocumented) - ON_ERROR = "onError", + ON_ERROR = 'onError', // (undocumented) - ON_STATE_CHANGE = "onStateChange", + ON_STATE_CHANGE = 'onStateChange', // (undocumented) - SETTINGS_WILL_UPDATE = "settingsWillUpdate" + SETTINGS_WILL_UPDATE = 'settingsWillUpdate' } // @public (undocumented) @@ -2234,7 +1341,7 @@ export interface WatchedQueryState { // @public export interface WatchExecuteOptions { // (undocumented) - db: AbstractPowerSyncDatabase; + db: CommonPowerSyncDatabase; // (undocumented) parameters: any[]; // (undocumented) @@ -2268,11 +1375,6 @@ export interface WithDiffOptions { castOperationIdAsText?: boolean; } -// Warnings were encountered during analysis: -// -// lib/db/crud/SyncStatus.d.ts:26:5 - (ae-forgotten-export) The symbol "InternalProgressInformation" needs to be exported by the entry point index.d.ts -// lib/db/crud/SyncStatus.d.ts:30:5 - (ae-forgotten-export) The symbol "CoreStreamSubscription" needs to be exported by the entry point index.d.ts - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/common/package.json b/packages/common/package.json index e35480031..ce8d4a234 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -18,8 +18,7 @@ "files": [ "lib", "dist", - "src", - "legacy" + "src" ], "repository": { "type": "git", diff --git a/packages/common/src/client/CommonPowerSyncDatabase.ts b/packages/common/src/client/CommonPowerSyncDatabase.ts index 40273f783..6a66b52bd 100644 --- a/packages/common/src/client/CommonPowerSyncDatabase.ts +++ b/packages/common/src/client/CommonPowerSyncDatabase.ts @@ -127,6 +127,7 @@ export interface PowerSyncDatabaseConstructor { } /** + * @public * @deprecated Use {@link CommonPowerSyncDatabase} instead. */ export type AbstractPowerSyncDatabase = CommonPowerSyncDatabase; @@ -149,7 +150,7 @@ export interface CommonPowerSyncDatabase extends BaseObserverInterface(callback: (tx: Transaction) => Promise, lockTimeout?: number): Promise; /** - * This version of `watch` uses `AsyncGenerator`, for documentation see {@link AbstractPowerSyncDatabase.watchWithAsyncGenerator}. - * Can be overloaded to use a callback handler instead, for documentation see {@link AbstractPowerSyncDatabase.watchWithCallback}. + * This version of `watch` uses `AsyncGenerator`, for documentation see {@link CommonPowerSyncDatabase.watchWithAsyncGenerator}. + * Can be overloaded to use a callback handler instead, for documentation see {@link CommonPowerSyncDatabase.watchWithCallback}. * * @example * ```javascript @@ -454,7 +455,7 @@ export interface CommonPowerSyncDatabase extends BaseObserverInterface; /** - * See {@link AbstractPowerSyncDatabase.watchWithCallback}. + * See {@link CommonPowerSyncDatabase.watchWithCallback}. * * @example * ```javascript @@ -473,7 +474,7 @@ export interface CommonPowerSyncDatabase extends BaseObserverInterface; /** - * This version of `onChange` uses `AsyncGenerator`, for documentation see {@link AbstractPowerSyncDatabase.onChangeWithAsyncGenerator}. - * Can be overloaded to use a callback handler instead, for documentation see {@link AbstractPowerSyncDatabase.onChangeWithCallback}. + * This version of `onChange` uses `AsyncGenerator`, for documentation see {@link CommonPowerSyncDatabase.onChangeWithAsyncGenerator}. + * Can be overloaded to use a callback handler instead, for documentation see {@link CommonPowerSyncDatabase.onChangeWithCallback}. * * @example * ```javascript @@ -562,7 +563,7 @@ export interface CommonPowerSyncDatabase extends BaseObserverInterface; /** - * See {@link AbstractPowerSyncDatabase.onChangeWithCallback}. + * See {@link CommonPowerSyncDatabase.onChangeWithCallback}. * * @example * ```javascript @@ -580,7 +581,7 @@ export interface CommonPowerSyncDatabase extends BaseObserverInterface */ database: OpenOptions; }; - -/** - * Internal helper to turn a {@link DatabaseSource} into an opened {@link DBAdapter}. - * - * @internal - */ -export function openDatabase( - source: DatabaseSource, - defaultFactory: (options: T) => DBAdapter -): DBAdapter { - if ('opened' in source) { - return source.opened; - } else if ('factory' in source) { - return source.factory.openDB(); - } else if ('database' in source && source.database?.dbFilename) { - return defaultFactory(source.database); - } else { - // This is dead code for well-typed programs, but JavaScript users might have forgotten to pass an option - // when creating a PowerSync database instance. - throw new Error('The provided `database` option is invalid.'); - } -} diff --git a/packages/common/src/client/connection/PowerSyncBackendConnector.ts b/packages/common/src/client/connection/PowerSyncBackendConnector.ts index c185a7632..ff7f95c99 100644 --- a/packages/common/src/client/connection/PowerSyncBackendConnector.ts +++ b/packages/common/src/client/connection/PowerSyncBackendConnector.ts @@ -20,7 +20,7 @@ export interface PowerSyncBackendConnector { /** Upload local changes to the app backend. * - * Use {@link AbstractPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload. + * Use {@link CommonPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload. * * Any thrown errors will result in a retry after the configured wait period (default: 5 seconds). */ diff --git a/packages/common/src/client/sync/options.ts b/packages/common/src/client/sync/options.ts index 645564427..a21193871 100644 --- a/packages/common/src/client/sync/options.ts +++ b/packages/common/src/client/sync/options.ts @@ -48,26 +48,6 @@ export interface SyncOptions { crudUploadThrottleMs?: number; } -/** - * @internal - */ -export type ResolvedSyncOptions = Required; - -/** - * @internal - */ -export function resolveSyncOptions(options: SyncOptions): ResolvedSyncOptions { - return { - appMetadata: options.appMetadata ?? {}, - connectionMethod: options.connectionMethod ?? SyncStreamConnectionMethod.HTTP, - fetchStrategy: options.fetchStrategy ?? FetchStrategy.Buffered, - params: options.params ?? {}, - includeDefaultStreams: options.includeDefaultStreams ?? true, - retryDelayMs: options.retryDelayMs ?? 5000, - crudUploadThrottleMs: options.crudUploadThrottleMs ?? 1000 - }; -} - // TODO: This should not be part of SyncOptions. Remove the WebSocket options from @powersync/common into a separate // package and make this an option only available when creating a custom remote. diff --git a/packages/common/src/client/sync/sync-streams.ts b/packages/common/src/client/sync/sync-streams.ts index 173a02dc3..d4c89ac91 100644 --- a/packages/common/src/client/sync/sync-streams.ts +++ b/packages/common/src/client/sync/sync-streams.ts @@ -81,7 +81,7 @@ export interface SyncStreamSubscribeOptions { /** * A handle to a {@link SyncStreamDescription} that allows subscribing to the stream. * - * To obtain an instance of {@link SyncStream}, call {@link AbstractPowerSyncDatabase.syncStream}. + * To obtain an instance of {@link SyncStream}, call {@link CommonPowerSyncDatabase.syncStream}. * * @public */ diff --git a/packages/common/src/client/triggers/TriggerManager.ts b/packages/common/src/client/triggers/TriggerManager.ts index 2ebcf8292..ecec25263 100644 --- a/packages/common/src/client/triggers/TriggerManager.ts +++ b/packages/common/src/client/triggers/TriggerManager.ts @@ -163,9 +163,10 @@ export interface TriggerCreationHooks { /** * Common interface for options used in creating a diff trigger. + * + * @alpha @experimental */ - -interface BaseCreateDiffTriggerOptions { +export interface BaseCreateDiffTriggerOptions { /** * PowerSync source table/view to trigger and track changes from. * This should be present in the PowerSync database's schema. @@ -188,16 +189,18 @@ interface BaseCreateDiffTriggerOptions { * The row id is available in the `id` column. * * NB! The WHEN clauses here are added directly to the SQLite trigger creation SQL. - * Any user input strings here should be sanitized externally. The {@link when} string template function performs - * some basic sanitization, extra external sanitization is recommended. + * Any user input strings here should be sanitized externally. The {@link BaseCreateDiffTriggerOptions.when} string + * template function performs some basic sanitization, extra external sanitization is recommended. * * @example + * ```JavaScript * { * 'INSERT': sanitizeSQL`json_extract(NEW.data, '$.list_id') = ${sanitizeUUID(list.id)}`, * 'INSERT': `TRUE`, * 'UPDATE': sanitizeSQL`NEW.id = 'abcd' AND json_extract(NEW.data, '$.status') = 'active'`, * 'DELETE': sanitizeSQL`json_extract(OLD.data, '$.list_id') = 'abcd'` * } + * ``` */ when: Partial>; @@ -363,7 +366,6 @@ export interface TrackDiffOptions extends BaseCreateDiffTriggerOptions { /** * The minimum interval, in milliseconds, between {@link TrackDiffOptions.onChange} invocations. - * @default {@link DEFAULT_WATCH_THROTTLE_MS} */ throttleMs?: number; } diff --git a/packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts b/packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts index cafae6004..7ed0327f4 100644 --- a/packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts +++ b/packages/common/src/client/watched/processors/DifferentialQueryProcessor.ts @@ -70,7 +70,6 @@ export interface DifferentialWatchedQueryOptions extends WatchedQueryOp * Row comparator used to identify and compare rows in the result set. * If not provided, the default comparator will be used which keys items by their `id` property if available, * otherwise it uses JSON stringification of the entire item for keying and comparison. - * @defaultValue {@link DEFAULT_ROW_COMPARATOR} */ rowComparator?: DifferentialWatchedQueryComparator; } diff --git a/packages/common/src/db/schema/Column.ts b/packages/common/src/db/schema/Column.ts index cb41a6ec4..0b95c971a 100644 --- a/packages/common/src/db/schema/Column.ts +++ b/packages/common/src/db/schema/Column.ts @@ -45,14 +45,6 @@ const real: BaseColumnType = { type: ColumnType.REAL }; -/** - * powersync-sqlite-core limits the number of column per table to 1999, due to internal SQLite limits. - * In earlier versions this was limited to 63. - * - * @internal - */ -export const MAX_AMOUNT_OF_COLUMNS = 1999; - /** * @public */ diff --git a/packages/common/src/db/schema/Index.ts b/packages/common/src/db/schema/Index.ts index 8b662182a..31d23230a 100644 --- a/packages/common/src/db/schema/Index.ts +++ b/packages/common/src/db/schema/Index.ts @@ -9,10 +9,7 @@ export interface IndexOptions { columns?: IndexedColumn[]; } -/** - * @internal - */ -export const DEFAULT_INDEX_OPTIONS: Partial = { +const DEFAULT_INDEX_OPTIONS: Partial = { columns: [] }; diff --git a/packages/common/src/db/schema/IndexedColumn.ts b/packages/common/src/db/schema/IndexedColumn.ts index 1203dddfd..baaa64b52 100644 --- a/packages/common/src/db/schema/IndexedColumn.ts +++ b/packages/common/src/db/schema/IndexedColumn.ts @@ -9,10 +9,7 @@ export interface IndexColumnOptions { ascending?: boolean; } -/** - * @internal - */ -export const DEFAULT_INDEX_COLUMN_OPTIONS: Partial = { +const DEFAULT_INDEX_COLUMN_OPTIONS: Partial = { ascending: true }; diff --git a/packages/common/src/db/schema/Table.ts b/packages/common/src/db/schema/Table.ts index 422b0c5f0..3e2c911eb 100644 --- a/packages/common/src/db/schema/Table.ts +++ b/packages/common/src/db/schema/Table.ts @@ -1,16 +1,15 @@ -import { - BaseColumnType, - Column, - ColumnsType, - ColumnType, - ExtractColumnValueType, - MAX_AMOUNT_OF_COLUMNS -} from './Column.js'; +import { BaseColumnType, Column, ColumnsType, ColumnType, ExtractColumnValueType } from './Column.js'; import { Index } from './Index.js'; import { IndexedColumn } from './IndexedColumn.js'; import { encodeTableOptions } from './internal.js'; import { TableV2 } from './TableV2.js'; +/** + * powersync-sqlite-core limits the number of column per table to 1999, due to internal SQLite limits. + * In earlier versions this was limited to 63. + */ +const MAX_AMOUNT_OF_COLUMNS = 1999; + /** * Options that apply both to JSON-based tables and raw tables. * @@ -76,10 +75,7 @@ export interface TableV2Options extends SharedTableOptions { indexes?: IndexShorthand; } -/** - * @internal - */ -export const DEFAULT_TABLE_OPTIONS = { +const DEFAULT_TABLE_OPTIONS = { indexes: [], insertOnly: false, localOnly: false, @@ -88,10 +84,7 @@ export const DEFAULT_TABLE_OPTIONS = { ignoreEmptyUpdates: false }; -/** - * @internal - */ -export const InvalidSQLCharacters = /["'%,.#\s[\]]/; +const InvalidSQLCharacters = /["'%,.#\s[\]]/; /** * @public diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index b5e92e6ab..4df2cc690 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,11 +1,9 @@ export * from './attachments/AttachmentContext.js'; export * from './attachments/AttachmentErrorHandler.js'; export * from './attachments/AttachmentQueue.js'; -export * from './attachments/AttachmentService.js'; export * from './attachments/LocalStorageAdapter.js'; export * from './attachments/RemoteStorageAdapter.js'; export * from './attachments/Schema.js'; -export * from './attachments/SyncingService.js'; export * from './attachments/WatchedAttachmentItem.js'; export * from './client/CommonPowerSyncDatabase.js'; @@ -18,16 +16,8 @@ export { CrudEntry, OpId, UpdateType } from './client/sync/bucket/CrudEntry.js'; export * from './client/sync/bucket/CrudTransaction.js'; export * from './client/sync/stream/JsonValue.js'; export * from './client/sync/sync-streams.js'; -export { - SyncOptions, - SyncStreamConnectionMethod, - FetchStrategy, - // TODO: Stop exporting this, move to separate package instead - ResolvedSyncOptions, - resolveSyncOptions -} from './client/sync/options.js'; +export { SyncOptions, SyncStreamConnectionMethod, FetchStrategy } from './client/sync/options.js'; -export * from './db/ConnectionClosedError.js'; export { ProgressWithOperations, SyncProgress } from './db/crud/SyncProgress.js'; export * from './db/crud/SyncStatus.js'; export * from './db/crud/UploadQueueStatus.js'; @@ -49,9 +39,9 @@ export * from './client/watched/processors/DifferentialQueryProcessor.js'; export * from './client/watched/processors/OnChangeQueryProcessor.js'; export * from './client/watched/WatchedQuery.js'; +export { type Mutex } from './utils/mutex.js'; export * from './utils/BaseObserver.js'; export * from './utils/MetaBaseObserver.js'; export * from './utils/Logger.js'; -export * from './utils/parseQuery.js'; export * from './types/types.js'; diff --git a/packages/common/src/utils/MetaBaseObserver.ts b/packages/common/src/utils/MetaBaseObserver.ts index 3b5a71fbb..515c85422 100644 --- a/packages/common/src/utils/MetaBaseObserver.ts +++ b/packages/common/src/utils/MetaBaseObserver.ts @@ -2,6 +2,8 @@ import { BaseListener, BaseObserverInterface } from './BaseObserver.js'; /** * Represents the counts of listeners for each event type in a BaseListener. + * + * @public */ export type ListenerCounts = Partial> & { total: number; @@ -9,17 +11,25 @@ export type ListenerCounts = Partial extends BaseListener { listenersChanged?: (counts: ListenerCounts) => void; } +/** + * @public + */ export interface ListenerMetaManager extends BaseObserverInterface< MetaListener > { counts: ListenerCounts; } +/** + * @public + */ export interface MetaBaseObserverInterface extends BaseObserverInterface { listenerMeta: ListenerMetaManager; } diff --git a/packages/node/src/db/PowerSyncDatabase.ts b/packages/node/src/db/PowerSyncDatabase.ts index a342b0cd1..e4b8fd608 100644 --- a/packages/node/src/db/PowerSyncDatabase.ts +++ b/packages/node/src/db/PowerSyncDatabase.ts @@ -3,17 +3,16 @@ import { CommonPowerSyncDatabase, DatabaseSource, DBAdapter, - openDatabase, PowerSyncBackendConnector, PowerSyncDatabaseConstructor } from '@powersync/common'; - import { AbstractPowerSyncDatabase, AbstractStreamingSyncImplementation, BucketStorageAdapter, CreateSyncImplementationOptions, - SqliteBucketStorage + SqliteBucketStorage, + openDatabase } from '@powersync/shared-internals'; import { NodeRemote, NodeRemoteOptions } from '../sync/stream/NodeRemote.js'; diff --git a/packages/node/src/db/RemoteConnection.ts b/packages/node/src/db/RemoteConnection.ts index 0e214f53f..41751ecc6 100644 --- a/packages/node/src/db/RemoteConnection.ts +++ b/packages/node/src/db/RemoteConnection.ts @@ -1,13 +1,8 @@ -import { - ConnectionClosedError, - DBGetUtilsDefaultMixin, - QueryResult, - SqlExecutor, - LockContext -} from '@powersync/common'; +import { DBGetUtilsDefaultMixin, QueryResult, SqlExecutor, LockContext } from '@powersync/common'; import { releaseProxy, Remote } from 'comlink'; import { Worker } from 'node:worker_threads'; import { AsyncDatabase, AsyncDatabaseOpener, ProxiedQueryResult } from './AsyncDatabase.js'; +import { ConnectionClosedError } from '@powersync/shared-internals'; /** * A PowerSync database connection implemented with RPC calls to a background worker. diff --git a/packages/react-native/src/db/PowerSyncDatabase.ts b/packages/react-native/src/db/PowerSyncDatabase.ts index 7e32c27df..32645a03c 100644 --- a/packages/react-native/src/db/PowerSyncDatabase.ts +++ b/packages/react-native/src/db/PowerSyncDatabase.ts @@ -1,7 +1,6 @@ import { CommonPowerSyncDatabase, DBAdapter, - openDatabase, PowerSyncBackendConnector, PowerSyncDatabaseConstructor, PowerSyncDatabaseOptions @@ -10,7 +9,8 @@ import { AbstractPowerSyncDatabase, AbstractStreamingSyncImplementation, BucketStorageAdapter, - CreateSyncImplementationOptions + CreateSyncImplementationOptions, + openDatabase } from '@powersync/shared-internals'; import { ReactNativeRemote } from '../sync/stream/ReactNativeRemote'; import { ReactNativeStreamingSyncImplementation } from '../sync/stream/ReactNativeStreamingSyncImplementation'; diff --git a/packages/shared-internals/src/client/ConnectionManager.ts b/packages/shared-internals/src/client/ConnectionManager.ts index 93a97f632..54a6ca10e 100644 --- a/packages/shared-internals/src/client/ConnectionManager.ts +++ b/packages/shared-internals/src/client/ConnectionManager.ts @@ -8,13 +8,12 @@ import { SyncStreamDescription, SyncStreamSubscribeOptions, SyncStreamSubscription, - ResolvedSyncOptions, - resolveSyncOptions, SyncOptions } from '@powersync/common'; import { BaseObserver } from '../utils/BaseObserver.js'; import { StreamingSyncImplementation, SubscribedStream } from './sync/stream/AbstractStreamingSyncImplementation.js'; +import { ResolvedSyncOptions, resolveSyncOptions } from './sync/options.js'; /** * @internal diff --git a/packages/shared-internals/src/client/sync/options.ts b/packages/shared-internals/src/client/sync/options.ts index e69de29bb..5d5debd23 100644 --- a/packages/shared-internals/src/client/sync/options.ts +++ b/packages/shared-internals/src/client/sync/options.ts @@ -0,0 +1,21 @@ +import { FetchStrategy, SyncOptions, SyncStreamConnectionMethod } from '@powersync/common'; + +/** + * @internal + */ +export type ResolvedSyncOptions = Required; + +/** + * @internal + */ +export function resolveSyncOptions(options: SyncOptions): ResolvedSyncOptions { + return { + appMetadata: options.appMetadata ?? {}, + connectionMethod: options.connectionMethod ?? SyncStreamConnectionMethod.HTTP, + fetchStrategy: options.fetchStrategy ?? FetchStrategy.Buffered, + params: options.params ?? {}, + includeDefaultStreams: options.includeDefaultStreams ?? true, + retryDelayMs: options.retryDelayMs ?? 5000, + crudUploadThrottleMs: options.crudUploadThrottleMs ?? 1000 + }; +} diff --git a/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index 1446c7fa9..6d0c7f01d 100644 --- a/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -6,7 +6,6 @@ import { LogLevels, PowerSyncLogger, SyncStreamConnectionMethod, - ResolvedSyncOptions, SyncStatus, SyncDataFlowStatus } from '@powersync/common'; @@ -30,6 +29,7 @@ import { } from '../../../utils/stream_transform.js'; import { asyncNotifier } from '../../../utils/async.js'; import { SyncStatusSnapshot } from '../../../db/crud/SyncStatus.js'; +import { ResolvedSyncOptions } from '../options.js'; /** * @internal diff --git a/packages/common/src/db/ConnectionClosedError.ts b/packages/shared-internals/src/db/ConnectionClosedError.ts similarity index 100% rename from packages/common/src/db/ConnectionClosedError.ts rename to packages/shared-internals/src/db/ConnectionClosedError.ts diff --git a/packages/shared-internals/src/db/openDatabase.ts b/packages/shared-internals/src/db/openDatabase.ts new file mode 100644 index 000000000..4511ce903 --- /dev/null +++ b/packages/shared-internals/src/db/openDatabase.ts @@ -0,0 +1,23 @@ +import { DatabaseSource, DBAdapter, SQLOpenOptions } from '@powersync/common'; + +/** + * Internal helper to turn a {@link DatabaseSource} into an opened {@link DBAdapter}. + * + * @internal + */ +export function openDatabase( + source: DatabaseSource, + defaultFactory: (options: T) => DBAdapter +): DBAdapter { + if ('opened' in source) { + return source.opened; + } else if ('factory' in source) { + return source.factory.openDB(); + } else if ('database' in source && source.database?.dbFilename) { + return defaultFactory(source.database); + } else { + // This is dead code for well-typed programs, but JavaScript users might have forgotten to pass an option + // when creating a PowerSync database instance. + throw new Error('The provided `database` option is invalid.'); + } +} diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts index 07253e1fd..ea5593e28 100644 --- a/packages/shared-internals/src/index.ts +++ b/packages/shared-internals/src/index.ts @@ -5,8 +5,11 @@ export * from './client/sync/bucket/SqliteBucketStorage.js'; export * from './client/sync/stream/AbstractRemote.js'; export * from './client/sync/stream/AbstractStreamingSyncImplementation.js'; export * from './client/sync/stream/core-instruction.js'; +export * from './db/ConnectionClosedError.js'; +export * from './db/openDatabase.js'; export * from './db/crud/SyncStatus.js'; export * from './client/ConnectionManager.js'; +export * from './client/sync/options.js'; export { MEMORY_TRIGGER_CLAIM_MANAGER } from './client/triggers/MemoryTriggerClaimManager.js'; export * from './client/triggers/TriggerManagerImpl.js'; @@ -17,4 +20,5 @@ export * from './utils/AbortOperation.js'; export * from './utils/BaseObserver.js'; export * from './utils/ControlledExecutor.js'; export * from './utils/mutex.js'; +export * from './utils/parseQuery.js'; export type { SimpleAsyncIterator } from './utils/stream_transform.js'; diff --git a/packages/common/src/utils/parseQuery.ts b/packages/shared-internals/src/utils/parseQuery.ts similarity index 92% rename from packages/common/src/utils/parseQuery.ts rename to packages/shared-internals/src/utils/parseQuery.ts index 99654d012..004b9ce92 100644 --- a/packages/common/src/utils/parseQuery.ts +++ b/packages/shared-internals/src/utils/parseQuery.ts @@ -1,4 +1,4 @@ -import type { CompilableQuery } from '../types/types.js'; +import { CompilableQuery } from '@powersync/common'; /** * @internal diff --git a/packages/common/tests/utils/parseQuery.test.ts b/packages/shared-internals/tests/utils/parseQuery.test.ts similarity index 97% rename from packages/common/tests/utils/parseQuery.test.ts rename to packages/shared-internals/tests/utils/parseQuery.test.ts index 08629c3f0..73f96cf43 100644 --- a/packages/common/tests/utils/parseQuery.test.ts +++ b/packages/shared-internals/tests/utils/parseQuery.test.ts @@ -12,7 +12,7 @@ describe('parseQuery', () => { it('should compile the query and return the sql statement and parameters if the query is compilable', () => { const sqlStatement = 'SELECT * FROM table'; - const parameters = []; + const parameters: any[] = []; const query = { compile: () => ({ sql: sqlStatement, parameters: ['test'] }), execute: () => Promise.resolve([]) diff --git a/packages/tanstack-react-query/package.json b/packages/tanstack-react-query/package.json index ad1d1f217..72ec825ae 100644 --- a/packages/tanstack-react-query/package.json +++ b/packages/tanstack-react-query/package.json @@ -33,6 +33,7 @@ "homepage": "https://docs.powersync.com", "peerDependencies": { "@powersync/common": "workspace:^1.54.0", + "@powersync/shared-internals": "workspace:^1.0.0", "react": "*" }, "dependencies": { diff --git a/packages/tanstack-react-query/src/hooks/usePowerSyncQueries.ts b/packages/tanstack-react-query/src/hooks/usePowerSyncQueries.ts index 9a4c7f877..80e5a2c7d 100644 --- a/packages/tanstack-react-query/src/hooks/usePowerSyncQueries.ts +++ b/packages/tanstack-react-query/src/hooks/usePowerSyncQueries.ts @@ -1,4 +1,5 @@ -import { type CompilableQuery, parseQuery } from '@powersync/common'; +import { type CompilableQuery } from '@powersync/common'; +import { parseQuery } from '@powersync/shared-internals'; import { QuerySyncStreamOptions, useAllSyncStreamsHaveSynced, usePowerSync } from '@powersync/react'; import { useEffect, useState, useCallback, useMemo } from 'react'; import * as Tanstack from '@tanstack/react-query'; diff --git a/packages/tanstack-react-query/tsconfig.json b/packages/tanstack-react-query/tsconfig.json index 156e0f747..652e2dd28 100644 --- a/packages/tanstack-react-query/tsconfig.json +++ b/packages/tanstack-react-query/tsconfig.json @@ -11,6 +11,9 @@ "references": [ { "path": "../common" + }, + { + "path": "../shared-internals" } ], "include": ["src/**/*"] diff --git a/packages/vue/package.json b/packages/vue/package.json index 87e4f2a25..11a24eac3 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -33,7 +33,8 @@ "homepage": "https://docs.powersync.com", "peerDependencies": { "vue": "*", - "@powersync/common": "workspace:^1.54.0" + "@powersync/common": "workspace:^1.54.0", + "@powersync/shared-internals": "workspace:^1.0.0" }, "devDependencies": { "@powersync/common": "workspace:*", diff --git a/packages/vue/src/composables/useSingleQuery.ts b/packages/vue/src/composables/useSingleQuery.ts index 6cd76bb42..a0fee9ae2 100644 --- a/packages/vue/src/composables/useSingleQuery.ts +++ b/packages/vue/src/composables/useSingleQuery.ts @@ -2,10 +2,9 @@ import { type CompilableQuery, DifferentialWatchedQueryComparator, LogLevels, - ParsedQuery, - parseQuery, SQLOnChangeOptions } from '@powersync/common'; +import { ParsedQuery, parseQuery } from '@powersync/shared-internals'; import { type MaybeRef, type Ref, ref, toValue, watchEffect } from 'vue'; import { usePowerSync } from './powerSync.js'; import { QuerySyncStreamOptions } from './useAllSyncStreamsHaveSynced.js'; diff --git a/packages/vue/src/composables/useWatchedQuery.ts b/packages/vue/src/composables/useWatchedQuery.ts index 2c98872ff..eff791ff1 100644 --- a/packages/vue/src/composables/useWatchedQuery.ts +++ b/packages/vue/src/composables/useWatchedQuery.ts @@ -1,4 +1,5 @@ -import { type CompilableQuery, LogLevels, ParsedQuery, parseQuery, WatchCompatibleQuery } from '@powersync/common'; +import { type CompilableQuery, LogLevels, WatchCompatibleQuery } from '@powersync/common'; +import { ParsedQuery, parseQuery } from '@powersync/shared-internals'; import { type MaybeRef, type Ref, ref, toValue, watchEffect } from 'vue'; import { usePowerSync } from './powerSync.js'; import { AdditionalOptions, WatchedQueryResult } from './useSingleQuery.js'; diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json index ad9df2b91..4878e7689 100644 --- a/packages/vue/tsconfig.json +++ b/packages/vue/tsconfig.json @@ -14,6 +14,9 @@ }, { "path": "../web" + }, + { + "path": "../shared-internals" } ], "include": ["src/**/*"] diff --git a/packages/web/src/db/PowerSyncDatabase.ts b/packages/web/src/db/PowerSyncDatabase.ts index 6b4286bba..cc989ac4b 100644 --- a/packages/web/src/db/PowerSyncDatabase.ts +++ b/packages/web/src/db/PowerSyncDatabase.ts @@ -4,7 +4,6 @@ import { LogLevels, BasePowerSyncDatabaseOptions, DatabaseSource, - openDatabase, DBAdapter, PowerSyncDatabaseConstructor, CommonPowerSyncDatabase @@ -16,7 +15,8 @@ import { Mutex, SqliteBucketStorage, StreamingSyncImplementation, - TriggerManagerConfig + TriggerManagerConfig, + openDatabase } from '@powersync/shared-internals'; import { getNavigatorLocks } from '../shared/navigator.js'; import { NAVIGATOR_TRIGGER_CLAIM_MANAGER } from './NavigatorTriggerClaimManager.js'; diff --git a/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts b/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts index 1a91c31e0..d8ad596db 100644 --- a/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts +++ b/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts @@ -7,7 +7,6 @@ import { SqlExecutor, DBGetUtilsDefaultMixin, BatchedUpdateNotification, - ConnectionClosedError, SQLOpenOptions } from '@powersync/common'; import { SharedConnectionWorker, WebDBAdapterConfiguration } from '../WebDBAdapter.js'; @@ -15,7 +14,7 @@ import { ClientConnectionView } from './DatabaseServer.js'; import { RawQueryResult } from './RawSqliteConnection.js'; import * as Comlink from 'comlink'; import type { ConnectToMultiDatabaseServerOptions } from '../../../worker/db/MultiDatabaseServer.js'; -import { BaseObserver } from '@powersync/shared-internals'; +import { BaseObserver, ConnectionClosedError } from '@powersync/shared-internals'; export interface OpenWorkerConnection { connect(config: ConnectToMultiDatabaseServerOptions): Promise; diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 51d65cb07..370fa9015 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -1,4 +1,4 @@ -import { LogRecord, PowerSyncCredentials, ResolvedSyncOptions } from '@powersync/common'; +import { LogRecord, PowerSyncCredentials } from '@powersync/common'; import * as Comlink from 'comlink'; import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider.js'; import { ManualSharedSyncPayload, SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation.js'; @@ -9,7 +9,7 @@ import { WebStreamingSyncImplementationOptions } from './WebStreamingSyncImplementation.js'; import { generateTabCloseSignal } from '../../shared/tab_close_signal.js'; -import { SubscribedStream, SyncStatusJson } from '@powersync/shared-internals'; +import { SubscribedStream, SyncStatusJson, ResolvedSyncOptions } from '@powersync/shared-internals'; /** * The shared worker will trigger methods on this side of the message port diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 86d8cbbcc..9df64eed8 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -7,9 +7,7 @@ import { LockContext, PowerSyncBackendConnector, SyncStatus, - LogLevels, - ResolvedSyncOptions, - SyncDataFlowStatus + LogLevels } from '@powersync/common'; import { AbortOperation, @@ -20,9 +18,8 @@ import { type StreamingSyncImplementation, type StreamingSyncImplementationListener, Mutex, - CoreSyncStatus, - SyncStatusSnapshot, - SyncStatusJson + ResolvedSyncOptions, + SyncStatusSnapshot } from '@powersync/shared-internals'; import * as Comlink from 'comlink'; import { WebRemote } from '../../db/sync/WebRemote.js'; diff --git a/packages/web/src/worker/sync/WorkerClient.ts b/packages/web/src/worker/sync/WorkerClient.ts index 19d0fc816..c62fe4b0b 100644 --- a/packages/web/src/worker/sync/WorkerClient.ts +++ b/packages/web/src/worker/sync/WorkerClient.ts @@ -1,4 +1,3 @@ -import { ResolvedSyncOptions } from '@powersync/common'; import * as Comlink from 'comlink'; import { getNavigatorLocks } from '../../shared/navigator.js'; import { @@ -8,7 +7,7 @@ import { SharedSyncInitOptions, WrappedSyncPort } from './SharedSyncImplementation.js'; -import { SubscribedStream } from '@powersync/shared-internals'; +import { SubscribedStream, ResolvedSyncOptions } from '@powersync/shared-internals'; /** * A client to the shared sync worker. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 858b865da..92c30e82b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -841,6 +841,9 @@ importers: '@powersync/react': specifier: workspace:* version: link:../react + '@powersync/shared-internals': + specifier: workspace:^1.0.0 + version: link:../shared-internals '@tanstack/react-query': specifier: ^5.70.0 version: 5.90.21(react@18.3.1) @@ -892,6 +895,10 @@ importers: packages/tauri/src: {} packages/vue: + dependencies: + '@powersync/shared-internals': + specifier: workspace:^1.0.0 + version: link:../shared-internals devDependencies: '@powersync/common': specifier: workspace:* diff --git a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts index 764cefd36..e98b84b89 100644 --- a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts +++ b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts @@ -3,7 +3,6 @@ import { createConsoleLogger, LogLevels, PowerSyncDatabase, - resolveSyncOptions, Schema, SyncStreamSubscription, TemporaryStorageOption, @@ -13,6 +12,7 @@ import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from '@powersync/web'; +import { resolveSyncOptions } from '@powersync/shared-internals'; import React from 'react'; import { ClientParameterRow, localStateDb } from './LocalStateManager'; import { DynamicSchemaManager } from './DynamicSchemaManager'; From 8f313ff2d103f03de5fd34c382b052b2e3699373 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 15:12:53 +0200 Subject: [PATCH 04/12] Don't bundle shared internals --- packages/adapter-sql-js/rollup.config.mjs | 2 +- packages/capacitor/rollup.config.mjs | 1 + packages/react-native/rollup.config.mjs | 1 + packages/shared-internals/rollup.config.mjs | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/adapter-sql-js/rollup.config.mjs b/packages/adapter-sql-js/rollup.config.mjs index b6625f355..82472e799 100644 --- a/packages/adapter-sql-js/rollup.config.mjs +++ b/packages/adapter-sql-js/rollup.config.mjs @@ -31,6 +31,6 @@ export default () => { ] }) ], - external: ['@powersync/common'] + external: ['@powersync/common', '@powersync/shared-internals'] }; }; diff --git a/packages/capacitor/rollup.config.mjs b/packages/capacitor/rollup.config.mjs index d8103c07c..b71d7a51c 100644 --- a/packages/capacitor/rollup.config.mjs +++ b/packages/capacitor/rollup.config.mjs @@ -4,6 +4,7 @@ const external = [ '@capacitor/core', '@capacitor-community/sqlite', '@powersync/common', + '@powersync/shared-internals', '@powersync/web', '@journeyapps/wa-sqlite' ]; diff --git a/packages/react-native/rollup.config.mjs b/packages/react-native/rollup.config.mjs index 6ba9a4a86..ae43b31e6 100644 --- a/packages/react-native/rollup.config.mjs +++ b/packages/react-native/rollup.config.mjs @@ -57,6 +57,7 @@ export default () => { '@journeyapps/react-native-quick-sqlite', '@powersync/common', '@powersync/react', + '@powersync/shared-internals', 'node-fetch', 'react-native', 'react' diff --git a/packages/shared-internals/rollup.config.mjs b/packages/shared-internals/rollup.config.mjs index d56271adb..dc445fe74 100644 --- a/packages/shared-internals/rollup.config.mjs +++ b/packages/shared-internals/rollup.config.mjs @@ -37,7 +37,7 @@ function defineBuild(isNode) { } ], plugins, - external: [isNode ? 'event-iterator' : undefined] + external: [isNode ? 'event-iterator' : undefined, '@powersync/common'] }; } From 7ca2e3c43ad5a613a64847f2156978080bff8719 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 15:30:36 +0200 Subject: [PATCH 05/12] Fix attachments-storage-react-native build --- .../package.json | 2 +- .../tsconfig.json | 2 +- pnpm-lock.yaml | 31 ++++++++----------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/packages/attachments-storage-react-native/package.json b/packages/attachments-storage-react-native/package.json index 15a93ff06..e93295edb 100644 --- a/packages/attachments-storage-react-native/package.json +++ b/packages/attachments-storage-react-native/package.json @@ -64,7 +64,7 @@ "@powersync/common": "workspace:*", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "15.2.3", - "@rollup/plugin-typescript": "^11.1.6", + "@rollup/plugin-typescript": "^12.3.0", "@dr.pogodin/react-native-fs": "^2.36.1", "expo-file-system": "^19.0.21", "rollup": "4.14.3", diff --git a/packages/attachments-storage-react-native/tsconfig.json b/packages/attachments-storage-react-native/tsconfig.json index a7bd9c9cd..965fb2987 100644 --- a/packages/attachments-storage-react-native/tsconfig.json +++ b/packages/attachments-storage-react-native/tsconfig.json @@ -4,7 +4,7 @@ "rootDir": "src", "module": "node16", "moduleResolution": "node16", - "target": "es6", + "target": "es2019", "outDir": "./lib", "strictNullChecks": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92c30e82b..6d14a78d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,8 +332,8 @@ importers: specifier: 15.2.3 version: 15.2.3(rollup@4.14.3) '@rollup/plugin-typescript': - specifier: ^11.1.6 - version: 11.1.6(rollup@4.14.3)(tslib@2.8.1)(typescript@6.0.3) + specifier: ^12.3.0 + version: 12.3.0(rollup@4.14.3)(tslib@2.8.1)(typescript@6.0.3) expo-file-system: specifier: ^19.0.21 version: 19.0.21(expo@54.0.33(@babel/core@7.29.7)(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.0.0(typescript@6.0.3))(@react-native/metro-config@0.83.1(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@6.0.3))(react-native@0.84.1(@babel/core@7.29.7)(@react-native-community/cli@20.0.0(typescript@6.0.3))(@react-native/metro-config@0.83.1(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.4)) @@ -6525,19 +6525,6 @@ packages: rollup: optional: true - '@rollup/plugin-typescript@11.1.6': - resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.14.0||^3.0.0||^4.0.0 - tslib: '*' - typescript: '>=3.7.0' - peerDependenciesMeta: - rollup: - optional: true - tslib: - optional: true - '@rollup/plugin-typescript@12.3.0': resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==} engines: {node: '>=14.0.0'} @@ -26224,10 +26211,10 @@ snapshots: optionalDependencies: rollup: 4.59.0 - '@rollup/plugin-typescript@11.1.6(rollup@4.14.3)(tslib@2.8.1)(typescript@6.0.3)': + '@rollup/plugin-typescript@12.3.0(rollup@4.14.3)(tslib@2.8.1)(typescript@6.0.3)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.14.3) - resolve: 1.22.11 + '@rollup/pluginutils': 5.4.0(rollup@4.14.3) + resolve: 1.22.12 typescript: 6.0.3 optionalDependencies: rollup: 4.14.3 @@ -26269,6 +26256,14 @@ snapshots: optionalDependencies: rollup: 4.14.3 + '@rollup/pluginutils@5.4.0(rollup@4.14.3)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.14.3 + '@rollup/pluginutils@5.4.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.9 From 475325a86f2d710765e6e907b1c375f6c5dba349 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 15:42:55 +0200 Subject: [PATCH 06/12] fix attw --- packages/common/etc/common.api.md | 16 ++++++---------- packages/shared-internals/package.json | 20 ++++++++++++++++++-- tools/diagnostics-app/Dockerfile | 4 ++-- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/common/etc/common.api.md b/packages/common/etc/common.api.md index 90e54ae52..e7df23516 100644 --- a/packages/common/etc/common.api.md +++ b/packages/common/etc/common.api.md @@ -1248,11 +1248,7 @@ export type WatchedAttachmentItem = { }; // @public (undocumented) -export interface WatchedQuery< -Data = unknown, -Settings extends WatchedQueryOptions = WatchedQueryOptions, -Listener extends WatchedQueryListener = WatchedQueryListener -> extends MetaBaseObserverInterface { +export interface WatchedQuery = WatchedQueryListener> extends MetaBaseObserverInterface { close(): Promise; // (undocumented) readonly closed: boolean; @@ -1297,15 +1293,15 @@ export interface WatchedQueryListener extends BaseListener { // @public (undocumented) export enum WatchedQueryListenerEvent { // (undocumented) - CLOSED = 'closed', + CLOSED = "closed", // (undocumented) - ON_DATA = 'onData', + ON_DATA = "onData", // (undocumented) - ON_ERROR = 'onError', + ON_ERROR = "onError", // (undocumented) - ON_STATE_CHANGE = 'onStateChange', + ON_STATE_CHANGE = "onStateChange", // (undocumented) - SETTINGS_WILL_UPDATE = 'settingsWillUpdate' + SETTINGS_WILL_UPDATE = "settingsWillUpdate" } // @public (undocumented) diff --git a/packages/shared-internals/package.json b/packages/shared-internals/package.json index 844a57c21..4a1fbb80e 100644 --- a/packages/shared-internals/package.json +++ b/packages/shared-internals/package.json @@ -9,8 +9,24 @@ "type": "module", "exports": { ".": { + "node": { + "import": { + "types": "./lib/index.d.ts", + "default": "./dist/bundle.node.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "require": "./dist/bundle.node.cjs" + } + }, + "import": { "types": "./lib/index.d.ts", - "default": "./lib/index.js" + "default": "./dist/bundle.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "require": "./dist/bundle.cjs" + } }, "./internal/sync_protocol": { "types": "./legacy/sync_protocol.d.ts" @@ -37,7 +53,7 @@ "build:prod": "tsc -b && rollup -c rollup.config.mjs", "clean": "rm -rf lib dist tsconfig.tsbuildinfo", "test": "vitest", - "test:exports": "attw --pack . --exclude-entrypoints internal/sync_protocol" + "test:exports": "attw --pack . --profile node16 --exclude-entrypoints internal/sync_protocol" }, "dependencies": { "event-iterator": "^2.0.0" diff --git a/tools/diagnostics-app/Dockerfile b/tools/diagnostics-app/Dockerfile index 95c068844..cf2acbef2 100644 --- a/tools/diagnostics-app/Dockerfile +++ b/tools/diagnostics-app/Dockerfile @@ -4,8 +4,8 @@ WORKDIR /app COPY . /app RUN corepack enable -RUN pnpm i --frozen-lockfile --filter ./packages/react --filter ./packages/common --filter ./packages/web --filter ./tools/diagnostics-app -RUN pnpm run --filter ./packages/react --filter ./packages/common --filter ./packages/web build +RUN pnpm i --frozen-lockfile --filter ./packages/react --filter ./packages/common --filter ./packages/shared-internals --filter ./packages/web --filter ./tools/diagnostics-app +RUN pnpm run --filter ./packages/react --filter ./packages/common --filter ./packages/shared-internals --filter ./packages/web build RUN pnpm run --filter ./tools/diagnostics-app build # === PROD === From d33e7d644ee54c2488bba5987794aa0caf15c064 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 16:05:35 +0200 Subject: [PATCH 07/12] Rename to BasePowerSyncDatabase --- .changeset/funny-deers-explode.md | 2 ++ packages/common/package.json | 3 --- packages/node/src/db/PowerSyncDatabase.ts | 4 ++-- packages/node/tests/sync.test.ts | 4 ++-- .../runtime/utils/RustClientInterceptor.ts | 4 ++-- .../react-native/src/db/PowerSyncDatabase.ts | 4 ++-- ...ncDatabase.ts => BasePowerSyncDatabase.ts} | 4 +--- .../src/client/CustomQuery.ts | 4 ++-- .../src/client/triggers/TriggerManagerImpl.ts | 4 ++-- .../client/watched/AbstractQueryProcessor.ts | 4 ++-- packages/shared-internals/src/index.ts | 2 +- packages/tauri/guest-js/database.ts | 4 ++-- packages/vue/tests/streams.test.ts | 24 ++++++++++++------- packages/web/src/db/PowerSyncDatabase.ts | 4 ++-- pnpm-lock.yaml | 10 +++----- tools/diagnostics-app/package.json | 4 ++-- .../powersync/RustClientInterceptor.ts | 4 ++-- tools/diagnostics-app/tsconfig.json | 7 +----- 18 files changed, 45 insertions(+), 51 deletions(-) rename packages/shared-internals/src/client/{AbstractPowerSyncDatabase.ts => BasePowerSyncDatabase.ts} (99%) diff --git a/.changeset/funny-deers-explode.md b/.changeset/funny-deers-explode.md index 4e8206ef5..35ac322c9 100644 --- a/.changeset/funny-deers-explode.md +++ b/.changeset/funny-deers-explode.md @@ -13,4 +13,6 @@ Rename `AbstractPowerSyncDatabase` to `CommonPowerSyncDatabase`, make it a TypeS `CrudEntry` is now a TypeScript interface, remove it's constructor and `CrudEntry.fromRow`. +`SyncStatus` is no longer constructable in user code. + Remove `DataFlowStatus.downloading`. Use `SyncStatus.downloading` instead. diff --git a/packages/common/package.json b/packages/common/package.json index ce8d4a234..eb4a2e2ef 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -35,9 +35,6 @@ "test": "vitest", "test:exports": "attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm" }, - "dependencies": { - "event-iterator": "^2.0.0" - }, "devDependencies": { "@types/node": "catalog:", "@types/uuid": "catalog:", diff --git a/packages/node/src/db/PowerSyncDatabase.ts b/packages/node/src/db/PowerSyncDatabase.ts index e4b8fd608..071f3205f 100644 --- a/packages/node/src/db/PowerSyncDatabase.ts +++ b/packages/node/src/db/PowerSyncDatabase.ts @@ -7,7 +7,7 @@ import { PowerSyncDatabaseConstructor } from '@powersync/common'; import { - AbstractPowerSyncDatabase, + BasePowerSyncDatabase, AbstractStreamingSyncImplementation, BucketStorageAdapter, CreateSyncImplementationOptions, @@ -29,7 +29,7 @@ export type NodePowerSyncDatabaseOptions = BasePowerSyncDatabaseOptions & remoteOptions?: Partial; }; -class NodePowerSyncDatabase extends AbstractPowerSyncDatabase { +class NodePowerSyncDatabase extends BasePowerSyncDatabase { constructor(options: NodePowerSyncDatabaseOptions) { super(options); } diff --git a/packages/node/tests/sync.test.ts b/packages/node/tests/sync.test.ts index c38337c74..df53a1f95 100644 --- a/packages/node/tests/sync.test.ts +++ b/packages/node/tests/sync.test.ts @@ -17,7 +17,7 @@ import { waitForSyncStatus } from './utils.js'; import { BucketChecksum, OplogEntryJSON } from '@powersync/shared-internals/internal/sync_protocol'; -import { AbstractPowerSyncDatabase } from '@powersync/shared-internals'; +import { BasePowerSyncDatabase } from '@powersync/shared-internals'; const defaultConnectOptions: SyncOptions = { // This might help with test stability/timeouts if a retry is needed. @@ -105,7 +105,7 @@ describe('Sync', () => { // Replicate what we'd see on the web when switching connections in the shared sync worker: The sync client would // suddenly see a database without an active sync iteration. await database.execute('SELECT powersync_control(?, null)', ['stop']); - (database as AbstractPowerSyncDatabase).syncStreamImplementation!.markConnectionMayHaveChanged(); + (database as BasePowerSyncDatabase).syncStreamImplementation!.markConnectionMayHaveChanged(); await database.waitForStatus((s) => !s.connected); await vi.waitFor(() => expect(syncService.connectedListeners).toHaveLength(1)); }); diff --git a/packages/nuxt/src/runtime/utils/RustClientInterceptor.ts b/packages/nuxt/src/runtime/utils/RustClientInterceptor.ts index 7559a3c23..053cca8fc 100644 --- a/packages/nuxt/src/runtime/utils/RustClientInterceptor.ts +++ b/packages/nuxt/src/runtime/utils/RustClientInterceptor.ts @@ -1,6 +1,6 @@ import type { ColumnType, PowerSyncDatabase, DBAdapter } from '@powersync/web'; import { type BSON } from 'bson'; -import { AbstractPowerSyncDatabase, PowerSyncControlCommand, SqliteBucketStorage } from '@powersync/shared-internals'; +import { BasePowerSyncDatabase, PowerSyncControlCommand, SqliteBucketStorage } from '@powersync/shared-internals'; import type { DynamicSchemaManager } from './DynamicSchemaManager'; import type { ShallowRef } from 'vue'; import type { BucketChecksum, Checkpoint, StreamingSyncLine } from '@powersync/shared-internals/internal/sync_protocol'; @@ -23,7 +23,7 @@ export class RustClientInterceptor extends SqliteBucketStorage { db: ShallowRef, private schemaManager: ShallowRef ) { - super(db.value.database, (AbstractPowerSyncDatabase as any).transactionMutex); + super(db.value.database, (BasePowerSyncDatabase as any).transactionMutex); this.rdb = db.value.database; } diff --git a/packages/react-native/src/db/PowerSyncDatabase.ts b/packages/react-native/src/db/PowerSyncDatabase.ts index 32645a03c..7292aec9e 100644 --- a/packages/react-native/src/db/PowerSyncDatabase.ts +++ b/packages/react-native/src/db/PowerSyncDatabase.ts @@ -6,7 +6,7 @@ import { PowerSyncDatabaseOptions } from '@powersync/common'; import { - AbstractPowerSyncDatabase, + BasePowerSyncDatabase, AbstractStreamingSyncImplementation, BucketStorageAdapter, CreateSyncImplementationOptions, @@ -17,7 +17,7 @@ import { ReactNativeStreamingSyncImplementation } from '../sync/stream/ReactNati import { ReactNativeBucketStorageAdapter } from './../sync/bucket/ReactNativeBucketStorageAdapter'; import { ReactNativeQuickSqliteOpenFactory } from './adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory'; -class ReactNativePowerSyncDatabase extends AbstractPowerSyncDatabase { +class ReactNativePowerSyncDatabase extends BasePowerSyncDatabase { constructor(options: PowerSyncDatabaseOptions) { super(options); } diff --git a/packages/shared-internals/src/client/AbstractPowerSyncDatabase.ts b/packages/shared-internals/src/client/BasePowerSyncDatabase.ts similarity index 99% rename from packages/shared-internals/src/client/AbstractPowerSyncDatabase.ts rename to packages/shared-internals/src/client/BasePowerSyncDatabase.ts index 69f74d703..e9e9b8ff4 100644 --- a/packages/shared-internals/src/client/AbstractPowerSyncDatabase.ts +++ b/packages/shared-internals/src/client/BasePowerSyncDatabase.ts @@ -78,9 +78,7 @@ const DEFAULT_CRUD_BATCH_LIMIT = 100; */ export const DEFAULT_LOCK_TIMEOUT_MS = 120_000; // 2 mins -export abstract class AbstractPowerSyncDatabase< - Options extends BasePowerSyncDatabaseOptions = BasePowerSyncDatabaseOptions -> +export abstract class BasePowerSyncDatabase extends BaseObserver implements CommonPowerSyncDatabase { diff --git a/packages/shared-internals/src/client/CustomQuery.ts b/packages/shared-internals/src/client/CustomQuery.ts index beb1dfa41..4731a8dd9 100644 --- a/packages/shared-internals/src/client/CustomQuery.ts +++ b/packages/shared-internals/src/client/CustomQuery.ts @@ -7,7 +7,7 @@ import { DifferentialWatchedQueryOptions } from '@powersync/common'; -import { AbstractPowerSyncDatabase } from './AbstractPowerSyncDatabase.js'; +import { BasePowerSyncDatabase } from './BasePowerSyncDatabase.js'; import { DifferentialQueryProcessor } from './watched/DifferentialQueryProcessor.js'; import { OnChangeQueryProcessor } from './watched/OnChangeQueryProcessor.js'; import { DEFAULT_WATCH_QUERY_OPTIONS } from './watched/WatchedQuery.js'; @@ -16,7 +16,7 @@ import { DEFAULT_WATCH_QUERY_OPTIONS } from './watched/WatchedQuery.js'; * @internal */ export interface CustomQueryOptions { - db: AbstractPowerSyncDatabase; + db: BasePowerSyncDatabase; query: WatchCompatibleQuery; } diff --git a/packages/shared-internals/src/client/triggers/TriggerManagerImpl.ts b/packages/shared-internals/src/client/triggers/TriggerManagerImpl.ts index fa1f05f5e..727b41c09 100644 --- a/packages/shared-internals/src/client/triggers/TriggerManagerImpl.ts +++ b/packages/shared-internals/src/client/triggers/TriggerManagerImpl.ts @@ -1,4 +1,4 @@ -import type { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js'; +import type { BasePowerSyncDatabase } from '../BasePowerSyncDatabase.js'; import { DEFAULT_WATCH_THROTTLE_MS } from '../watched/WatchedQuery.js'; import { CreateDiffTriggerOptions, @@ -48,7 +48,7 @@ export interface TriggerClaimManager { } export type TriggerManagerImplOptions = TriggerManagerConfig & { - db: AbstractPowerSyncDatabase; + db: BasePowerSyncDatabase; schema: Schema; }; diff --git a/packages/shared-internals/src/client/watched/AbstractQueryProcessor.ts b/packages/shared-internals/src/client/watched/AbstractQueryProcessor.ts index 04230db12..04b27a9b3 100644 --- a/packages/shared-internals/src/client/watched/AbstractQueryProcessor.ts +++ b/packages/shared-internals/src/client/watched/AbstractQueryProcessor.ts @@ -7,13 +7,13 @@ import { WatchedQueryState } from '@powersync/common'; import { MetaBaseObserver } from '../../utils/MetaBaseObserver.js'; -import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js'; +import { BasePowerSyncDatabase } from '../BasePowerSyncDatabase.js'; /** * @internal */ export interface AbstractQueryProcessorOptions { - db: AbstractPowerSyncDatabase; + db: BasePowerSyncDatabase; watchOptions: Settings; placeholderData: Data; } diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts index ea5593e28..cf62aa312 100644 --- a/packages/shared-internals/src/index.ts +++ b/packages/shared-internals/src/index.ts @@ -1,4 +1,4 @@ -export * from './client/AbstractPowerSyncDatabase.js'; +export * from './client/BasePowerSyncDatabase.js'; export * from './client/sync/bucket/BucketStorageAdapter.js'; export * from './client/sync/bucket/CrudEntry.js'; export * from './client/sync/bucket/SqliteBucketStorage.js'; diff --git a/packages/tauri/guest-js/database.ts b/packages/tauri/guest-js/database.ts index 6e63f9f72..01b6f4baa 100644 --- a/packages/tauri/guest-js/database.ts +++ b/packages/tauri/guest-js/database.ts @@ -12,7 +12,7 @@ import { CreatedDatabase, powersyncCommand } from './command'; import { listen, UnlistenFn } from '@tauri-apps/api/event'; import { join } from '@tauri-apps/api/path'; import { - AbstractPowerSyncDatabase, + BasePowerSyncDatabase, BucketStorageAdapter, StreamingSyncImplementation, SyncStatusJson, @@ -38,7 +38,7 @@ export interface TauriSQLOpenOptions extends SQLOpenOptions { /** * A PowerSync database backed by a Rust-owned structure for Tauri apps. */ -export class PowerSyncTauriDatabase extends AbstractPowerSyncDatabase { +export class PowerSyncTauriDatabase extends BasePowerSyncDatabase { declare private handle: LateHandle; private didInitializeSchema = false; private tableUpdateListener?: UnlistenFn; diff --git a/packages/vue/tests/streams.test.ts b/packages/vue/tests/streams.test.ts index 951eb2861..e9510e564 100644 --- a/packages/vue/tests/streams.test.ts +++ b/packages/vue/tests/streams.test.ts @@ -1,4 +1,5 @@ import * as commonSdk from '@powersync/common'; +import * as internals from '@powersync/shared-internals'; import { PowerSyncDatabase } from '@powersync/web'; import { beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest'; import { computed, ref } from 'vue'; @@ -28,14 +29,18 @@ function openPowerSync() { return db; } -function currentStreams(db: commonSdk.AbstractPowerSyncDatabase) { - const connections = (db as any).connectionManager as commonSdk.ConnectionManager; +function currentStreams(db: commonSdk.CommonPowerSyncDatabase) { + const connections = (db as any).connectionManager as internals.ConnectionManager; return connections.activeStreams; } -const _testStatus = new commonSdk.SyncStatus({ - dataFlow: { - internalStreamSubscriptions: [ +const _testStatus = new internals.SyncStatusSnapshot( + { + connected: true, + connecting: false, + downloading: { buckets: {} }, + priority_status: [], + streams: [ { name: 'a', parameters: null, @@ -48,11 +53,12 @@ const _testStatus = new commonSdk.SyncStatus({ priority: 1 } ] - } -}); + }, + {} +); describe('stream composables', () => { - let db: commonSdk.AbstractPowerSyncDatabase; + let db: commonSdk.CommonPowerSyncDatabase; beforeEach(() => { db = openPowerSync(); @@ -96,7 +102,7 @@ describe('stream composables', () => { expect(result.isLoading.value).toBe(true); (db as any).currentStatus = _testStatus; - db.iterateListeners((l: any) => l.statusChanged?.(_testStatus)); + (db as unknown as internals.BasePowerSyncDatabase).iterateListeners((l: any) => l.statusChanged?.(_testStatus)); await vi.waitFor(() => expect(result.data.value).toHaveLength(1), { timeout: 1000, interval: 100 }); }); diff --git a/packages/web/src/db/PowerSyncDatabase.ts b/packages/web/src/db/PowerSyncDatabase.ts index cc989ac4b..38c0edd2b 100644 --- a/packages/web/src/db/PowerSyncDatabase.ts +++ b/packages/web/src/db/PowerSyncDatabase.ts @@ -9,7 +9,7 @@ import { CommonPowerSyncDatabase } from '@powersync/common'; import { - AbstractPowerSyncDatabase, + BasePowerSyncDatabase, BucketStorageAdapter, CreateSyncImplementationOptions, Mutex, @@ -67,7 +67,7 @@ export interface WebSyncOptions { /** * @internal Use {@link PowerSyncDatabase} instead, this class is only used by other SDKs also needing web support. */ -export class WebPowerSyncDatabase extends AbstractPowerSyncDatabase { +export class WebPowerSyncDatabase extends BasePowerSyncDatabase { static SHARED_MUTEX = new Mutex(); protected resolvedOpenOptions: WebSpecificOpenOptions; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d14a78d6..399cf817f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -409,10 +409,6 @@ importers: version: 4.1.8(@types/node@25.9.1)(@vitest/browser-playwright@4.1.8)(@vitest/browser-preview@4.1.8)(jsdom@24.1.3)(vite@7.3.1(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.9.0)) packages/common: - dependencies: - event-iterator: - specifier: ^2.0.0 - version: 2.0.0 devDependencies: '@microsoft/api-extractor': specifier: 'catalog:' @@ -976,6 +972,9 @@ importers: '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@powersync/common': + specifier: workspace:* + version: link:../../packages/common '@powersync/react': specifier: workspace:* version: link:../../packages/react @@ -1052,9 +1051,6 @@ importers: specifier: ^4.3.5 version: 4.3.6 devDependencies: - '@powersync/common': - specifier: workspace:* - version: link:../../packages/common '@swc/core': specifier: ~1.6.0 version: 1.6.13(@swc/helpers@0.5.19) diff --git a/tools/diagnostics-app/package.json b/tools/diagnostics-app/package.json index 5e8f267ed..97072fde5 100644 --- a/tools/diagnostics-app/package.json +++ b/tools/diagnostics-app/package.json @@ -15,6 +15,7 @@ "@powersync/react": "workspace:*", "@powersync/web": "workspace:*", "@powersync/shared-internals": "workspace:*", + "@powersync/common": "workspace:*", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", @@ -51,7 +52,6 @@ "vite": "^5.1.5", "vite-plugin-pwa": "^0.19.2", "vite-plugin-top-level-await": "^1.4.1", - "vite-plugin-wasm": "^3.3.0", - "@powersync/common": "workspace:*" + "vite-plugin-wasm": "^3.3.0" } } diff --git a/tools/diagnostics-app/src/library/powersync/RustClientInterceptor.ts b/tools/diagnostics-app/src/library/powersync/RustClientInterceptor.ts index 8969f4004..601a907e1 100644 --- a/tools/diagnostics-app/src/library/powersync/RustClientInterceptor.ts +++ b/tools/diagnostics-app/src/library/powersync/RustClientInterceptor.ts @@ -1,6 +1,6 @@ import { ColumnType, CommonPowerSyncDatabase } from '@powersync/web'; import { - AbstractPowerSyncDatabase, + BasePowerSyncDatabase, AbstractRemote, PowerSyncControlCommand, SqliteBucketStorage @@ -28,7 +28,7 @@ export class RustClientInterceptor extends SqliteBucketStorage { private remote: AbstractRemote, private schemaManager: DynamicSchemaManager ) { - super(db.database, (AbstractPowerSyncDatabase as any).transactionMutex); + super(db.database, (BasePowerSyncDatabase as any).transactionMutex); this.rdb = db; } diff --git a/tools/diagnostics-app/tsconfig.json b/tools/diagnostics-app/tsconfig.json index a7adfe039..db2329f9a 100644 --- a/tools/diagnostics-app/tsconfig.json +++ b/tools/diagnostics-app/tsconfig.json @@ -19,10 +19,5 @@ "@types/react": ["./node_modules/@types/react"] } }, - "exclude": ["node_modules"], - "references": [ - {"path": "../../packages/common" }, - {"path": "../../packages/shared-internals" }, - {"path": "../../packages/web" } - ] + "exclude": ["node_modules"] } From 14627fc28b26fc8c4de2df4e33d9482c8d73b1e3 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 16:18:00 +0200 Subject: [PATCH 08/12] Fix react tests --- packages/react/package.json | 3 +- packages/react/tests/streams.test.tsx | 37 ++++++++++++++-------- packages/react/tests/useQuery.test.tsx | 3 +- packages/shared-internals/src/reexports.ts | 1 - pnpm-lock.yaml | 3 ++ 5 files changed, 30 insertions(+), 17 deletions(-) delete mode 100644 packages/shared-internals/src/reexports.ts diff --git a/packages/react/package.json b/packages/react/package.json index 74546b0dd..3901b51b8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -49,6 +49,7 @@ "jsdom": "catalog:", "react": "18.3.1", "react-dom": "18.3.1", - "react-error-boundary": "^4.1.0" + "react-error-boundary": "^4.1.0", + "@powersync/shared-internals": "workspace:" } } diff --git a/packages/react/tests/streams.test.tsx b/packages/react/tests/streams.test.tsx index fee818aba..9a2e1005d 100644 --- a/packages/react/tests/streams.test.tsx +++ b/packages/react/tests/streams.test.tsx @@ -1,7 +1,8 @@ import { cleanup, renderHook, waitFor } from '@testing-library/react'; -import { describe, expect, vi } from 'vitest'; +import { describe, expect, vi, beforeEach, it } from 'vitest'; import React, { act, useSyncExternalStore } from 'react'; -import { AbstractPowerSyncDatabase, ConnectionManager, SyncStatus } from '@powersync/common'; +import { CommonPowerSyncDatabase } from '@powersync/common'; +import { BasePowerSyncDatabase, ConnectionManager, SyncStatusSnapshot } from '@powersync/shared-internals'; import { openPowerSync } from './utils'; import { PowerSyncContext } from '../src/hooks/PowerSyncContext'; import { useSyncStream, useSyncStreams, UseSyncStreamOptions } from '../src/hooks/streams'; @@ -9,7 +10,7 @@ import { useQuery } from '../src/hooks/watched/useQuery'; import { QuerySyncStreamOptions } from '../src/hooks/watched/watch-types'; describe('stream hooks', () => { - let db: AbstractPowerSyncDatabase; + let db: CommonPowerSyncDatabase; beforeEach(() => { db = openPowerSync(); @@ -86,8 +87,8 @@ describe('stream hooks', () => { expect(result.current).toMatchObject({ isLoading: true }); // Set last_synced_at for the subscription - db.currentStatus = _testStatus; - db.iterateListeners((l) => l.statusChanged?.(_testStatus)); + (db as unknown as BasePowerSyncDatabase).currentStatus = _testStatus; + (db as unknown as BasePowerSyncDatabase).iterateListeners((l) => l.statusChanged?.(_testStatus)); // Which should eventually run the query. await waitFor(() => expect(result.current.data).toHaveLength(1), { timeout: 1000, interval: 100 }); @@ -110,15 +111,19 @@ describe('stream hooks', () => { await waitFor(() => expect(currentStreams()).toHaveLength(1), { timeout: 1000, interval: 100 }); // Trigger sync — this causes streamsHaveSynced to transition to true - db.currentStatus = _testStatus; - db.iterateListeners((l) => l.statusChanged?.(_testStatus)); + (db as unknown as BasePowerSyncDatabase).currentStatus = _testStatus; + (db as unknown as BasePowerSyncDatabase).iterateListeners((l) => l.statusChanged?.(_testStatus)); // Wait for the query to eventually resolve await waitFor(() => expect(result.current.data).toHaveLength(1), { timeout: 1000, interval: 100 }); // Every intermediate render result should have the complete shape for (const r of allResults) { - expect(r).toMatchObject({ data: expect.any(Array), isLoading: expect.any(Boolean), isFetching: expect.any(Boolean) }); + expect(r).toMatchObject({ + data: expect.any(Array), + isLoading: expect.any(Boolean), + isFetching: expect.any(Boolean) + }); } }); @@ -285,9 +290,13 @@ describe('stream hooks', () => { }); }); -const _testStatus = new SyncStatus({ - dataFlow: { - internalStreamSubscriptions: [ +const _testStatus = new SyncStatusSnapshot( + { + connected: true, + connecting: false, + downloading: { buckets: {} }, + priority_status: [], + streams: [ { name: 'a', parameters: null, @@ -300,6 +309,6 @@ const _testStatus = new SyncStatus({ priority: 1 } ] - } -}); - + }, + {} +); diff --git a/packages/react/tests/useQuery.test.tsx b/packages/react/tests/useQuery.test.tsx index 273b5730e..3f880ae25 100644 --- a/packages/react/tests/useQuery.test.tsx +++ b/packages/react/tests/useQuery.test.tsx @@ -1,4 +1,5 @@ import * as commonSdk from '@powersync/common'; +import * as internals from '@powersync/shared-internals'; import { toCompilableQuery, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver'; import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; import { eq } from 'drizzle-orm'; @@ -205,7 +206,7 @@ describe('useQuery', () => { const hookEvents: TestEvent[] = []; - const queryObserver = new commonSdk.BaseObserver(); + const queryObserver = new internals.BaseObserver(); const baseQuery = 'SELECT * FROM lists WHERE name = ?'; const query = () => { const [query, setQuery] = React.useState({ diff --git a/packages/shared-internals/src/reexports.ts b/packages/shared-internals/src/reexports.ts deleted file mode 100644 index f34f807ad..000000000 --- a/packages/shared-internals/src/reexports.ts +++ /dev/null @@ -1 +0,0 @@ -// Declarations that all PowerSync SDKs should re-export. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 399cf817f..62f9a8222 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -682,6 +682,9 @@ importers: '@powersync/drizzle-driver': specifier: workspace:* version: link:../drizzle-driver + '@powersync/shared-internals': + specifier: 'workspace:' + version: link:../shared-internals '@powersync/web': specifier: workspace:* version: link:../web From 6a0ba744c2426d8eed1443bf385605b5d0845e24 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 16:42:39 +0200 Subject: [PATCH 09/12] Move BaseObserver back to common --- .../src/components/UserComponent.tsx | 4 +- .../components/providers/SystemProvider.tsx | 4 +- .../src/library/SupabaseConnector.ts | 46 +++++++++---------- .../src/library/TimedPowerSyncDatabase.ts | 6 +-- .../library/powersync/SupabaseConnector.ts | 2 +- packages/adapter-sql-js/src/SQLJSAdapter.ts | 3 +- .../src/adapter/CapacitorSQLiteAdapter.ts | 3 +- packages/common/etc/common.api.md | 14 ++++++ packages/common/package.json | 1 + packages/common/src/utils/BaseObserver.ts | 35 ++++++++++++++ packages/node/src/db/WorkerConnectionPool.ts | 3 +- .../src/db/OPSQLiteConnection.ts | 2 +- .../src/db/OPSqliteAdapter.ts | 3 +- .../RNQSDBAdapter.ts | 2 +- .../src/client/BasePowerSyncDatabase.ts | 4 +- .../src/client/ConnectionManager.ts | 2 +- .../client/sync/bucket/SqliteBucketStorage.ts | 2 +- .../AbstractStreamingSyncImplementation.ts | 4 +- packages/shared-internals/src/index.ts | 1 - .../src/utils/BaseObserver.ts | 33 ------------- .../src/utils/MetaBaseObserver.ts | 2 +- packages/tauri/guest-js/pool.ts | 2 +- packages/web/package.json | 3 +- packages/web/src/db/adapters/SSRDBAdapter.ts | 12 ++++- .../db/adapters/wa-sqlite/DatabaseClient.ts | 3 +- .../sync/SSRWebStreamingSyncImplementation.ts | 12 +---- .../worker/sync/SharedSyncImplementation.ts | 2 +- packages/web/tests/open.test.ts | 16 +------ 28 files changed, 118 insertions(+), 108 deletions(-) delete mode 100644 packages/shared-internals/src/utils/BaseObserver.ts diff --git a/demos/react-multi-client/src/components/UserComponent.tsx b/demos/react-multi-client/src/components/UserComponent.tsx index 752f207fd..39b59d2bf 100644 --- a/demos/react-multi-client/src/components/UserComponent.tsx +++ b/demos/react-multi-client/src/components/UserComponent.tsx @@ -200,13 +200,13 @@ export const UserComponent: React.FC = (props) => { const className = useMemo(() => { return [ - status.dataFlowStatus.downloading ? DOWNLOADING_CSS_CLASS : '', + status.downloading ? DOWNLOADING_CSS_CLASS : '', status.dataFlowStatus.uploading ? UPLOADING_CSS_CLASS : '', showOnline ? TOGGLE_ONLINE_CSS_CLASS : '' ] .join(' ') .trim(); - }, [status.dataFlowStatus.downloading, status.dataFlowStatus.uploading, showOnline]); + }, [status.downloading, status.dataFlowStatus.uploading, showOnline]); return (
diff --git a/demos/react-multi-client/src/components/providers/SystemProvider.tsx b/demos/react-multi-client/src/components/providers/SystemProvider.tsx index 2800dfcd4..3697858cc 100644 --- a/demos/react-multi-client/src/components/providers/SystemProvider.tsx +++ b/demos/react-multi-client/src/components/providers/SystemProvider.tsx @@ -16,8 +16,6 @@ export interface SystemProviderProps { const SystemProvider: React.FC> = (props) => { const { client } = useSupabase(); - const [connector] = React.useState(new SupabaseConnector(client)); - const [powersync] = React.useState( new TimedPowerSyncDatabase({ database: { @@ -29,6 +27,8 @@ const SystemProvider: React.FC> = (props) }) ); + const [connector] = React.useState(new SupabaseConnector(client, powersync)); + React.useEffect(() => { powersync.init(); diff --git a/demos/react-multi-client/src/library/SupabaseConnector.ts b/demos/react-multi-client/src/library/SupabaseConnector.ts index 78ee756f1..fc88975e6 100644 --- a/demos/react-multi-client/src/library/SupabaseConnector.ts +++ b/demos/react-multi-client/src/library/SupabaseConnector.ts @@ -1,11 +1,11 @@ import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, BaseListener, BaseObserver, CrudEntry, - Mutex, PowerSyncBackendConnector, - UpdateType + UpdateType, + Mutex } from '@powersync/web'; import { Session, SupabaseClient } from '@supabase/supabase-js'; @@ -16,15 +16,17 @@ export interface SupabaseConnectorListener extends BaseListener { } export class SupabaseConnector extends BaseObserver implements PowerSyncBackendConnector { - static SHARED_MUTEX = new Mutex(); + static SHARED_MUTEX: Mutex | null = null; readonly client: SupabaseClient; ready: boolean; currentSession: Session | null; - constructor(client: SupabaseClient) { + constructor(client: SupabaseClient, db: CommonPowerSyncDatabase) { super(); + + SupabaseConnector.SHARED_MUTEX ??= db.createMutex(); this.client = client; this.currentSession = null; this.ready = false; @@ -36,26 +38,24 @@ export class SupabaseConnector extends BaseObserver i } // Ensures that we don't accidentally check/create multiple anon sessions during initialization - const release = await SupabaseConnector.SHARED_MUTEX.acquire(); - - let sessionResponse = await this.client.auth.getSession(); - if (sessionResponse.error) { - console.error(sessionResponse.error); - throw sessionResponse.error; - } else if (!sessionResponse.data.session) { - const anonUser = await this.client.auth.signInAnonymously(); - if (anonUser.error) { - throw anonUser.error; + await SupabaseConnector.SHARED_MUTEX!.runExclusive(async () => { + let sessionResponse = await this.client.auth.getSession(); + if (sessionResponse.error) { + console.error(sessionResponse.error); + throw sessionResponse.error; + } else if (!sessionResponse.data.session) { + const anonUser = await this.client.auth.signInAnonymously(); + if (anonUser.error) { + throw anonUser.error; + } + sessionResponse = await this.client.auth.getSession(); } - sessionResponse = await this.client.auth.getSession(); - } - - this.updateSession(sessionResponse.data.session); - this.ready = true; - this.iterateListeners((cb) => cb.initialized?.()); + this.updateSession(sessionResponse.data.session); - release(); + this.ready = true; + this.iterateListeners((cb) => cb.initialized?.()); + }); } async fetchCredentials() { @@ -73,7 +73,7 @@ export class SupabaseConnector extends BaseObserver i }; } - async uploadData(database: AbstractPowerSyncDatabase): Promise { + async uploadData(database: CommonPowerSyncDatabase): Promise { const transaction = await database.getNextCrudTransaction(); if (!transaction) { return; diff --git a/demos/react-multi-client/src/library/TimedPowerSyncDatabase.ts b/demos/react-multi-client/src/library/TimedPowerSyncDatabase.ts index e533d84ea..14460d3b5 100644 --- a/demos/react-multi-client/src/library/TimedPowerSyncDatabase.ts +++ b/demos/react-multi-client/src/library/TimedPowerSyncDatabase.ts @@ -1,10 +1,10 @@ import { - PowerSyncDatabase, PowerSyncDBListener, Transaction, WebPowerSyncDatabaseOptions, PowerSyncBackendConnector, - LockContext + LockContext, + WebPowerSyncDatabase } from '@powersync/web'; export enum OperationType { @@ -24,7 +24,7 @@ export interface TimedPowerSyncListener extends PowerSyncDBListener { operationCompleted: (event: TimedOperation) => void; } -export class TimedPowerSyncDatabase extends PowerSyncDatabase { +export class TimedPowerSyncDatabase extends WebPowerSyncDatabase { localKey: string; constructor(options: WebPowerSyncDatabaseOptions) { diff --git a/demos/vue-supabase-todolist/src/library/powersync/SupabaseConnector.ts b/demos/vue-supabase-todolist/src/library/powersync/SupabaseConnector.ts index 07472b7e0..cb3f26fce 100644 --- a/demos/vue-supabase-todolist/src/library/powersync/SupabaseConnector.ts +++ b/demos/vue-supabase-todolist/src/library/powersync/SupabaseConnector.ts @@ -1,5 +1,5 @@ import { - AbstractPowerSyncDatabase, + CommonPowerSyncDatabase, BaseObserver, CrudEntry, PowerSyncBackendConnector, diff --git a/packages/adapter-sql-js/src/SQLJSAdapter.ts b/packages/adapter-sql-js/src/SQLJSAdapter.ts index a1aa919d2..c00d3524c 100644 --- a/packages/adapter-sql-js/src/SQLJSAdapter.ts +++ b/packages/adapter-sql-js/src/SQLJSAdapter.ts @@ -1,4 +1,5 @@ import { + BaseObserver, BaseListener, BatchedUpdateNotification, ConnectionPool, @@ -17,7 +18,7 @@ import { SQLOpenOptions, Transaction } from '@powersync/common'; -import { Mutex, timeoutSignal, BaseObserver, ControlledExecutor } from '@powersync/shared-internals'; +import { Mutex, timeoutSignal, ControlledExecutor } from '@powersync/shared-internals'; // This uses a pure JS version which avoids the need for WebAssembly, which is not supported in React Native. import SQLJs from '@powersync/sql-js/dist/sql-asm.js'; diff --git a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts index 5f3442554..cc1f94984 100644 --- a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts +++ b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts @@ -2,6 +2,7 @@ import { CapacitorSQLite, SQLiteConnection, SQLiteDBConnection } from '@capacito import { Capacitor } from '@capacitor/core'; import { + BaseObserver, BatchedUpdateNotification, ConnectionPool, DBAdapter, @@ -11,7 +12,7 @@ import { LockContext, QueryResult } from '@powersync/web'; -import { BaseObserver, Mutex, timeoutSignal } from '@powersync/shared-internals'; +import { Mutex, timeoutSignal } from '@powersync/shared-internals'; import { PowerSyncCore } from '../plugin/PowerSyncCore.js'; import { messageForErrorCode } from '../plugin/PowerSyncPlugin.js'; import { CapacitorSQLiteOpenFactoryOptions, DEFAULT_SQLITE_OPTIONS } from './CapacitorSQLiteOpenFactory.js'; diff --git a/packages/common/etc/common.api.md b/packages/common/etc/common.api.md index e7df23516..977e442ff 100644 --- a/packages/common/etc/common.api.md +++ b/packages/common/etc/common.api.md @@ -177,6 +177,20 @@ export interface BaseCreateDiffTriggerOptions { // @public (undocumented) export type BaseListener = Record any) | undefined>; +// @public (undocumented) +export class BaseObserver implements BaseObserverInterface { + constructor(); + // (undocumented) + dispose(): void; + // (undocumented) + iterateAsyncListeners(cb: (listener: Partial) => Promise): Promise; + // (undocumented) + iterateListeners(cb: (listener: Partial) => any): void; + // (undocumented) + protected listeners: Set>; + registerListener(listener: Partial): () => void; +} + // @public (undocumented) export interface BaseObserverInterface { // (undocumented) diff --git a/packages/common/package.json b/packages/common/package.json index eb4a2e2ef..e32c0f7c4 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -7,6 +7,7 @@ }, "description": "API definitions for PowerSync", "type": "module", + "types": "./lib/index.d.ts", "exports": { ".": { "types": "./lib/index.d.ts", diff --git a/packages/common/src/utils/BaseObserver.ts b/packages/common/src/utils/BaseObserver.ts index 548cadc24..c3c7a42d7 100644 --- a/packages/common/src/utils/BaseObserver.ts +++ b/packages/common/src/utils/BaseObserver.ts @@ -16,3 +16,38 @@ export type BaseListener = Record any) | undefined>; export interface BaseObserverInterface { registerListener(listener: Partial): () => void; } + +/** + * @public + */ +export class BaseObserver implements BaseObserverInterface { + protected listeners = new Set>(); + + constructor() {} + + dispose(): void { + this.listeners.clear(); + } + + /** + * Register a listener for updates to the PowerSync client. + */ + registerListener(listener: Partial): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + iterateListeners(cb: (listener: Partial) => any) { + for (const listener of this.listeners) { + cb(listener); + } + } + + async iterateAsyncListeners(cb: (listener: Partial) => Promise) { + for (let i of Array.from(this.listeners.values())) { + await cb(i); + } + } +} diff --git a/packages/node/src/db/WorkerConnectionPool.ts b/packages/node/src/db/WorkerConnectionPool.ts index 184ac0b44..f85dd73f6 100644 --- a/packages/node/src/db/WorkerConnectionPool.ts +++ b/packages/node/src/db/WorkerConnectionPool.ts @@ -4,6 +4,7 @@ import * as path from 'node:path'; import { Worker } from 'node:worker_threads'; import { + BaseObserver, BatchedUpdateNotification, ConnectionPool, DBAdapterDefaultMixin, @@ -13,7 +14,7 @@ import { QueryResult, Transaction } from '@powersync/common'; -import { BaseObserver, Semaphore, timeoutSignal } from '@powersync/shared-internals'; +import { Semaphore, timeoutSignal } from '@powersync/shared-internals'; import { Remote } from 'comlink'; import { AsyncDatabase, AsyncDatabaseOpener } from './AsyncDatabase.js'; import { RemoteConnection } from './RemoteConnection.js'; diff --git a/packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts b/packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts index 6f60bb563..80c249aee 100644 --- a/packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts +++ b/packages/powersync-op-sqlite/src/db/OPSQLiteConnection.ts @@ -1,5 +1,6 @@ import { DB, SQLBatchTuple, UpdateHookOperation } from '@op-engineering/op-sqlite'; import { + BaseObserver, BatchedUpdateNotification, DBAdapterListener, DBGetUtilsDefaultMixin, @@ -9,7 +10,6 @@ import { SqlExecutor, UpdateNotification } from '@powersync/common'; -import { BaseObserver } from '@powersync/shared-internals'; export type OPSQLiteConnectionOptions = { baseDB: DB; diff --git a/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts b/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts index 28d238bdb..7fe9c87f0 100644 --- a/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts +++ b/packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts @@ -1,5 +1,6 @@ import { getDylibPath, open, type DB } from '@op-engineering/op-sqlite'; import { + BaseObserver, ConnectionPool, DBAdapter, DBAdapterDefaultMixin, @@ -8,7 +9,7 @@ import { QueryResult, Transaction } from '@powersync/common'; -import { timeoutSignal, Semaphore, BaseObserver } from '@powersync/shared-internals'; +import { timeoutSignal, Semaphore } from '@powersync/shared-internals'; import { Platform } from 'react-native'; import { OPSQLiteConnection } from './OPSQLiteConnection'; import { SqliteOptions } from './SqliteOptions'; diff --git a/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts b/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts index 81d8e7006..1df89bbb9 100644 --- a/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts +++ b/packages/react-native/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts @@ -1,4 +1,5 @@ import { + BaseObserver, DBAdapter, DBAdapterListener, LockContext as PowerSyncLockContext, @@ -11,7 +12,6 @@ import { } from '@powersync/common'; import type { QuickSQLiteConnection, LockContext as RNQSLockContext } from '@journeyapps/react-native-quick-sqlite'; import { QueryResult, SqlExecutor } from '@powersync/common'; -import { BaseObserver } from '@powersync/shared-internals'; class RNQSConnectionPool extends BaseObserver implements ConnectionPool { constructor( diff --git a/packages/shared-internals/src/client/BasePowerSyncDatabase.ts b/packages/shared-internals/src/client/BasePowerSyncDatabase.ts index e9e9b8ff4..d19f41767 100644 --- a/packages/shared-internals/src/client/BasePowerSyncDatabase.ts +++ b/packages/shared-internals/src/client/BasePowerSyncDatabase.ts @@ -31,7 +31,8 @@ import { WatchCompatibleQuery, WatchHandler, WatchOnChangeEvent, - WatchOnChangeHandler + WatchOnChangeHandler, + BaseObserver } from '@powersync/common'; import { BucketStorageAdapter, PSInternalTable } from './sync/bucket/BucketStorageAdapter.js'; import { EventIterator } from 'event-iterator'; @@ -42,7 +43,6 @@ import { InternalSubscriptionAdapter } from './ConnectionManager.js'; import { Mutex } from '../utils/mutex.js'; -import { BaseObserver } from '../utils/BaseObserver.js'; import { TriggerManagerConfig, TriggerManagerImpl } from './triggers/TriggerManagerImpl.js'; import { StreamingSyncImplementation } from './sync/stream/AbstractStreamingSyncImplementation.js'; import { CoreSyncStatus } from './sync/stream/core-instruction.js'; diff --git a/packages/shared-internals/src/client/ConnectionManager.ts b/packages/shared-internals/src/client/ConnectionManager.ts index 54a6ca10e..2945894e5 100644 --- a/packages/shared-internals/src/client/ConnectionManager.ts +++ b/packages/shared-internals/src/client/ConnectionManager.ts @@ -1,4 +1,5 @@ import { + BaseObserver, LogLevels, PowerSyncLogger, SyncStatus, @@ -11,7 +12,6 @@ import { SyncOptions } from '@powersync/common'; -import { BaseObserver } from '../utils/BaseObserver.js'; import { StreamingSyncImplementation, SubscribedStream } from './sync/stream/AbstractStreamingSyncImplementation.js'; import { ResolvedSyncOptions, resolveSyncOptions } from './sync/options.js'; diff --git a/packages/shared-internals/src/client/sync/bucket/SqliteBucketStorage.ts b/packages/shared-internals/src/client/sync/bucket/SqliteBucketStorage.ts index 66aeba269..f7eea89fd 100644 --- a/packages/shared-internals/src/client/sync/bucket/SqliteBucketStorage.ts +++ b/packages/shared-internals/src/client/sync/bucket/SqliteBucketStorage.ts @@ -1,4 +1,5 @@ import { + BaseObserver, LogLevels, PowerSyncLogger, DBAdapter, @@ -8,7 +9,6 @@ import { CrudBatch } from '@powersync/common'; -import { BaseObserver } from '../../../utils/BaseObserver.js'; import { BucketStorageAdapter, BucketStorageListener, diff --git a/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index 6d0c7f01d..d6197cd63 100644 --- a/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/shared-internals/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -7,11 +7,11 @@ import { PowerSyncLogger, SyncStreamConnectionMethod, SyncStatus, - SyncDataFlowStatus + SyncDataFlowStatus, + BaseObserver } from '@powersync/common'; import { AbortOperation } from '../../../utils/AbortOperation.js'; -import { BaseObserver } from '../../../utils/BaseObserver.js'; import { BucketStorageAdapter, PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js'; import { AbstractRemote, SyncStreamOptions } from './AbstractRemote.js'; import { diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts index cf62aa312..8db0c6e1f 100644 --- a/packages/shared-internals/src/index.ts +++ b/packages/shared-internals/src/index.ts @@ -17,7 +17,6 @@ export * from './client/watched/DifferentialQueryProcessor.js'; export * from './client/watched/OnChangeQueryProcessor.js'; export * from './utils/AbortOperation.js'; -export * from './utils/BaseObserver.js'; export * from './utils/ControlledExecutor.js'; export * from './utils/mutex.js'; export * from './utils/parseQuery.js'; diff --git a/packages/shared-internals/src/utils/BaseObserver.ts b/packages/shared-internals/src/utils/BaseObserver.ts deleted file mode 100644 index dde41d0ee..000000000 --- a/packages/shared-internals/src/utils/BaseObserver.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { BaseListener, BaseObserverInterface } from '@powersync/common'; - -export class BaseObserver implements BaseObserverInterface { - protected listeners = new Set>(); - - constructor() {} - - dispose(): void { - this.listeners.clear(); - } - - /** - * Register a listener for updates to the PowerSync client. - */ - registerListener(listener: Partial): () => void { - this.listeners.add(listener); - return () => { - this.listeners.delete(listener); - }; - } - - iterateListeners(cb: (listener: Partial) => any) { - for (const listener of this.listeners) { - cb(listener); - } - } - - async iterateAsyncListeners(cb: (listener: Partial) => Promise) { - for (let i of Array.from(this.listeners.values())) { - await cb(i); - } - } -} diff --git a/packages/shared-internals/src/utils/MetaBaseObserver.ts b/packages/shared-internals/src/utils/MetaBaseObserver.ts index fec979ef9..868803a6c 100644 --- a/packages/shared-internals/src/utils/MetaBaseObserver.ts +++ b/packages/shared-internals/src/utils/MetaBaseObserver.ts @@ -1,11 +1,11 @@ import { + BaseObserver, BaseListener, ListenerCounts, ListenerMetaManager, MetaBaseObserverInterface, MetaListener } from '@powersync/common'; -import { BaseObserver } from './BaseObserver.js'; /** * A BaseObserver that tracks the counts of listeners for each event type. diff --git a/packages/tauri/guest-js/pool.ts b/packages/tauri/guest-js/pool.ts index 8d27423bf..916236e2e 100644 --- a/packages/tauri/guest-js/pool.ts +++ b/packages/tauri/guest-js/pool.ts @@ -1,4 +1,5 @@ import { + BaseObserver, ConnectionPool, DBAdapterDefaultMixin, DBAdapterListener, @@ -8,7 +9,6 @@ import { QueryResult, SqlExecutor } from '@powersync/common'; -import { BaseObserver } from '@powersync/shared-internals'; import { ExecuteBatchResult, ExecuteSqlResult, powersyncCommand, SqliteValue } from './command'; /** diff --git a/packages/web/package.json b/packages/web/package.json index 0382e8e50..dd82d63fd 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -67,7 +67,8 @@ "license": "Apache-2.0", "peerDependencies": { "@journeyapps/wa-sqlite": "catalog:", - "@powersync/common": "workspace:^1.54.0" + "@powersync/common": "workspace:^1.54.0", + "@powersync/shared-internals": "workspace:^1.0.0" }, "dependencies": { "@powersync/common": "workspace:*", diff --git a/packages/web/src/db/adapters/SSRDBAdapter.ts b/packages/web/src/db/adapters/SSRDBAdapter.ts index dd9a18cbc..c4a74ef34 100644 --- a/packages/web/src/db/adapters/SSRDBAdapter.ts +++ b/packages/web/src/db/adapters/SSRDBAdapter.ts @@ -1,5 +1,13 @@ -import { DBAdapterListener, DBAdapter, DBLockOptions, LockContext, QueryResult, Transaction } from '@powersync/common'; -import { BaseObserver, Mutex, timeoutSignal } from '@powersync/shared-internals'; +import { + BaseObserver, + DBAdapterListener, + DBAdapter, + DBLockOptions, + LockContext, + QueryResult, + Transaction +} from '@powersync/common'; +import { Mutex, timeoutSignal } from '@powersync/shared-internals'; const MOCK_QUERY_RESPONSE: QueryResult = { rowsAffected: 0 diff --git a/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts b/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts index d8ad596db..f2946f86e 100644 --- a/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts +++ b/packages/web/src/db/adapters/wa-sqlite/DatabaseClient.ts @@ -1,4 +1,5 @@ import { + BaseObserver, QueryResult, LockContext, DBLockOptions, @@ -14,7 +15,7 @@ import { ClientConnectionView } from './DatabaseServer.js'; import { RawQueryResult } from './RawSqliteConnection.js'; import * as Comlink from 'comlink'; import type { ConnectToMultiDatabaseServerOptions } from '../../../worker/db/MultiDatabaseServer.js'; -import { BaseObserver, ConnectionClosedError } from '@powersync/shared-internals'; +import { ConnectionClosedError } from '@powersync/shared-internals'; export interface OpenWorkerConnection { connect(config: ConnectToMultiDatabaseServerOptions): Promise; diff --git a/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts index fa90ba2bb..9da712407 100644 --- a/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SSRWebStreamingSyncImplementation.ts @@ -1,13 +1,5 @@ -import { SyncStatus } from '@powersync/common'; -import { - AbstractStreamingSyncImplementationOptions, - BaseObserver, - Mutex, - StreamingSyncImplementation, - LockOptions, - LockType, - SyncStatusSnapshot -} from '@powersync/shared-internals'; +import { BaseObserver, SyncStatus } from '@powersync/common'; +import { Mutex, StreamingSyncImplementation, LockOptions, LockType } from '@powersync/shared-internals'; export class SSRStreamingSyncImplementation extends BaseObserver implements StreamingSyncImplementation { syncMutex: Mutex; diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 9df64eed8..9f2376ecd 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -1,4 +1,5 @@ import { + BaseObserver, ConnectionPool, DBAdapter, DBAdapterDefaultMixin, @@ -11,7 +12,6 @@ import { } from '@powersync/common'; import { AbortOperation, - BaseObserver, ConnectionManager, SqliteBucketStorage, SubscribedStream, diff --git a/packages/web/tests/open.test.ts b/packages/web/tests/open.test.ts index dacaeba3f..177b8eef7 100644 --- a/packages/web/tests/open.test.ts +++ b/packages/web/tests/open.test.ts @@ -1,17 +1,5 @@ -import { - CommonPowerSyncDatabase, - createConsoleLogger, - DBAdapterDefaultMixin, - LogLevels, - Schema -} from '@powersync/common'; -import { - PowerSyncDatabase, - ResolvedWebSQLOpenOptions, - TemporaryStorageOption, - WASQLiteOpenFactory, - WASQLiteVFS -} from '@powersync/web'; +import { CommonPowerSyncDatabase, createConsoleLogger, DBAdapterDefaultMixin, Schema } from '@powersync/common'; +import { PowerSyncDatabase, TemporaryStorageOption, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TEST_SCHEMA } from './utils/test-schema.js'; import { MultiDatabaseServer } from '../src/worker/db/MultiDatabaseServer.js'; From d4e7cb91e574ca3977d022be916973e8925fe3c2 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 16:54:09 +0200 Subject: [PATCH 10/12] Fix nextjs demo, react tests --- demos/example-nextjs/src/components/StatusPanel.tsx | 4 ++-- packages/react/tests/useQuery.test.tsx | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/demos/example-nextjs/src/components/StatusPanel.tsx b/demos/example-nextjs/src/components/StatusPanel.tsx index 7df4a6413..6028ba009 100644 --- a/demos/example-nextjs/src/components/StatusPanel.tsx +++ b/demos/example-nextjs/src/components/StatusPanel.tsx @@ -39,8 +39,8 @@ function ArrowDown() { export function StatusPanel() { const status = useStatus(); - const { connected, hasSynced, dataFlowStatus } = status; - const { uploading, downloading, uploadError, downloadError } = dataFlowStatus; + const { connected, hasSynced, downloading, dataFlowStatus } = status; + const { uploading, uploadError, downloadError } = dataFlowStatus; let label = 'Connecting…'; let chipColor: keyof typeof chipStyles = 'warning'; diff --git a/packages/react/tests/useQuery.test.tsx b/packages/react/tests/useQuery.test.tsx index 3f880ae25..273b5730e 100644 --- a/packages/react/tests/useQuery.test.tsx +++ b/packages/react/tests/useQuery.test.tsx @@ -1,5 +1,4 @@ import * as commonSdk from '@powersync/common'; -import * as internals from '@powersync/shared-internals'; import { toCompilableQuery, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver'; import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; import { eq } from 'drizzle-orm'; @@ -206,7 +205,7 @@ describe('useQuery', () => { const hookEvents: TestEvent[] = []; - const queryObserver = new internals.BaseObserver(); + const queryObserver = new commonSdk.BaseObserver(); const baseQuery = 'SELECT * FROM lists WHERE name = ?'; const query = () => { const [query, setQuery] = React.useState({ From 521417a2c4991a1f628896d6009b549c026b3d8f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 17:08:36 +0200 Subject: [PATCH 11/12] Fix import tests --- pnpm-lock.yaml | 3 +++ tools/import-tests/package.json | 3 ++- tools/import-tests/src/cjs-test.cjs | 7 +++++-- tools/import-tests/src/esm-test.mjs | 7 +++++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62f9a8222..81bb534cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1102,6 +1102,9 @@ importers: '@powersync/capacitor': specifier: workspace:* version: link:../../packages/capacitor + '@powersync/common': + specifier: workspace:* + version: link:../../packages/common '@powersync/drizzle-driver': specifier: workspace:* version: link:../../packages/drizzle-driver diff --git a/tools/import-tests/package.json b/tools/import-tests/package.json index a69bc82eb..86793f989 100644 --- a/tools/import-tests/package.json +++ b/tools/import-tests/package.json @@ -15,6 +15,7 @@ "@powersync/kysely-driver": "workspace:*", "@powersync/node": "workspace:*", "@powersync/capacitor": "workspace:*", - "@powersync/web": "workspace:*" + "@powersync/web": "workspace:*", + "@powersync/common": "workspace:*" } } diff --git a/tools/import-tests/src/cjs-test.cjs b/tools/import-tests/src/cjs-test.cjs index 5e0c96303..469199aff 100644 --- a/tools/import-tests/src/cjs-test.cjs +++ b/tools/import-tests/src/cjs-test.cjs @@ -1,9 +1,12 @@ const assert = require('assert'); const NodeSDK = require('@powersync/node'); -const { ControlledExecutor } = require('@powersync/node'); +const { PowerSyncDatabase } = require('@powersync/node'); assert(NodeSDK); -assert(ControlledExecutor); +assert(PowerSyncDatabase); + +const { AttachmentContext } = require('@powersync/common'); +assert(AttachmentContext); const Attachments = require('@powersync/attachments'); const { AttachmentState } = require('@powersync/attachments'); diff --git a/tools/import-tests/src/esm-test.mjs b/tools/import-tests/src/esm-test.mjs index e48d52732..63c277aae 100644 --- a/tools/import-tests/src/esm-test.mjs +++ b/tools/import-tests/src/esm-test.mjs @@ -1,9 +1,12 @@ import assert from 'assert'; import * as NodeSDK from '@powersync/node'; -import { ControlledExecutor } from '@powersync/node'; +import { PowerSyncDatabase } from '@powersync/node'; assert(NodeSDK); -assert(ControlledExecutor); +assert(PowerSyncDatabase); + +import { AttachmentContext } from '@powersync/common'; +assert(AttachmentContext); // Simulates SSR environment import * as WebSDK from '@powersync/web'; From 98c6f74ed284749fe61d97da936fa08c4c8ed9ff Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 15 Jun 2026 17:18:02 +0200 Subject: [PATCH 12/12] Fix tanstack query tests --- .../tests/streams.test.tsx | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/packages/tanstack-react-query/tests/streams.test.tsx b/packages/tanstack-react-query/tests/streams.test.tsx index 8a2aa3e28..b0f71041c 100644 --- a/packages/tanstack-react-query/tests/streams.test.tsx +++ b/packages/tanstack-react-query/tests/streams.test.tsx @@ -1,7 +1,8 @@ import { cleanup, renderHook, waitFor } from '@testing-library/react'; import { describe, expect, vi } from 'vitest'; import React, { act, useSyncExternalStore } from 'react'; -import { AbstractPowerSyncDatabase, ConnectionManager, SyncStatus } from '@powersync/common'; +import { CommonPowerSyncDatabase } from '@powersync/common'; +import { BasePowerSyncDatabase, ConnectionManager, SyncStatusSnapshot } from '@powersync/shared-internals'; import { openPowerSync } from './utils'; import { PowerSyncContext } from '@powersync/react'; import { useQuery } from '../src/hooks/useQuery'; @@ -10,7 +11,7 @@ import { QuerySyncStreamOptions } from '@powersync/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; describe('stream hooks', () => { - let db: AbstractPowerSyncDatabase; + let db: CommonPowerSyncDatabase; let queryClient = new QueryClient({ defaultOptions: { queries: { @@ -52,9 +53,12 @@ describe('stream hooks', () => { testCases.forEach(({ mode, wrapper: testWrapper }) => { describe(`in ${mode}`, () => { it('can take syncStream instance', async () => { - const { result } = renderHook(() => useQuery({ queryKey: ['test'], query: 'SELECT 1', streams: [db.syncStream('a')] }), { - wrapper: testWrapper - }); + const { result } = renderHook( + () => useQuery({ queryKey: ['test'], query: 'SELECT 1', streams: [db.syncStream('a')] }), + { + wrapper: testWrapper + } + ); // Not resolving the subscription. await waitFor(() => expect(result.current.data).toHaveLength(1), { timeout: 1000, interval: 100 }); @@ -79,8 +83,8 @@ describe('stream hooks', () => { expect(getAllSpy).not.toHaveBeenCalledWith('SELECT 1', expect.anything()); // Set last_synced_at for the subscription - db.currentStatus = _testStatus; - db.iterateListeners((l) => l.statusChanged?.(_testStatus)); + (db as unknown as BasePowerSyncDatabase).currentStatus = _testStatus; + (db as unknown as BasePowerSyncDatabase).iterateListeners((l) => l.statusChanged?.(_testStatus)); // Which should eventually run the query. await waitFor(() => expect(result.current.data).toHaveLength(1), { timeout: 1000, interval: 100 }); @@ -90,18 +94,24 @@ describe('stream hooks', () => { it('not waiting on stream', async () => { // By default, it should still run the query immediately instead of waiting for the stream to resolve. - const { result } = renderHook(() => useQuery({ queryKey: ['test'], query: 'SELECT 1', streams: [{ name: 'a' }] }), { - wrapper: testWrapper - }); + const { result } = renderHook( + () => useQuery({ queryKey: ['test'], query: 'SELECT 1', streams: [{ name: 'a' }] }), + { + wrapper: testWrapper + } + ); // Not resolving the subscription. await waitFor(() => expect(result.current.data).toHaveLength(1), { timeout: 1000, interval: 100 }); }); it('unsubscribes on unmount', async () => { - const { unmount } = renderHook(() => useQuery({ queryKey: ['test'], query: 'SELECT 1', streams: [{ name: 'a' }, { name: 'b' }] }), { - wrapper: testWrapper - }); + const { unmount } = renderHook( + () => useQuery({ queryKey: ['test'], query: 'SELECT 1', streams: [{ name: 'a' }, { name: 'b' }] }), + { + wrapper: testWrapper + } + ); await waitFor(() => expect(currentStreams()).toHaveLength(2), { timeout: 1000, interval: 100 }); unmount(); @@ -205,8 +215,8 @@ describe('stream hooks', () => { expect(getAllSpy).not.toHaveBeenCalledWith('SELECT 2', expect.anything()); // Set last_synced_at for the subscription - db.currentStatus = _testStatus; - db.iterateListeners((l) => l.statusChanged?.(_testStatus)); + (db as unknown as BasePowerSyncDatabase).currentStatus = _testStatus; + (db as unknown as BasePowerSyncDatabase).iterateListeners((l) => l.statusChanged?.(_testStatus)); // Which should eventually run both queries. await waitFor( @@ -360,9 +370,13 @@ describe('stream hooks', () => { }); }); -const _testStatus = new SyncStatus({ - dataFlow: { - internalStreamSubscriptions: [ +const _testStatus = new SyncStatusSnapshot( + { + connected: true, + connecting: false, + downloading: { buckets: {} }, + priority_status: [], + streams: [ { name: 'a', parameters: null, @@ -375,5 +389,6 @@ const _testStatus = new SyncStatus({ priority: 1 } ] - } -}); + }, + {} +);