@powersync/common 1.36.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -2217,6 +2217,368 @@ declare class CrudTransaction extends CrudBatch {
2217
2217
  transactionId?: number | undefined);
2218
2218
  }
2219
2219
 
2220
+ /**
2221
+ * SQLite operations to track changes for with {@link TriggerManager}
2222
+ * @experimental
2223
+ */
2224
+ declare enum DiffTriggerOperation {
2225
+ INSERT = "INSERT",
2226
+ UPDATE = "UPDATE",
2227
+ DELETE = "DELETE"
2228
+ }
2229
+ /**
2230
+ * @experimental
2231
+ * Diffs created by {@link TriggerManager#createDiffTrigger} are stored in a temporary table.
2232
+ * This is the base record structure for all diff records.
2233
+ */
2234
+ interface BaseTriggerDiffRecord {
2235
+ /**
2236
+ * The modified row's `id` column value.
2237
+ */
2238
+ id: string;
2239
+ /**
2240
+ * The operation performed which created this record.
2241
+ */
2242
+ operation: DiffTriggerOperation;
2243
+ /**
2244
+ * Time the change operation was recorded.
2245
+ * This is in ISO 8601 format, e.g. `2023-10-01T12:00:00.000Z`.
2246
+ */
2247
+ timestamp: string;
2248
+ }
2249
+ /**
2250
+ * @experimental
2251
+ * Represents a diff record for a SQLite UPDATE operation.
2252
+ * This record contains the new value and optionally the previous value.
2253
+ * Values are stored as JSON strings.
2254
+ */
2255
+ interface TriggerDiffUpdateRecord extends BaseTriggerDiffRecord {
2256
+ operation: DiffTriggerOperation.UPDATE;
2257
+ /**
2258
+ * The updated state of the row in JSON string format.
2259
+ */
2260
+ value: string;
2261
+ /**
2262
+ * The previous value of the row in JSON string format.
2263
+ */
2264
+ previous_value: string;
2265
+ }
2266
+ /**
2267
+ * @experimental
2268
+ * Represents a diff record for a SQLite INSERT operation.
2269
+ * This record contains the new value represented as a JSON string.
2270
+ */
2271
+ interface TriggerDiffInsertRecord extends BaseTriggerDiffRecord {
2272
+ operation: DiffTriggerOperation.INSERT;
2273
+ /**
2274
+ * The value of the row, at the time of INSERT, in JSON string format.
2275
+ */
2276
+ value: string;
2277
+ }
2278
+ /**
2279
+ * @experimental
2280
+ * Represents a diff record for a SQLite DELETE operation.
2281
+ * This record contains the new value represented as a JSON string.
2282
+ */
2283
+ interface TriggerDiffDeleteRecord extends BaseTriggerDiffRecord {
2284
+ operation: DiffTriggerOperation.DELETE;
2285
+ /**
2286
+ * The value of the row, before the DELETE operation, in JSON string format.
2287
+ */
2288
+ value: string;
2289
+ }
2290
+ /**
2291
+ * @experimental
2292
+ * Diffs created by {@link TriggerManager#createDiffTrigger} are stored in a temporary table.
2293
+ * This is the record structure for all diff records.
2294
+ *
2295
+ * Querying the DIFF table directly with {@link TriggerDiffHandlerContext#withDiff} will return records
2296
+ * with the structure of this type.
2297
+ * @example
2298
+ * ```typescript
2299
+ * const diffs = await context.withDiff<TriggerDiffRecord>('SELECT * FROM DIFF');
2300
+ * diff.forEach(diff => console.log(diff.operation, diff.timestamp, JSON.parse(diff.value)))
2301
+ * ```
2302
+ */
2303
+ type TriggerDiffRecord = TriggerDiffUpdateRecord | TriggerDiffInsertRecord | TriggerDiffDeleteRecord;
2304
+ /**
2305
+ * @experimental
2306
+ * Querying the DIFF table directly with {@link TriggerDiffHandlerContext#withExtractedDiff} will return records
2307
+ * with the tracked columns extracted from the JSON value.
2308
+ * This type represents the structure of such records.
2309
+ * @example
2310
+ * ```typescript
2311
+ * const diffs = await context.withExtractedDiff<ExtractedTriggerDiffRecord<{id: string, name: string}>>('SELECT * FROM DIFF');
2312
+ * diff.forEach(diff => console.log(diff.__operation, diff.__timestamp, diff.columnName))
2313
+ * ```
2314
+ */
2315
+ type ExtractedTriggerDiffRecord<T> = T & {
2316
+ [K in keyof Omit<BaseTriggerDiffRecord, 'id'> as `__${string & K}`]: TriggerDiffRecord[K];
2317
+ } & {
2318
+ __previous_value?: string;
2319
+ };
2320
+ /**
2321
+ * @experimental
2322
+ * Hooks used in the creation of a table diff trigger.
2323
+ */
2324
+ interface TriggerCreationHooks {
2325
+ /**
2326
+ * Executed inside a write lock before the trigger is created.
2327
+ */
2328
+ beforeCreate?: (context: LockContext) => Promise<void>;
2329
+ }
2330
+ /**
2331
+ * Common interface for options used in creating a diff trigger.
2332
+ */
2333
+ interface BaseCreateDiffTriggerOptions {
2334
+ /**
2335
+ * PowerSync source table/view to trigger and track changes from.
2336
+ * This should be present in the PowerSync database's schema.
2337
+ */
2338
+ source: string;
2339
+ /**
2340
+ * Columns to track and report changes for.
2341
+ * Defaults to all columns in the source table.
2342
+ * Use an empty array to track only the ID and operation.
2343
+ */
2344
+ columns?: string[];
2345
+ /**
2346
+ * Condition to filter when the triggers should fire.
2347
+ * This corresponds to a SQLite [WHEN](https://sqlite.org/lang_createtrigger.html) clause in the trigger body.
2348
+ * This is useful for only triggering on specific conditions.
2349
+ * For example, you can use it to only trigger on certain values in the NEW row.
2350
+ * Note that for PowerSync the row data is stored in a JSON column named `data`.
2351
+ * The row id is available in the `id` column.
2352
+ *
2353
+ * NB! The WHEN clauses here are added directly to the SQLite trigger creation SQL.
2354
+ * Any user input strings here should be sanitized externally. The {@link when} string template function performs
2355
+ * some basic sanitization, extra external sanitization is recommended.
2356
+ *
2357
+ * @example
2358
+ * {
2359
+ * 'INSERT': sanitizeSQL`json_extract(NEW.data, '$.list_id') = ${sanitizeUUID(list.id)}`,
2360
+ * 'INSERT': `TRUE`,
2361
+ * 'UPDATE': sanitizeSQL`NEW.id = 'abcd' AND json_extract(NEW.data, '$.status') = 'active'`,
2362
+ * 'DELETE': sanitizeSQL`json_extract(OLD.data, '$.list_id') = 'abcd'`
2363
+ * }
2364
+ */
2365
+ when: Partial<Record<DiffTriggerOperation, string>>;
2366
+ /**
2367
+ * Hooks which allow execution during the trigger creation process.
2368
+ */
2369
+ hooks?: TriggerCreationHooks;
2370
+ }
2371
+ /**
2372
+ * @experimental
2373
+ * Options for {@link TriggerManager#createDiffTrigger}.
2374
+ */
2375
+ interface CreateDiffTriggerOptions extends BaseCreateDiffTriggerOptions {
2376
+ /**
2377
+ * Destination table to send changes to.
2378
+ * This table is created internally as a SQLite temporary table.
2379
+ * This table will be dropped once the trigger is removed.
2380
+ */
2381
+ destination: string;
2382
+ }
2383
+ /**
2384
+ * @experimental
2385
+ * Callback to drop a trigger after it has been created.
2386
+ */
2387
+ type TriggerRemoveCallback = () => Promise<void>;
2388
+ /**
2389
+ * @experimental
2390
+ * Context for the `onChange` handler provided to {@link TriggerManager#trackTableDiff}.
2391
+ */
2392
+ interface TriggerDiffHandlerContext extends LockContext {
2393
+ /**
2394
+ * The name of the temporary destination table created by the trigger.
2395
+ */
2396
+ destinationTable: string;
2397
+ /**
2398
+ * Allows querying the database with access to the table containing DIFF records.
2399
+ * The diff table is accessible via the `DIFF` accessor.
2400
+ *
2401
+ * The `DIFF` table is of the form described in {@link TriggerManager#createDiffTrigger}
2402
+ * ```sql
2403
+ * CREATE TEMP DIFF (
2404
+ * id TEXT,
2405
+ * operation TEXT,
2406
+ * timestamp TEXT
2407
+ * value TEXT,
2408
+ * previous_value TEXT
2409
+ * );
2410
+ * ```
2411
+ *
2412
+ * Note that the `value` and `previous_value` columns store the row state in JSON string format.
2413
+ * To access the row state in an extracted form see {@link TriggerDiffHandlerContext#withExtractedDiff}.
2414
+ *
2415
+ * @example
2416
+ * ```sql
2417
+ * --- This fetches the current state of `todo` rows which have a diff operation present.
2418
+ * --- The state of the row at the time of the operation is accessible in the DIFF records.
2419
+ * SELECT
2420
+ * todos.*
2421
+ * FROM
2422
+ * DIFF
2423
+ * JOIN todos ON DIFF.id = todos.id
2424
+ * WHERE json_extract(DIFF.value, '$.status') = 'active'
2425
+ * ```
2426
+ */
2427
+ withDiff: <T = any>(query: string, params?: ReadonlyArray<Readonly<any>>) => Promise<T[]>;
2428
+ /**
2429
+ * Allows querying the database with access to the table containing diff records.
2430
+ * The diff table is accessible via the `DIFF` accessor.
2431
+ *
2432
+ * This is similar to {@link withDiff} but extracts the row columns from the tracked JSON value. The diff operation
2433
+ * data is aliased as `__` columns to avoid column conflicts.
2434
+ *
2435
+ * For {@link DiffTriggerOperation#DELETE} operations the previous_value columns are extracted for convenience.
2436
+ *
2437
+ *
2438
+ * ```sql
2439
+ * CREATE TEMP TABLE DIFF (
2440
+ * id TEXT,
2441
+ * replicated_column_1 COLUMN_TYPE,
2442
+ * replicated_column_2 COLUMN_TYPE,
2443
+ * __operation TEXT,
2444
+ * __timestamp TEXT,
2445
+ * __previous_value TEXT
2446
+ * );
2447
+ * ```
2448
+ *
2449
+ * @example
2450
+ * ```sql
2451
+ * SELECT
2452
+ * todos.*
2453
+ * FROM
2454
+ * DIFF
2455
+ * JOIN todos ON DIFF.id = todos.id
2456
+ * --- The todo column names are extracted from json and are available as DIFF.name
2457
+ * WHERE DIFF.name = 'example'
2458
+ * ```
2459
+ */
2460
+ withExtractedDiff: <T = any>(query: string, params?: ReadonlyArray<Readonly<any>>) => Promise<T[]>;
2461
+ }
2462
+ /**
2463
+ * @experimental
2464
+ * Options for tracking changes to a table with {@link TriggerManager#trackTableDiff}.
2465
+ */
2466
+ interface TrackDiffOptions extends BaseCreateDiffTriggerOptions {
2467
+ /**
2468
+ * Handler for processing diff operations.
2469
+ * Automatically invoked once diff items are present.
2470
+ * Diff items are automatically cleared after the handler is invoked.
2471
+ */
2472
+ onChange: (context: TriggerDiffHandlerContext) => Promise<void>;
2473
+ /**
2474
+ * The minimum interval, in milliseconds, between {@link onChange} invocations.
2475
+ * @default {@link DEFAULT_WATCH_THROTTLE_MS}
2476
+ */
2477
+ throttleMs?: number;
2478
+ }
2479
+ /**
2480
+ * @experimental
2481
+ */
2482
+ interface TriggerManager {
2483
+ /**
2484
+ * @experimental
2485
+ * Creates a temporary trigger which tracks changes to a source table
2486
+ * and writes changes to a destination table.
2487
+ * The temporary destination table is created internally and will be dropped when the trigger is removed.
2488
+ * The temporary destination table is created with the structure:
2489
+ *
2490
+ * ```sql
2491
+ * CREATE TEMP TABLE ${destination} (
2492
+ * id TEXT,
2493
+ * operation TEXT,
2494
+ * timestamp TEXT
2495
+ * value TEXT,
2496
+ * previous_value TEXT
2497
+ * );
2498
+ * ```
2499
+ * The `value` column contains the JSON representation of the row's value at the change.
2500
+ *
2501
+ * For {@link DiffTriggerOperation#UPDATE} operations the `previous_value` column contains the previous value of the changed row
2502
+ * in a JSON format.
2503
+ *
2504
+ * NB: The triggers created by this method might be invalidated by {@link AbstractPowerSyncDatabase#updateSchema} calls.
2505
+ * These triggers should manually be dropped and recreated when updating the schema.
2506
+ *
2507
+ * @returns A callback to remove the trigger and drop the destination table.
2508
+ *
2509
+ * @example
2510
+ * ```javascript
2511
+ * const dispose = await database.triggers.createDiffTrigger({
2512
+ * source: 'lists',
2513
+ * destination: 'ps_temp_lists_diff',
2514
+ * columns: ['name'],
2515
+ * when: {
2516
+ * [DiffTriggerOperation.INSERT]: 'TRUE',
2517
+ * [DiffTriggerOperation.UPDATE]: 'TRUE',
2518
+ * [DiffTriggerOperation.DELETE]: 'TRUE'
2519
+ * }
2520
+ * });
2521
+ * ```
2522
+ */
2523
+ createDiffTrigger(options: CreateDiffTriggerOptions): Promise<TriggerRemoveCallback>;
2524
+ /**
2525
+ * @experimental
2526
+ * Tracks changes for a table. Triggering a provided handler on changes.
2527
+ * Uses {@link createDiffTrigger} internally to create a temporary destination table.
2528
+ *
2529
+ * @returns A callback to cleanup the trigger and stop tracking changes.
2530
+ *
2531
+ * NB: The triggers created by this method might be invalidated by {@link AbstractPowerSyncDatabase#updateSchema} calls.
2532
+ * These triggers should manually be dropped and recreated when updating the schema.
2533
+ *
2534
+ * @example
2535
+ * ```javascript
2536
+ * const dispose = database.triggers.trackTableDiff({
2537
+ * source: 'todos',
2538
+ * columns: ['list_id'],
2539
+ * when: {
2540
+ * [DiffTriggerOperation.INSERT]: sanitizeSQL`json_extract(NEW.data, '$.list_id') = ${sanitizeUUID(someIdVariable)}`
2541
+ * },
2542
+ * onChange: async (context) => {
2543
+ * // Fetches the todo records that were inserted during this diff
2544
+ * const newTodos = await context.getAll<Database['todos']>(`
2545
+ * SELECT
2546
+ * todos.*
2547
+ * FROM
2548
+ * DIFF
2549
+ * JOIN todos ON DIFF.id = todos.id
2550
+ * `);
2551
+ *
2552
+ * // Process newly created todos
2553
+ * },
2554
+ * hooks: {
2555
+ * beforeCreate: async (lockContext) => {
2556
+ * // This hook is executed inside the write lock before the trigger is created.
2557
+ * // It can be used to synchronize the current state of the table with processor logic.
2558
+ * // Any changes after this callback are guaranteed to trigger the `onChange` handler.
2559
+ *
2560
+ * // Read the current state of the todos table
2561
+ * const currentTodos = await lockContext.getAll<Database['todos']>(
2562
+ * `
2563
+ * SELECT
2564
+ * *
2565
+ * FROM
2566
+ * todos
2567
+ * WHERE
2568
+ * list_id = ?
2569
+ * `,
2570
+ * ['123']
2571
+ * );
2572
+ *
2573
+ * // Process existing todos
2574
+ * }
2575
+ * }
2576
+ * });
2577
+ * ```
2578
+ */
2579
+ trackTableDiff(options: TrackDiffOptions): Promise<TriggerRemoveCallback>;
2580
+ }
2581
+
2220
2582
  interface DisconnectAndClearOptions {
2221
2583
  /** When set to false, data in local-only tables is preserved. */
2222
2584
  clearLocal?: boolean;
@@ -2334,6 +2696,11 @@ declare abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncD
2334
2696
  protected _schema: Schema;
2335
2697
  private _database;
2336
2698
  protected runExclusiveMutex: Mutex;
2699
+ /**
2700
+ * @experimental
2701
+ * Allows creating SQLite triggers which can be used to track various operations on SQLite tables.
2702
+ */
2703
+ readonly triggers: TriggerManager;
2337
2704
  logger: ILogger;
2338
2705
  constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
2339
2706
  constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
@@ -2482,6 +2849,38 @@ declare abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncD
2482
2849
  * @returns A transaction of CRUD operations to upload, or null if there are none
2483
2850
  */
2484
2851
  getNextCrudTransaction(): Promise<CrudTransaction | null>;
2852
+ /**
2853
+ * Returns an async iterator of completed transactions with local writes against the database.
2854
+ *
2855
+ * This is typically used from the {@link PowerSyncBackendConnector.uploadData} callback. Each entry emitted by the
2856
+ * returned iterator is a full transaction containing all local writes made while that transaction was active.
2857
+ *
2858
+ * Unlike {@link getNextCrudTransaction}, which always returns the oldest transaction that hasn't been
2859
+ * {@link CrudTransaction.complete}d yet, this iterator can be used to receive multiple transactions. Calling
2860
+ * {@link CrudTransaction.complete} will mark that and all prior transactions emitted by the iterator as completed.
2861
+ *
2862
+ * This can be used to upload multiple transactions in a single batch, e.g with:
2863
+ *
2864
+ * ```JavaScript
2865
+ * let lastTransaction = null;
2866
+ * let batch = [];
2867
+ *
2868
+ * for await (const transaction of database.getCrudTransactions()) {
2869
+ * batch.push(...transaction.crud);
2870
+ * lastTransaction = transaction;
2871
+ *
2872
+ * if (batch.length > 10) {
2873
+ * break;
2874
+ * }
2875
+ * }
2876
+ * ```
2877
+ *
2878
+ * If there is no local data to upload, the async iterator complete without emitting any items.
2879
+ *
2880
+ * 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)
2881
+ * for React Native.
2882
+ */
2883
+ getCrudTransactions(): AsyncIterable<CrudTransaction, null>;
2485
2884
  /**
2486
2885
  * Get an unique client id for this database.
2487
2886
  *
@@ -2828,6 +3227,41 @@ declare class SqliteBucketStorage extends BaseObserver<BucketStorageListener> im
2828
3227
  static _subkeyMigrationKey: string;
2829
3228
  }
2830
3229
 
3230
+ /**
3231
+ * Helper function for sanitizing UUID input strings.
3232
+ * Typically used with {@link sanitizeSQL}.
3233
+ */
3234
+ declare function sanitizeUUID(uuid: string): string;
3235
+ /**
3236
+ * SQL string template function for {@link TrackDiffOptions#when} and {@link CreateDiffTriggerOptions#when}.
3237
+ *
3238
+ * This function performs basic string interpolation for SQLite WHEN clauses.
3239
+ *
3240
+ * **String placeholders:**
3241
+ * - All string values passed as placeholders are automatically wrapped in single quotes (`'`).
3242
+ * - Do not manually wrap placeholders in single quotes in your template string; the function will handle quoting and escaping for you.
3243
+ * - Any single quotes within the string value are escaped by doubling them (`''`), as required by SQL syntax.
3244
+ *
3245
+ * **Other types:**
3246
+ * - `null` and `undefined` are converted to SQL `NULL`.
3247
+ * - Objects are stringified using `JSON.stringify()` and wrapped in single quotes, with any single quotes inside the stringified value escaped.
3248
+ * - Numbers and other primitive types are inserted directly.
3249
+ *
3250
+ * **Usage example:**
3251
+ * ```typescript
3252
+ * const myID = "O'Reilly";
3253
+ * const clause = sanitizeSQL`New.id = ${myID}`;
3254
+ * // Result: "New.id = 'O''Reilly'"
3255
+ * ```
3256
+ *
3257
+ * Avoid manually quoting placeholders:
3258
+ * ```typescript
3259
+ * // Incorrect:
3260
+ * sanitizeSQL`New.id = '${myID}'` // Produces double quotes: New.id = ''O''Reilly''
3261
+ * ```
3262
+ */
3263
+ declare function sanitizeSQL(strings: TemplateStringsArray, ...values: any[]): string;
3264
+
2831
3265
  /**
2832
3266
  * Options for {@link GetAllQuery}.
2833
3267
  */
@@ -2930,5 +3364,5 @@ interface ParsedQuery {
2930
3364
  }
2931
3365
  declare const parseQuery: <T>(query: string | CompilableQuery<T>, parameters: any[]) => ParsedQuery;
2932
3366
 
2933
- export { AbortOperation, AbstractPowerSyncDatabase, AbstractPowerSyncDatabaseOpenFactory, AbstractQueryProcessor, AbstractRemote, AbstractStreamingSyncImplementation, ArrayComparator, BaseObserver, Column, ColumnType, ConnectionManager, ControlledExecutor, CrudBatch, CrudEntry, CrudTransaction, DEFAULT_CRUD_BATCH_LIMIT, DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_INDEX_COLUMN_OPTIONS, DEFAULT_INDEX_OPTIONS, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_POWERSYNC_CLOSE_OPTIONS, DEFAULT_POWERSYNC_DB_OPTIONS, DEFAULT_PRESSURE_LIMITS, DEFAULT_REMOTE_LOGGER, DEFAULT_REMOTE_OPTIONS, DEFAULT_RETRY_DELAY_MS, DEFAULT_ROW_COMPARATOR, DEFAULT_STREAMING_SYNC_OPTIONS, DEFAULT_STREAM_CONNECTION_OPTIONS, DEFAULT_SYNC_CLIENT_IMPLEMENTATION, DEFAULT_TABLE_OPTIONS, DEFAULT_WATCH_QUERY_OPTIONS, DEFAULT_WATCH_THROTTLE_MS, DataStream, DifferentialQueryProcessor, EMPTY_DIFFERENTIAL, FalsyComparator, FetchImplementationProvider, FetchStrategy, GetAllQuery, Index, IndexedColumn, InvalidSQLCharacters, LockType, LogLevel, MAX_AMOUNT_OF_COLUMNS, MAX_OP_ID, OnChangeQueryProcessor, OpType, OpTypeEnum, OplogEntry, PSInternalTable, PowerSyncControlCommand, RowUpdateType, Schema, SqliteBucketStorage, SyncClientImplementation, SyncDataBatch, SyncDataBucket, SyncProgress, SyncStatus, SyncStreamConnectionMethod, Table, TableV2, UpdateType, UploadQueueStats, WatchedQueryListenerEvent, column, compilableQueryWatch, createBaseLogger, createLogger, extractTableUpdates, isBatchedUpdateNotification, isContinueCheckpointRequest, isDBAdapter, isPowerSyncDatabaseOptionsWithSettings, isSQLOpenFactory, isSQLOpenOptions, isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData, isSyncNewCheckpointRequest, parseQuery, runOnSchemaChange };
2934
- export type { AbstractQueryProcessorOptions, AbstractRemoteOptions, AbstractStreamingSyncImplementationOptions, AdditionalConnectionOptions, ArrayComparatorOptions, ArrayQueryDefinition, BSONImplementation, BaseColumnType, BaseConnectionOptions, BaseListener, BaseObserverInterface, BasePowerSyncDatabaseOptions, BatchedUpdateNotification, BucketChecksum, BucketDescription, BucketOperationProgress, BucketRequest, BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, ChecksumCache, ColumnOptions, ColumnsType, CompilableQuery, CompilableQueryWatchHandler, CompiledQuery, ConnectionManagerListener, ConnectionManagerOptions, ConnectionManagerSyncImplementationResult, ContinueCheckpointRequest, ControlledExecutorOptions, CreateLoggerOptions, CrudRequest, CrudResponse, CrudUploadNotification, DBAdapter, DBAdapterListener, DBGetUtils, DBLockOptions, DataStreamCallback, DataStreamListener, DataStreamOptions, DifferentialQueryProcessorOptions, DifferentialWatchedQuery, DifferentialWatchedQueryComparator, DifferentialWatchedQueryListener, DifferentialWatchedQueryOptions, DifferentialWatchedQuerySettings, DisconnectAndClearOptions, Disposable, ExtractColumnValueType, FetchImplementation, GetAllQueryOptions, IndexColumnOptions, IndexOptions, IndexShorthand, InternalConnectionOptions, LinkQueryOptions, LockContext, LockOptions, MutableWatchedQueryState, OnChangeQueryProcessorOptions, OpId, OpTypeJSON, OplogEntryJSON, ParsedQuery, PowerSyncBackendConnector, PowerSyncCloseOptions, PowerSyncConnectionOptions, PowerSyncCredentials, PowerSyncDBListener, PowerSyncDatabaseOptions, PowerSyncDatabaseOptionsWithDBAdapter, PowerSyncDatabaseOptionsWithOpenFactory, PowerSyncDatabaseOptionsWithSettings, PowerSyncOpenFactoryOptions, ProgressWithOperations, Query, QueryParam, QueryResult, RemoteConnector, RequiredAdditionalConnectionOptions, RequiredPowerSyncConnectionOptions, RowType, SQLOnChangeOptions, SQLOpenFactory, SQLOpenOptions, SQLWatchOptions, SavedProgress, SchemaTableType, SocketSyncStreamOptions, StandardWatchedQuery, StandardWatchedQueryOptions, StreamingSyncCheckpoint, StreamingSyncCheckpointComplete, StreamingSyncCheckpointDiff, StreamingSyncCheckpointPartiallyComplete, StreamingSyncDataJSON, StreamingSyncImplementation, StreamingSyncImplementationListener, StreamingSyncKeepalive, StreamingSyncLine, StreamingSyncLineOrCrudUploadComplete, StreamingSyncRequest, StreamingSyncRequestParameterType, SyncDataBucketJSON, SyncDataFlowStatus, SyncLocalDatabaseResult, SyncNewCheckpointRequest, SyncPriorityStatus, SyncRequest, SyncResponse, SyncStatusOptions, SyncStreamOptions, TableOptions, TableUpdateOperation, TableV2Options, TrackPreviousOptions, Transaction, UpdateNotification, WatchCompatibleQuery, WatchExecuteOptions, WatchHandler, WatchOnChangeEvent, WatchOnChangeHandler, WatchedQuery, WatchedQueryComparator, WatchedQueryDifferential, WatchedQueryListener, WatchedQueryOptions, WatchedQueryRowDifferential, WatchedQuerySettings, WatchedQueryState };
3367
+ export { AbortOperation, AbstractPowerSyncDatabase, AbstractPowerSyncDatabaseOpenFactory, AbstractQueryProcessor, AbstractRemote, AbstractStreamingSyncImplementation, ArrayComparator, BaseObserver, Column, ColumnType, ConnectionManager, ControlledExecutor, CrudBatch, CrudEntry, CrudTransaction, DEFAULT_CRUD_BATCH_LIMIT, DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_INDEX_COLUMN_OPTIONS, DEFAULT_INDEX_OPTIONS, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_POWERSYNC_CLOSE_OPTIONS, DEFAULT_POWERSYNC_DB_OPTIONS, DEFAULT_PRESSURE_LIMITS, DEFAULT_REMOTE_LOGGER, DEFAULT_REMOTE_OPTIONS, DEFAULT_RETRY_DELAY_MS, DEFAULT_ROW_COMPARATOR, DEFAULT_STREAMING_SYNC_OPTIONS, DEFAULT_STREAM_CONNECTION_OPTIONS, DEFAULT_SYNC_CLIENT_IMPLEMENTATION, DEFAULT_TABLE_OPTIONS, DEFAULT_WATCH_QUERY_OPTIONS, DEFAULT_WATCH_THROTTLE_MS, DataStream, DiffTriggerOperation, DifferentialQueryProcessor, EMPTY_DIFFERENTIAL, FalsyComparator, FetchImplementationProvider, FetchStrategy, GetAllQuery, Index, IndexedColumn, InvalidSQLCharacters, LockType, LogLevel, MAX_AMOUNT_OF_COLUMNS, MAX_OP_ID, OnChangeQueryProcessor, OpType, OpTypeEnum, OplogEntry, PSInternalTable, PowerSyncControlCommand, RowUpdateType, Schema, SqliteBucketStorage, SyncClientImplementation, SyncDataBatch, SyncDataBucket, SyncProgress, SyncStatus, SyncStreamConnectionMethod, Table, TableV2, UpdateType, UploadQueueStats, WatchedQueryListenerEvent, column, compilableQueryWatch, createBaseLogger, createLogger, extractTableUpdates, isBatchedUpdateNotification, isContinueCheckpointRequest, isDBAdapter, isPowerSyncDatabaseOptionsWithSettings, isSQLOpenFactory, isSQLOpenOptions, isStreamingKeepalive, isStreamingSyncCheckpoint, isStreamingSyncCheckpointComplete, isStreamingSyncCheckpointDiff, isStreamingSyncCheckpointPartiallyComplete, isStreamingSyncData, isSyncNewCheckpointRequest, parseQuery, runOnSchemaChange, sanitizeSQL, sanitizeUUID };
3368
+ export type { AbstractQueryProcessorOptions, AbstractRemoteOptions, AbstractStreamingSyncImplementationOptions, AdditionalConnectionOptions, ArrayComparatorOptions, ArrayQueryDefinition, BSONImplementation, BaseColumnType, BaseConnectionOptions, BaseListener, BaseObserverInterface, BasePowerSyncDatabaseOptions, BaseTriggerDiffRecord, BatchedUpdateNotification, BucketChecksum, BucketDescription, BucketOperationProgress, BucketRequest, BucketState, BucketStorageAdapter, BucketStorageListener, Checkpoint, ChecksumCache, ColumnOptions, ColumnsType, CompilableQuery, CompilableQueryWatchHandler, CompiledQuery, ConnectionManagerListener, ConnectionManagerOptions, ConnectionManagerSyncImplementationResult, ContinueCheckpointRequest, ControlledExecutorOptions, CreateDiffTriggerOptions, CreateLoggerOptions, CrudRequest, CrudResponse, CrudUploadNotification, DBAdapter, DBAdapterListener, DBGetUtils, DBLockOptions, DataStreamCallback, DataStreamListener, DataStreamOptions, DifferentialQueryProcessorOptions, DifferentialWatchedQuery, DifferentialWatchedQueryComparator, DifferentialWatchedQueryListener, DifferentialWatchedQueryOptions, DifferentialWatchedQuerySettings, DisconnectAndClearOptions, Disposable, ExtractColumnValueType, ExtractedTriggerDiffRecord, FetchImplementation, GetAllQueryOptions, IndexColumnOptions, IndexOptions, IndexShorthand, InternalConnectionOptions, LinkQueryOptions, LockContext, LockOptions, MutableWatchedQueryState, OnChangeQueryProcessorOptions, OpId, OpTypeJSON, OplogEntryJSON, ParsedQuery, PowerSyncBackendConnector, PowerSyncCloseOptions, PowerSyncConnectionOptions, PowerSyncCredentials, PowerSyncDBListener, PowerSyncDatabaseOptions, PowerSyncDatabaseOptionsWithDBAdapter, PowerSyncDatabaseOptionsWithOpenFactory, PowerSyncDatabaseOptionsWithSettings, PowerSyncOpenFactoryOptions, ProgressWithOperations, Query, QueryParam, QueryResult, RemoteConnector, RequiredAdditionalConnectionOptions, RequiredPowerSyncConnectionOptions, RowType, SQLOnChangeOptions, SQLOpenFactory, SQLOpenOptions, SQLWatchOptions, SavedProgress, SchemaTableType, SocketSyncStreamOptions, StandardWatchedQuery, StandardWatchedQueryOptions, StreamingSyncCheckpoint, StreamingSyncCheckpointComplete, StreamingSyncCheckpointDiff, StreamingSyncCheckpointPartiallyComplete, StreamingSyncDataJSON, StreamingSyncImplementation, StreamingSyncImplementationListener, StreamingSyncKeepalive, StreamingSyncLine, StreamingSyncLineOrCrudUploadComplete, StreamingSyncRequest, StreamingSyncRequestParameterType, SyncDataBucketJSON, SyncDataFlowStatus, SyncLocalDatabaseResult, SyncNewCheckpointRequest, SyncPriorityStatus, SyncRequest, SyncResponse, SyncStatusOptions, SyncStreamOptions, TableOptions, TableUpdateOperation, TableV2Options, TrackDiffOptions, TrackPreviousOptions, Transaction, TriggerCreationHooks, TriggerDiffDeleteRecord, TriggerDiffHandlerContext, TriggerDiffInsertRecord, TriggerDiffRecord, TriggerDiffUpdateRecord, TriggerManager, TriggerRemoveCallback, UpdateNotification, WatchCompatibleQuery, WatchExecuteOptions, WatchHandler, WatchOnChangeEvent, WatchOnChangeHandler, WatchedQuery, WatchedQueryComparator, WatchedQueryDifferential, WatchedQueryListener, WatchedQueryOptions, WatchedQueryRowDifferential, WatchedQuerySettings, WatchedQueryState };
@@ -13,6 +13,7 @@ import { BucketStorageAdapter } from './sync/bucket/BucketStorageAdapter.js';
13
13
  import { CrudBatch } from './sync/bucket/CrudBatch.js';
14
14
  import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
15
15
  import { StreamingSyncImplementation, StreamingSyncImplementationListener, type AdditionalConnectionOptions, type PowerSyncConnectionOptions, type RequiredAdditionalConnectionOptions } from './sync/stream/AbstractStreamingSyncImplementation.js';
16
+ import { TriggerManager } from './triggers/TriggerManager.js';
16
17
  import { WatchCompatibleQuery } from './watched/WatchedQuery.js';
17
18
  import { WatchedQueryComparator } from './watched/processors/comparators.js';
18
19
  export interface DisconnectAndClearOptions {
@@ -132,6 +133,11 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
132
133
  protected _schema: Schema;
133
134
  private _database;
134
135
  protected runExclusiveMutex: Mutex;
136
+ /**
137
+ * @experimental
138
+ * Allows creating SQLite triggers which can be used to track various operations on SQLite tables.
139
+ */
140
+ readonly triggers: TriggerManager;
135
141
  logger: ILogger;
136
142
  constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
137
143
  constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
@@ -280,6 +286,38 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
280
286
  * @returns A transaction of CRUD operations to upload, or null if there are none
281
287
  */
282
288
  getNextCrudTransaction(): Promise<CrudTransaction | null>;
289
+ /**
290
+ * Returns an async iterator of completed transactions with local writes against the database.
291
+ *
292
+ * This is typically used from the {@link PowerSyncBackendConnector.uploadData} callback. Each entry emitted by the
293
+ * returned iterator is a full transaction containing all local writes made while that transaction was active.
294
+ *
295
+ * Unlike {@link getNextCrudTransaction}, which always returns the oldest transaction that hasn't been
296
+ * {@link CrudTransaction.complete}d yet, this iterator can be used to receive multiple transactions. Calling
297
+ * {@link CrudTransaction.complete} will mark that and all prior transactions emitted by the iterator as completed.
298
+ *
299
+ * This can be used to upload multiple transactions in a single batch, e.g with:
300
+ *
301
+ * ```JavaScript
302
+ * let lastTransaction = null;
303
+ * let batch = [];
304
+ *
305
+ * for await (const transaction of database.getCrudTransactions()) {
306
+ * batch.push(...transaction.crud);
307
+ * lastTransaction = transaction;
308
+ *
309
+ * if (batch.length > 10) {
310
+ * break;
311
+ * }
312
+ * }
313
+ * ```
314
+ *
315
+ * If there is no local data to upload, the async iterator complete without emitting any items.
316
+ *
317
+ * 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)
318
+ * for React Native.
319
+ */
320
+ getCrudTransactions(): AsyncIterable<CrudTransaction, null>;
283
321
  /**
284
322
  * Get an unique client id for this database.
285
323
  *
@@ -7,7 +7,7 @@ import { SyncStatus } from '../db/crud/SyncStatus.js';
7
7
  import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
8
8
  import { BaseObserver } from '../utils/BaseObserver.js';
9
9
  import { ControlledExecutor } from '../utils/ControlledExecutor.js';
10
- import { throttleTrailing } from '../utils/async.js';
10
+ import { symbolAsyncIterator, throttleTrailing } from '../utils/async.js';
11
11
  import { ConnectionManager } from './ConnectionManager.js';
12
12
  import { CustomQuery } from './CustomQuery.js';
13
13
  import { isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
@@ -16,6 +16,7 @@ import { CrudBatch } from './sync/bucket/CrudBatch.js';
16
16
  import { CrudEntry } from './sync/bucket/CrudEntry.js';
17
17
  import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
18
18
  import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_RETRY_DELAY_MS } from './sync/stream/AbstractStreamingSyncImplementation.js';
19
+ import { TriggerManagerImpl } from './triggers/TriggerManagerImpl.js';
19
20
  import { DEFAULT_WATCH_THROTTLE_MS } from './watched/WatchedQuery.js';
20
21
  import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
21
22
  const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
@@ -64,6 +65,11 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
64
65
  _schema;
65
66
  _database;
66
67
  runExclusiveMutex;
68
+ /**
69
+ * @experimental
70
+ * Allows creating SQLite triggers which can be used to track various operations on SQLite tables.
71
+ */
72
+ triggers;
67
73
  logger;
68
74
  constructor(options) {
69
75
  super();
@@ -118,6 +124,10 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
118
124
  logger: this.logger
119
125
  });
120
126
  this._isReadyPromise = this.initialize();
127
+ this.triggers = new TriggerManagerImpl({
128
+ db: this,
129
+ schema: this.schema
130
+ });
121
131
  }
122
132
  /**
123
133
  * Schema used for the local database.
@@ -426,23 +436,72 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
426
436
  * @returns A transaction of CRUD operations to upload, or null if there are none
427
437
  */
428
438
  async getNextCrudTransaction() {
429
- return await this.readTransaction(async (tx) => {
430
- const first = await tx.getOptional(`SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} ORDER BY id ASC LIMIT 1`);
431
- if (!first) {
432
- return null;
433
- }
434
- const txId = first.tx_id;
435
- let all;
436
- if (!txId) {
437
- all = [CrudEntry.fromRow(first)];
438
- }
439
- else {
440
- const result = await tx.getAll(`SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} WHERE tx_id = ? ORDER BY id ASC`, [txId]);
441
- all = result.map((row) => CrudEntry.fromRow(row));
439
+ const iterator = this.getCrudTransactions()[symbolAsyncIterator]();
440
+ return (await iterator.next()).value;
441
+ }
442
+ /**
443
+ * Returns an async iterator of completed transactions with local writes against the database.
444
+ *
445
+ * This is typically used from the {@link PowerSyncBackendConnector.uploadData} callback. Each entry emitted by the
446
+ * returned iterator is a full transaction containing all local writes made while that transaction was active.
447
+ *
448
+ * Unlike {@link getNextCrudTransaction}, which always returns the oldest transaction that hasn't been
449
+ * {@link CrudTransaction.complete}d yet, this iterator can be used to receive multiple transactions. Calling
450
+ * {@link CrudTransaction.complete} will mark that and all prior transactions emitted by the iterator as completed.
451
+ *
452
+ * This can be used to upload multiple transactions in a single batch, e.g with:
453
+ *
454
+ * ```JavaScript
455
+ * let lastTransaction = null;
456
+ * let batch = [];
457
+ *
458
+ * for await (const transaction of database.getCrudTransactions()) {
459
+ * batch.push(...transaction.crud);
460
+ * lastTransaction = transaction;
461
+ *
462
+ * if (batch.length > 10) {
463
+ * break;
464
+ * }
465
+ * }
466
+ * ```
467
+ *
468
+ * If there is no local data to upload, the async iterator complete without emitting any items.
469
+ *
470
+ * 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)
471
+ * for React Native.
472
+ */
473
+ getCrudTransactions() {
474
+ return {
475
+ [symbolAsyncIterator]: () => {
476
+ let lastCrudItemId = -1;
477
+ const sql = `
478
+ WITH RECURSIVE crud_entries AS (
479
+ SELECT id, tx_id, data FROM ps_crud WHERE id = (SELECT min(id) FROM ps_crud WHERE id > ?)
480
+ UNION ALL
481
+ SELECT ps_crud.id, ps_crud.tx_id, ps_crud.data FROM ps_crud
482
+ INNER JOIN crud_entries ON crud_entries.id + 1 = rowid
483
+ WHERE crud_entries.tx_id = ps_crud.tx_id
484
+ )
485
+ SELECT * FROM crud_entries;
486
+ `;
487
+ return {
488
+ next: async () => {
489
+ const nextTransaction = await this.database.getAll(sql, [lastCrudItemId]);
490
+ if (nextTransaction.length == 0) {
491
+ return { done: true, value: null };
492
+ }
493
+ const items = nextTransaction.map((row) => CrudEntry.fromRow(row));
494
+ const last = items[items.length - 1];
495
+ const txId = last.transactionId;
496
+ lastCrudItemId = last.clientId;
497
+ return {
498
+ done: false,
499
+ value: new CrudTransaction(items, async (writeCheckpoint) => this.handleCrudCheckpoint(last.clientId, writeCheckpoint), txId)
500
+ };
501
+ }
502
+ };
442
503
  }
443
- const last = all[all.length - 1];
444
- return new CrudTransaction(all, async (writeCheckpoint) => this.handleCrudCheckpoint(last.clientId, writeCheckpoint), txId);
445
- });
504
+ };
446
505
  }
447
506
  /**
448
507
  * Get an unique client id for this database.
@@ -382,6 +382,9 @@ export class AbstractRemote {
382
382
  * Aborting the active fetch request while it is being consumed seems to throw
383
383
  * an unhandled exception on the window level.
384
384
  */
385
+ if (abortSignal?.aborted) {
386
+ throw new AbortOperation('Abort request received before making postStreamRaw request');
387
+ }
385
388
  const controller = new AbortController();
386
389
  let requestResolved = false;
387
390
  abortSignal?.addEventListener('abort', () => {