@powersync/common 1.33.2 → 1.35.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.
Files changed (42) hide show
  1. package/dist/bundle.cjs +5 -5
  2. package/dist/bundle.mjs +3 -3
  3. package/lib/client/AbstractPowerSyncDatabase.d.ts +58 -13
  4. package/lib/client/AbstractPowerSyncDatabase.js +107 -50
  5. package/lib/client/ConnectionManager.d.ts +4 -4
  6. package/lib/client/CustomQuery.d.ts +22 -0
  7. package/lib/client/CustomQuery.js +42 -0
  8. package/lib/client/Query.d.ts +97 -0
  9. package/lib/client/Query.js +1 -0
  10. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
  11. package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -3
  12. package/lib/client/sync/bucket/SqliteBucketStorage.js +15 -17
  13. package/lib/client/sync/stream/AbstractRemote.d.ts +1 -10
  14. package/lib/client/sync/stream/AbstractRemote.js +31 -35
  15. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +13 -8
  16. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +112 -82
  17. package/lib/client/sync/stream/streaming-sync-types.d.ts +4 -0
  18. package/lib/client/watched/GetAllQuery.d.ts +32 -0
  19. package/lib/client/watched/GetAllQuery.js +24 -0
  20. package/lib/client/watched/WatchedQuery.d.ts +98 -0
  21. package/lib/client/watched/WatchedQuery.js +12 -0
  22. package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
  23. package/lib/client/watched/processors/AbstractQueryProcessor.js +135 -0
  24. package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +121 -0
  25. package/lib/client/watched/processors/DifferentialQueryProcessor.js +166 -0
  26. package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
  27. package/lib/client/watched/processors/OnChangeQueryProcessor.js +76 -0
  28. package/lib/client/watched/processors/comparators.d.ts +30 -0
  29. package/lib/client/watched/processors/comparators.js +34 -0
  30. package/lib/db/schema/RawTable.d.ts +61 -0
  31. package/lib/db/schema/RawTable.js +32 -0
  32. package/lib/db/schema/Schema.d.ts +14 -0
  33. package/lib/db/schema/Schema.js +20 -1
  34. package/lib/index.d.ts +8 -0
  35. package/lib/index.js +8 -0
  36. package/lib/utils/BaseObserver.d.ts +3 -4
  37. package/lib/utils/BaseObserver.js +3 -0
  38. package/lib/utils/MetaBaseObserver.d.ts +29 -0
  39. package/lib/utils/MetaBaseObserver.js +50 -0
  40. package/lib/utils/async.d.ts +0 -1
  41. package/lib/utils/async.js +0 -10
  42. package/package.json +1 -1
@@ -1,17 +1,20 @@
1
1
  import { Mutex } from 'async-mutex';
2
- import Logger, { ILogger } from 'js-logger';
2
+ import { ILogger } from 'js-logger';
3
3
  import { DBAdapter, QueryResult, Transaction } from '../db/DBAdapter.js';
4
4
  import { SyncStatus } from '../db/crud/SyncStatus.js';
5
5
  import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
6
6
  import { Schema } from '../db/schema/Schema.js';
7
7
  import { BaseObserver } from '../utils/BaseObserver.js';
8
8
  import { ConnectionManager } from './ConnectionManager.js';
9
+ import { ArrayQueryDefinition, Query } from './Query.js';
9
10
  import { SQLOpenFactory, SQLOpenOptions } from './SQLOpenFactory.js';
10
11
  import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
11
12
  import { BucketStorageAdapter } from './sync/bucket/BucketStorageAdapter.js';
12
13
  import { CrudBatch } from './sync/bucket/CrudBatch.js';
13
14
  import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
14
15
  import { StreamingSyncImplementation, StreamingSyncImplementationListener, type AdditionalConnectionOptions, type PowerSyncConnectionOptions, type RequiredAdditionalConnectionOptions } from './sync/stream/AbstractStreamingSyncImplementation.js';
16
+ import { WatchCompatibleQuery } from './watched/WatchedQuery.js';
17
+ import { WatchedQueryComparator } from './watched/processors/comparators.js';
15
18
  export interface DisconnectAndClearOptions {
16
19
  /** When set to false, data in local-only tables is preserved. */
17
20
  clearLocal?: boolean;
@@ -44,7 +47,7 @@ export interface PowerSyncDatabaseOptionsWithOpenFactory extends BasePowerSyncDa
44
47
  export interface PowerSyncDatabaseOptionsWithSettings extends BasePowerSyncDatabaseOptions {
45
48
  database: SQLOpenOptions;
46
49
  }
47
- export interface SQLWatchOptions {
50
+ export interface SQLOnChangeOptions {
48
51
  signal?: AbortSignal;
49
52
  tables?: string[];
50
53
  /** The minimum interval between queries. */
@@ -56,6 +59,17 @@ export interface SQLWatchOptions {
56
59
  * by not removing PowerSync table name prefixes
57
60
  */
58
61
  rawTableNames?: boolean;
62
+ /**
63
+ * Emits an empty result set immediately
64
+ */
65
+ triggerImmediate?: boolean;
66
+ }
67
+ export interface SQLWatchOptions extends SQLOnChangeOptions {
68
+ /**
69
+ * Optional comparator which will be used to compare the results of the query.
70
+ * The watched query will only yield results if the comparator returns false.
71
+ */
72
+ comparator?: WatchedQueryComparator<QueryResult>;
59
73
  }
60
74
  export interface WatchOnChangeEvent {
61
75
  changedTables: string[];
@@ -71,6 +85,8 @@ export interface WatchOnChangeHandler {
71
85
  export interface PowerSyncDBListener extends StreamingSyncImplementationListener {
72
86
  initialized: () => void;
73
87
  schemaChanged: (schema: Schema) => void;
88
+ closing: () => Promise<void> | void;
89
+ closed: () => Promise<void> | void;
74
90
  }
75
91
  export interface PowerSyncCloseOptions {
76
92
  /**
@@ -81,10 +97,8 @@ export interface PowerSyncCloseOptions {
81
97
  disconnect?: boolean;
82
98
  }
83
99
  export declare const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions;
84
- export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
85
100
  export declare const DEFAULT_POWERSYNC_DB_OPTIONS: {
86
101
  retryDelayMs: number;
87
- logger: Logger.ILogger;
88
102
  crudUploadThrottleMs: number;
89
103
  };
90
104
  export declare const DEFAULT_CRUD_BATCH_LIMIT = 100;
@@ -101,11 +115,6 @@ export declare const DEFAULT_LOCK_TIMEOUT_MS = 120000;
101
115
  export declare const isPowerSyncDatabaseOptionsWithSettings: (test: any) => test is PowerSyncDatabaseOptionsWithSettings;
102
116
  export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDBListener> {
103
117
  protected options: PowerSyncDatabaseOptions;
104
- /**
105
- * Transactions should be queued in the DBAdapter, but we also want to prevent
106
- * calls to `.execute` while an async transaction is running.
107
- */
108
- protected static transactionMutex: Mutex;
109
118
  /**
110
119
  * Returns true if the connection is closed.
111
120
  */
@@ -123,6 +132,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
123
132
  protected _schema: Schema;
124
133
  private _database;
125
134
  protected runExclusiveMutex: Mutex;
135
+ logger: ILogger;
126
136
  constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
127
137
  constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
128
138
  constructor(options: PowerSyncDatabaseOptionsWithSettings);
@@ -185,7 +195,6 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
185
195
  * Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
186
196
  */
187
197
  updateSchema(schema: Schema): Promise<void>;
188
- get logger(): Logger.ILogger;
189
198
  /**
190
199
  * Wait for initialization to complete.
191
200
  * While initializing is automatic, this helps to catch and report initialization errors.
@@ -395,6 +404,42 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
395
404
  * ```
396
405
  */
397
406
  watch(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void;
407
+ /**
408
+ * Allows defining a query which can be used to build a {@link WatchedQuery}.
409
+ * The defined query will be executed with {@link AbstractPowerSyncDatabase#getAll}.
410
+ * An optional mapper function can be provided to transform the results.
411
+ *
412
+ * @example
413
+ * ```javascript
414
+ * const watchedTodos = powersync.query({
415
+ * sql: `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`,
416
+ * parameters: [],
417
+ * mapper: (row) => ({
418
+ * ...row,
419
+ * created_at: new Date(row.created_at as string)
420
+ * })
421
+ * })
422
+ * .watch()
423
+ * // OR use .differentialWatch() for fine-grained watches.
424
+ * ```
425
+ */
426
+ query<RowType>(query: ArrayQueryDefinition<RowType>): Query<RowType>;
427
+ /**
428
+ * Allows building a {@link WatchedQuery} using an existing {@link WatchCompatibleQuery}.
429
+ * The watched query will use the provided {@link WatchCompatibleQuery.execute} method to query results.
430
+ *
431
+ * @example
432
+ * ```javascript
433
+ *
434
+ * // Potentially a query from an ORM like Drizzle
435
+ * const query = db.select().from(lists);
436
+ *
437
+ * const watchedTodos = powersync.customQuery(query)
438
+ * .watch()
439
+ * // OR use .differentialWatch() for fine-grained watches.
440
+ * ```
441
+ */
442
+ customQuery<RowType>(query: WatchCompatibleQuery<RowType[]>): Query<RowType>;
398
443
  /**
399
444
  * Execute a read query every time the source tables are modified.
400
445
  * Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
@@ -443,7 +488,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
443
488
  * }
444
489
  * ```
445
490
  */
446
- onChange(options?: SQLWatchOptions): AsyncIterable<WatchOnChangeEvent>;
491
+ onChange(options?: SQLOnChangeOptions): AsyncIterable<WatchOnChangeEvent>;
447
492
  /**
448
493
  * See {@link onChangeWithCallback}.
449
494
  *
@@ -458,7 +503,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
458
503
  * }
459
504
  * ```
460
505
  */
461
- onChange(handler?: WatchOnChangeHandler, options?: SQLWatchOptions): () => void;
506
+ onChange(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void;
462
507
  /**
463
508
  * Invoke the provided callback on any changes to any of the specified tables.
464
509
  *
@@ -471,7 +516,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
471
516
  * @param options Options for configuring watch behavior
472
517
  * @returns A dispose function to stop watching for changes
473
518
  */
474
- onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLWatchOptions): () => void;
519
+ onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void;
475
520
  /**
476
521
  * Create a Stream of changes to any of the specified tables.
477
522
  *
@@ -8,15 +8,16 @@ import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
8
8
  import { BaseObserver } from '../utils/BaseObserver.js';
9
9
  import { ControlledExecutor } from '../utils/ControlledExecutor.js';
10
10
  import { throttleTrailing } from '../utils/async.js';
11
- import { mutexRunExclusive } from '../utils/mutex.js';
12
11
  import { ConnectionManager } from './ConnectionManager.js';
12
+ import { CustomQuery } from './CustomQuery.js';
13
13
  import { isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
14
- import { runOnSchemaChange } from './runOnSchemaChange.js';
15
14
  import { PSInternalTable } from './sync/bucket/BucketStorageAdapter.js';
16
15
  import { CrudBatch } from './sync/bucket/CrudBatch.js';
17
16
  import { CrudEntry } from './sync/bucket/CrudEntry.js';
18
17
  import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
19
18
  import { DEFAULT_CRUD_UPLOAD_THROTTLE_MS, DEFAULT_RETRY_DELAY_MS } from './sync/stream/AbstractStreamingSyncImplementation.js';
19
+ import { DEFAULT_WATCH_THROTTLE_MS } from './watched/WatchedQuery.js';
20
+ import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
20
21
  const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
21
22
  const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
22
23
  clearLocal: true
@@ -24,10 +25,8 @@ const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
24
25
  export const DEFAULT_POWERSYNC_CLOSE_OPTIONS = {
25
26
  disconnect: true
26
27
  };
27
- export const DEFAULT_WATCH_THROTTLE_MS = 30;
28
28
  export const DEFAULT_POWERSYNC_DB_OPTIONS = {
29
29
  retryDelayMs: 5000,
30
- logger: Logger.get('PowerSyncDatabase'),
31
30
  crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
32
31
  };
33
32
  export const DEFAULT_CRUD_BATCH_LIMIT = 100;
@@ -46,11 +45,6 @@ export const isPowerSyncDatabaseOptionsWithSettings = (test) => {
46
45
  };
47
46
  export class AbstractPowerSyncDatabase extends BaseObserver {
48
47
  options;
49
- /**
50
- * Transactions should be queued in the DBAdapter, but we also want to prevent
51
- * calls to `.execute` while an async transaction is running.
52
- */
53
- static transactionMutex = new Mutex();
54
48
  /**
55
49
  * Returns true if the connection is closed.
56
50
  */
@@ -70,6 +64,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
70
64
  _schema;
71
65
  _database;
72
66
  runExclusiveMutex;
67
+ logger;
73
68
  constructor(options) {
74
69
  super();
75
70
  this.options = options;
@@ -89,6 +84,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
89
84
  else {
90
85
  throw new Error('The provided `database` option is invalid.');
91
86
  }
87
+ this.logger = options.logger ?? Logger.get(`PowerSyncDatabase[${this._database.name}]`);
92
88
  this.bucketStorageAdapter = this.generateBucketStorageAdapter();
93
89
  this.closed = false;
94
90
  this.currentStatus = new SyncStatus({});
@@ -268,16 +264,13 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
268
264
  schema.validate();
269
265
  }
270
266
  catch (ex) {
271
- this.options.logger?.warn('Schema validation failed. Unexpected behaviour could occur', ex);
267
+ this.logger.warn('Schema validation failed. Unexpected behaviour could occur', ex);
272
268
  }
273
269
  this._schema = schema;
274
270
  await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
275
271
  await this.database.refreshSchema();
276
272
  this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
277
273
  }
278
- get logger() {
279
- return this.options.logger;
280
- }
281
274
  /**
282
275
  * Wait for initialization to complete.
283
276
  * While initializing is automatic, this helps to catch and report initialization errors.
@@ -304,7 +297,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
304
297
  * Connects to stream of events from the PowerSync instance.
305
298
  */
306
299
  async connect(connector, options) {
307
- return this.connectionManager.connect(connector, options);
300
+ const resolvedOptions = options ?? {};
301
+ resolvedOptions.serializedSchema = this.schema.toJSON();
302
+ return this.connectionManager.connect(connector, resolvedOptions);
308
303
  }
309
304
  /**
310
305
  * Close the sync connection.
@@ -347,6 +342,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
347
342
  if (this.closed) {
348
343
  return;
349
344
  }
345
+ await this.iterateAsyncListeners(async (cb) => cb.closing?.());
350
346
  const { disconnect } = options;
351
347
  if (disconnect) {
352
348
  await this.disconnect();
@@ -354,6 +350,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
354
350
  await this.connectionManager.close();
355
351
  await this.database.close();
356
352
  this.closed = true;
353
+ await this.iterateAsyncListeners(async (cb) => cb.closed?.());
357
354
  }
358
355
  /**
359
356
  * Get upload queue size estimate and count.
@@ -477,8 +474,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
477
474
  * @returns The query result as an object with structured key-value pairs
478
475
  */
479
476
  async execute(sql, parameters) {
480
- await this.waitForReady();
481
- return this.database.execute(sql, parameters);
477
+ return this.writeLock((tx) => tx.execute(sql, parameters));
482
478
  }
483
479
  /**
484
480
  * Execute a SQL write (INSERT/UPDATE/DELETE) query directly on the database without any PowerSync processing.
@@ -546,7 +542,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
546
542
  */
547
543
  async readLock(callback) {
548
544
  await this.waitForReady();
549
- return mutexRunExclusive(AbstractPowerSyncDatabase.transactionMutex, () => callback(this.database));
545
+ return this.database.readLock(callback);
550
546
  }
551
547
  /**
552
548
  * Takes a global lock, without starting a transaction.
@@ -554,10 +550,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
554
550
  */
555
551
  async writeLock(callback) {
556
552
  await this.waitForReady();
557
- return mutexRunExclusive(AbstractPowerSyncDatabase.transactionMutex, async () => {
558
- const res = await callback(this.database);
559
- return res;
560
- });
553
+ return this.database.writeLock(callback);
561
554
  }
562
555
  /**
563
556
  * Open a read-only transaction.
@@ -604,6 +597,60 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
604
597
  const options = handlerOrOptions;
605
598
  return this.watchWithAsyncGenerator(sql, parameters, options);
606
599
  }
600
+ /**
601
+ * Allows defining a query which can be used to build a {@link WatchedQuery}.
602
+ * The defined query will be executed with {@link AbstractPowerSyncDatabase#getAll}.
603
+ * An optional mapper function can be provided to transform the results.
604
+ *
605
+ * @example
606
+ * ```javascript
607
+ * const watchedTodos = powersync.query({
608
+ * sql: `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`,
609
+ * parameters: [],
610
+ * mapper: (row) => ({
611
+ * ...row,
612
+ * created_at: new Date(row.created_at as string)
613
+ * })
614
+ * })
615
+ * .watch()
616
+ * // OR use .differentialWatch() for fine-grained watches.
617
+ * ```
618
+ */
619
+ query(query) {
620
+ const { sql, parameters = [], mapper } = query;
621
+ const compatibleQuery = {
622
+ compile: () => ({
623
+ sql,
624
+ parameters
625
+ }),
626
+ execute: async ({ sql, parameters }) => {
627
+ const result = await this.getAll(sql, parameters);
628
+ return mapper ? result.map(mapper) : result;
629
+ }
630
+ };
631
+ return this.customQuery(compatibleQuery);
632
+ }
633
+ /**
634
+ * Allows building a {@link WatchedQuery} using an existing {@link WatchCompatibleQuery}.
635
+ * The watched query will use the provided {@link WatchCompatibleQuery.execute} method to query results.
636
+ *
637
+ * @example
638
+ * ```javascript
639
+ *
640
+ * // Potentially a query from an ORM like Drizzle
641
+ * const query = db.select().from(lists);
642
+ *
643
+ * const watchedTodos = powersync.customQuery(query)
644
+ * .watch()
645
+ * // OR use .differentialWatch() for fine-grained watches.
646
+ * ```
647
+ */
648
+ customQuery(query) {
649
+ return new CustomQuery({
650
+ db: this,
651
+ query
652
+ });
653
+ }
607
654
  /**
608
655
  * Execute a read query every time the source tables are modified.
609
656
  * Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
@@ -617,39 +664,46 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
617
664
  * @param options Options for configuring watch behavior
618
665
  */
619
666
  watchWithCallback(sql, parameters, handler, options) {
620
- const { onResult, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
667
+ const { onResult, onError = (e) => this.logger.error(e) } = handler ?? {};
621
668
  if (!onResult) {
622
669
  throw new Error('onResult is required');
623
670
  }
624
- const watchQuery = async (abortSignal) => {
625
- try {
626
- const resolvedTables = await this.resolveTables(sql, parameters, options);
627
- // Fetch initial data
628
- const result = await this.executeReadOnly(sql, parameters);
629
- onResult(result);
630
- this.onChangeWithCallback({
631
- onChange: async () => {
632
- try {
633
- const result = await this.executeReadOnly(sql, parameters);
634
- onResult(result);
635
- }
636
- catch (error) {
637
- onError?.(error);
638
- }
639
- },
640
- onError
641
- }, {
642
- ...(options ?? {}),
643
- tables: resolvedTables,
644
- // Override the abort signal since we intercept it
645
- signal: abortSignal
646
- });
671
+ const { comparator } = options ?? {};
672
+ // This API yields a QueryResult type.
673
+ // This is not a standard Array result, which makes it incompatible with the .query API.
674
+ const watchedQuery = new OnChangeQueryProcessor({
675
+ db: this,
676
+ comparator,
677
+ placeholderData: null,
678
+ watchOptions: {
679
+ query: {
680
+ compile: () => ({
681
+ sql: sql,
682
+ parameters: parameters ?? []
683
+ }),
684
+ execute: () => this.executeReadOnly(sql, parameters)
685
+ },
686
+ reportFetching: false,
687
+ throttleMs: options?.throttleMs ?? DEFAULT_WATCH_THROTTLE_MS,
688
+ triggerOnTables: options?.tables
647
689
  }
648
- catch (error) {
649
- onError?.(error);
690
+ });
691
+ const dispose = watchedQuery.registerListener({
692
+ onData: (data) => {
693
+ if (!data) {
694
+ // This should not happen. We only use null for the initial data.
695
+ return;
696
+ }
697
+ onResult(data);
698
+ },
699
+ onError: (error) => {
700
+ onError(error);
650
701
  }
651
- };
652
- runOnSchemaChange(watchQuery, this, options);
702
+ });
703
+ options?.signal?.addEventListener('abort', () => {
704
+ dispose();
705
+ watchedQuery.close();
706
+ });
653
707
  }
654
708
  /**
655
709
  * Execute a read query every time the source tables are modified.
@@ -723,7 +777,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
723
777
  * @returns A dispose function to stop watching for changes
724
778
  */
725
779
  onChangeWithCallback(handler, options) {
726
- const { onChange, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
780
+ const { onChange, onError = (e) => this.logger.error(e) } = handler ?? {};
727
781
  if (!onChange) {
728
782
  throw new Error('onChange is required');
729
783
  }
@@ -739,6 +793,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
739
793
  return;
740
794
  executor.schedule({ changedTables: intersection });
741
795
  }), throttleMs);
796
+ if (options?.triggerImmediate) {
797
+ executor.schedule({ changedTables: [] });
798
+ }
742
799
  const dispose = this.database.registerListener({
743
800
  tablesUpdated: async (update) => {
744
801
  try {
@@ -1,7 +1,7 @@
1
1
  import { ILogger } from 'js-logger';
2
2
  import { BaseListener, BaseObserver } from '../utils/BaseObserver.js';
3
3
  import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
4
- import { PowerSyncConnectionOptions, StreamingSyncImplementation } from './sync/stream/AbstractStreamingSyncImplementation.js';
4
+ import { InternalConnectionOptions, StreamingSyncImplementation } from './sync/stream/AbstractStreamingSyncImplementation.js';
5
5
  /**
6
6
  * @internal
7
7
  */
@@ -17,12 +17,12 @@ export interface ConnectionManagerSyncImplementationResult {
17
17
  * @internal
18
18
  */
19
19
  export interface ConnectionManagerOptions {
20
- createSyncImplementation(connector: PowerSyncBackendConnector, options: PowerSyncConnectionOptions): Promise<ConnectionManagerSyncImplementationResult>;
20
+ createSyncImplementation(connector: PowerSyncBackendConnector, options: InternalConnectionOptions): Promise<ConnectionManagerSyncImplementationResult>;
21
21
  logger: ILogger;
22
22
  }
23
23
  type StoredConnectionOptions = {
24
24
  connector: PowerSyncBackendConnector;
25
- options: PowerSyncConnectionOptions;
25
+ options: InternalConnectionOptions;
26
26
  };
27
27
  /**
28
28
  * @internal
@@ -66,7 +66,7 @@ export declare class ConnectionManager extends BaseObserver<ConnectionManagerLis
66
66
  constructor(options: ConnectionManagerOptions);
67
67
  get logger(): ILogger;
68
68
  close(): Promise<void>;
69
- connect(connector: PowerSyncBackendConnector, options?: PowerSyncConnectionOptions): Promise<void>;
69
+ connect(connector: PowerSyncBackendConnector, options: InternalConnectionOptions): Promise<void>;
70
70
  protected connectInternal(): Promise<void>;
71
71
  /**
72
72
  * Close the sync connection.
@@ -0,0 +1,22 @@
1
+ import { AbstractPowerSyncDatabase } from './AbstractPowerSyncDatabase.js';
2
+ import { Query, StandardWatchedQueryOptions } from './Query.js';
3
+ import { DifferentialQueryProcessor, DifferentialWatchedQueryOptions } from './watched/processors/DifferentialQueryProcessor.js';
4
+ import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
5
+ import { WatchCompatibleQuery, WatchedQueryOptions } from './watched/WatchedQuery.js';
6
+ /**
7
+ * @internal
8
+ */
9
+ export interface CustomQueryOptions<RowType> {
10
+ db: AbstractPowerSyncDatabase;
11
+ query: WatchCompatibleQuery<RowType[]>;
12
+ }
13
+ /**
14
+ * @internal
15
+ */
16
+ export declare class CustomQuery<RowType> implements Query<RowType> {
17
+ protected options: CustomQueryOptions<RowType>;
18
+ constructor(options: CustomQueryOptions<RowType>);
19
+ protected resolveOptions(options: WatchedQueryOptions): WatchedQueryOptions;
20
+ watch(watchOptions: StandardWatchedQueryOptions<RowType>): OnChangeQueryProcessor<RowType[]>;
21
+ differentialWatch(differentialWatchOptions: DifferentialWatchedQueryOptions<RowType>): DifferentialQueryProcessor<RowType>;
22
+ }
@@ -0,0 +1,42 @@
1
+ import { FalsyComparator } from './watched/processors/comparators.js';
2
+ import { DifferentialQueryProcessor } from './watched/processors/DifferentialQueryProcessor.js';
3
+ import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
4
+ import { DEFAULT_WATCH_QUERY_OPTIONS } from './watched/WatchedQuery.js';
5
+ /**
6
+ * @internal
7
+ */
8
+ export class CustomQuery {
9
+ options;
10
+ constructor(options) {
11
+ this.options = options;
12
+ }
13
+ resolveOptions(options) {
14
+ return {
15
+ reportFetching: options?.reportFetching ?? DEFAULT_WATCH_QUERY_OPTIONS.reportFetching,
16
+ throttleMs: options?.throttleMs ?? DEFAULT_WATCH_QUERY_OPTIONS.throttleMs,
17
+ triggerOnTables: options?.triggerOnTables
18
+ };
19
+ }
20
+ watch(watchOptions) {
21
+ return new OnChangeQueryProcessor({
22
+ db: this.options.db,
23
+ comparator: watchOptions?.comparator ?? FalsyComparator,
24
+ placeholderData: watchOptions?.placeholderData ?? [],
25
+ watchOptions: {
26
+ ...this.resolveOptions(watchOptions),
27
+ query: this.options.query
28
+ }
29
+ });
30
+ }
31
+ differentialWatch(differentialWatchOptions) {
32
+ return new DifferentialQueryProcessor({
33
+ db: this.options.db,
34
+ rowComparator: differentialWatchOptions?.rowComparator,
35
+ placeholderData: differentialWatchOptions?.placeholderData ?? [],
36
+ watchOptions: {
37
+ ...this.resolveOptions(differentialWatchOptions),
38
+ query: this.options.query
39
+ }
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,97 @@
1
+ import { WatchedQueryComparator } from './watched/processors/comparators.js';
2
+ import { DifferentialWatchedQuery, DifferentialWatchedQueryOptions } from './watched/processors/DifferentialQueryProcessor.js';
3
+ import { StandardWatchedQuery } from './watched/processors/OnChangeQueryProcessor.js';
4
+ import { WatchedQueryOptions } from './watched/WatchedQuery.js';
5
+ /**
6
+ * Query parameters for {@link ArrayQueryDefinition#parameters}
7
+ */
8
+ export type QueryParam = string | number | boolean | null | undefined | bigint | Uint8Array;
9
+ /**
10
+ * Options for building a query with {@link AbstractPowerSyncDatabase#query}.
11
+ * This query will be executed with {@link AbstractPowerSyncDatabase#getAll}.
12
+ */
13
+ export interface ArrayQueryDefinition<RowType = unknown> {
14
+ sql: string;
15
+ parameters?: ReadonlyArray<Readonly<QueryParam>>;
16
+ /**
17
+ * Maps the raw SQLite row to a custom typed object.
18
+ * @example
19
+ * ```javascript
20
+ * mapper: (row) => ({
21
+ * ...row,
22
+ * created_at: new Date(row.created_at as string),
23
+ * })
24
+ * ```
25
+ */
26
+ mapper?: (row: Record<string, unknown>) => RowType;
27
+ }
28
+ /**
29
+ * Options for {@link Query#watch}.
30
+ */
31
+ export interface StandardWatchedQueryOptions<RowType> extends WatchedQueryOptions {
32
+ /**
33
+ * The underlying watched query implementation (re)evaluates the query on any SQLite table change.
34
+ *
35
+ * Providing this optional comparator can be used to filter duplicate result set emissions when the result set is unchanged.
36
+ * The comparator compares the previous and current result set.
37
+ *
38
+ * For an efficient comparator see {@link ArrayComparator}.
39
+ *
40
+ * @example
41
+ * ```javascript
42
+ * comparator: new ArrayComparator({
43
+ * compareBy: (item) => JSON.stringify(item)
44
+ * })
45
+ * ```
46
+ */
47
+ comparator?: WatchedQueryComparator<RowType[]>;
48
+ /**
49
+ * The initial data state reported while the query is loading for the first time.
50
+ * @default []
51
+ */
52
+ placeholderData?: RowType[];
53
+ }
54
+ export interface Query<RowType> {
55
+ /**
56
+ * Creates a {@link WatchedQuery} which watches and emits results of the linked query.
57
+ *
58
+ * By default the returned watched query will emit changes whenever a change to the underlying SQLite tables is made.
59
+ * These changes might not be relevant to the query, but the query will emit a new result set.
60
+ *
61
+ * A {@link StandardWatchedQueryOptions#comparator} can be provided to limit the data emissions. The watched query will still
62
+ * query the underlying DB on underlying table changes, but the result will only be emitted if the comparator detects a change in the results.
63
+ *
64
+ * The comparator in this method is optimized and returns early as soon as it detects a change. Each data emission will correlate to a change in the result set,
65
+ * but note that the result set will not maintain internal object references to the previous result set. If internal object references are needed,
66
+ * consider using {@link Query#differentialWatch} instead.
67
+ */
68
+ watch(options?: StandardWatchedQueryOptions<RowType>): StandardWatchedQuery<ReadonlyArray<Readonly<RowType>>>;
69
+ /**
70
+ * Creates a {@link WatchedQuery} which watches and emits results of the linked query.
71
+ *
72
+ * This query method watches for changes in the underlying SQLite tables and runs the query on each table change.
73
+ * The difference between the current and previous result set is computed.
74
+ * The watched query will not emit changes if the result set is identical to the previous result set.
75
+ *
76
+ * If the result set is different, the watched query will emit the new result set and emit a detailed diff of the changes via the `onData` and `onDiff` listeners.
77
+ *
78
+ * The deep differentiation allows maintaining result set object references between result emissions.
79
+ * The {@link DifferentialWatchedQuery#state} `data` array will contain the previous row references for unchanged rows.
80
+ *
81
+ * @example
82
+ * ```javascript
83
+ * const watchedLists = powerSync.query({sql: 'SELECT * FROM lists'})
84
+ * .differentialWatch();
85
+ *
86
+ * const disposeListener = watchedLists.registerListener({
87
+ * onData: (lists) => {
88
+ * console.log('The latest result set for the query is', lists);
89
+ * },
90
+ * onDiff: (diff) => {
91
+ * console.log('The lists result set has changed since the last emission', diff.added, diff.removed, diff.updated, diff.all)
92
+ * }
93
+ * })
94
+ * ```
95
+ */
96
+ differentialWatch(options?: DifferentialWatchedQueryOptions<RowType>): DifferentialWatchedQuery<RowType>;
97
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver.js';
1
+ import { BaseListener, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
2
2
  import { CrudBatch } from './CrudBatch.js';
3
3
  import { CrudEntry, OpId } from './CrudEntry.js';
4
4
  import { SyncDataBatch } from './SyncDataBatch.js';
@@ -62,7 +62,7 @@ export declare enum PowerSyncControlCommand {
62
62
  export interface BucketStorageListener extends BaseListener {
63
63
  crudUpdate: () => void;
64
64
  }
65
- export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener>, Disposable {
65
+ export interface BucketStorageAdapter extends BaseObserverInterface<BucketStorageListener>, Disposable {
66
66
  init(): Promise<void>;
67
67
  saveSyncData(batch: SyncDataBatch, fixedKeyFormat?: boolean): Promise<void>;
68
68
  removeBuckets(buckets: string[]): Promise<void>;