@powersync/common 0.0.0-dev-20250714144421 → 0.0.0-dev-20250714151300

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 (31) hide show
  1. package/dist/bundle.cjs +5 -5
  2. package/dist/bundle.mjs +3 -3
  3. package/lib/client/AbstractPowerSyncDatabase.d.ts +59 -7
  4. package/lib/client/AbstractPowerSyncDatabase.js +102 -34
  5. package/lib/client/CustomQuery.d.ts +25 -0
  6. package/lib/client/CustomQuery.js +41 -0
  7. package/lib/client/Query.d.ts +79 -0
  8. package/lib/client/Query.js +1 -0
  9. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
  10. package/lib/client/sync/bucket/SqliteBucketStorage.js +14 -14
  11. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +3 -2
  12. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +19 -25
  13. package/lib/client/watched/GetAllQuery.d.ts +32 -0
  14. package/lib/client/watched/GetAllQuery.js +24 -0
  15. package/lib/client/watched/WatchedQuery.d.ts +93 -0
  16. package/lib/client/watched/WatchedQuery.js +12 -0
  17. package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
  18. package/lib/client/watched/processors/AbstractQueryProcessor.js +136 -0
  19. package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +129 -0
  20. package/lib/client/watched/processors/DifferentialQueryProcessor.js +175 -0
  21. package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +27 -0
  22. package/lib/client/watched/processors/OnChangeQueryProcessor.js +74 -0
  23. package/lib/client/watched/processors/comparators.d.ts +24 -0
  24. package/lib/client/watched/processors/comparators.js +33 -0
  25. package/lib/index.d.ts +7 -0
  26. package/lib/index.js +7 -0
  27. package/lib/utils/BaseObserver.d.ts +3 -4
  28. package/lib/utils/BaseObserver.js +3 -0
  29. package/lib/utils/MetaBaseObserver.d.ts +29 -0
  30. package/lib/utils/MetaBaseObserver.js +50 -0
  31. package/package.json +1 -1
@@ -1,17 +1,20 @@
1
1
  import { Mutex } from 'async-mutex';
2
- import { ILogger } from 'js-logger';
2
+ import Logger, { 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,9 +97,9 @@ 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;
102
+ logger: Logger.ILogger;
87
103
  crudUploadThrottleMs: number;
88
104
  };
89
105
  export declare const DEFAULT_CRUD_BATCH_LIMIT = 100;
@@ -117,7 +133,6 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
117
133
  protected _schema: Schema;
118
134
  private _database;
119
135
  protected runExclusiveMutex: Mutex;
120
- logger: ILogger;
121
136
  constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
122
137
  constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
123
138
  constructor(options: PowerSyncDatabaseOptionsWithSettings);
@@ -180,6 +195,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
180
195
  * Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
181
196
  */
182
197
  updateSchema(schema: Schema): Promise<void>;
198
+ get logger(): Logger.ILogger;
183
199
  /**
184
200
  * Wait for initialization to complete.
185
201
  * While initializing is automatic, this helps to catch and report initialization errors.
@@ -389,6 +405,42 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
389
405
  * ```
390
406
  */
391
407
  watch(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void;
408
+ /**
409
+ * Allows defining a query which can be used to build a {@link WatchedQuery}.
410
+ * The defined query will be executed with {@link AbstractPowerSyncDatabase#getAll}.
411
+ * An optional mapper function can be provided to transform the results.
412
+ *
413
+ * @example
414
+ * ```javascript
415
+ * const watchedTodos = powersync.query({
416
+ * sql: `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`,
417
+ * parameters: [],
418
+ * mapper: (row) => ({
419
+ * ...row,
420
+ * created_at: new Date(row.created_at as string)
421
+ * })
422
+ * })
423
+ * .watch()
424
+ * // OR use .differentialWatch() for fine-grained watches.
425
+ * ```
426
+ */
427
+ query<RowType>(query: ArrayQueryDefinition<RowType>): Query<RowType>;
428
+ /**
429
+ * Allows building a {@link WatchedQuery} using an existing {@link WatchCompatibleQuery}.
430
+ * The watched query will use the provided {@link WatchCompatibleQuery.execute} method to query results.
431
+ *
432
+ * @example
433
+ * ```javascript
434
+ *
435
+ * // Potentially a query from an ORM like Drizzle
436
+ * const query = db.select().from(lists);
437
+ *
438
+ * const watchedTodos = powersync.customQuery(query)
439
+ * .watch()
440
+ * // OR use .differentialWatch() for fine-grained watches.
441
+ * ```
442
+ */
443
+ customQuery<RowType>(query: WatchCompatibleQuery<RowType[]>): Query<RowType>;
392
444
  /**
393
445
  * Execute a read query every time the source tables are modified.
394
446
  * Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
@@ -437,7 +489,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
437
489
  * }
438
490
  * ```
439
491
  */
440
- onChange(options?: SQLWatchOptions): AsyncIterable<WatchOnChangeEvent>;
492
+ onChange(options?: SQLOnChangeOptions): AsyncIterable<WatchOnChangeEvent>;
441
493
  /**
442
494
  * See {@link onChangeWithCallback}.
443
495
  *
@@ -452,7 +504,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
452
504
  * }
453
505
  * ```
454
506
  */
455
- onChange(handler?: WatchOnChangeHandler, options?: SQLWatchOptions): () => void;
507
+ onChange(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void;
456
508
  /**
457
509
  * Invoke the provided callback on any changes to any of the specified tables.
458
510
  *
@@ -465,7 +517,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
465
517
  * @param options Options for configuring watch behavior
466
518
  * @returns A dispose function to stop watching for changes
467
519
  */
468
- onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLWatchOptions): () => void;
520
+ onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void;
469
521
  /**
470
522
  * Create a Stream of changes to any of the specified tables.
471
523
  *
@@ -9,13 +9,15 @@ import { BaseObserver } from '../utils/BaseObserver.js';
9
9
  import { ControlledExecutor } from '../utils/ControlledExecutor.js';
10
10
  import { throttleTrailing } from '../utils/async.js';
11
11
  import { ConnectionManager } from './ConnectionManager.js';
12
+ import { CustomQuery } from './CustomQuery.js';
12
13
  import { isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
13
- import { runOnSchemaChange } from './runOnSchemaChange.js';
14
14
  import { PSInternalTable } from './sync/bucket/BucketStorageAdapter.js';
15
15
  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 { DEFAULT_WATCH_THROTTLE_MS } from './watched/WatchedQuery.js';
20
+ import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
19
21
  const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
20
22
  const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
21
23
  clearLocal: true
@@ -23,9 +25,9 @@ const DEFAULT_DISCONNECT_CLEAR_OPTIONS = {
23
25
  export const DEFAULT_POWERSYNC_CLOSE_OPTIONS = {
24
26
  disconnect: true
25
27
  };
26
- export const DEFAULT_WATCH_THROTTLE_MS = 30;
27
28
  export const DEFAULT_POWERSYNC_DB_OPTIONS = {
28
29
  retryDelayMs: 5000,
30
+ logger: Logger.get('PowerSyncDatabase'),
29
31
  crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
30
32
  };
31
33
  export const DEFAULT_CRUD_BATCH_LIMIT = 100;
@@ -63,7 +65,6 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
63
65
  _schema;
64
66
  _database;
65
67
  runExclusiveMutex;
66
- logger;
67
68
  constructor(options) {
68
69
  super();
69
70
  this.options = options;
@@ -83,7 +84,6 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
83
84
  else {
84
85
  throw new Error('The provided `database` option is invalid.');
85
86
  }
86
- this.logger = options.logger ?? Logger.get(`PowerSyncDatabase[${this._database.name}]`);
87
87
  this.bucketStorageAdapter = this.generateBucketStorageAdapter();
88
88
  this.closed = false;
89
89
  this.currentStatus = new SyncStatus({});
@@ -263,13 +263,16 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
263
263
  schema.validate();
264
264
  }
265
265
  catch (ex) {
266
- this.logger.warn('Schema validation failed. Unexpected behaviour could occur', ex);
266
+ this.options.logger?.warn('Schema validation failed. Unexpected behaviour could occur', ex);
267
267
  }
268
268
  this._schema = schema;
269
269
  await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
270
270
  await this.database.refreshSchema();
271
271
  this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
272
272
  }
273
+ get logger() {
274
+ return this.options.logger;
275
+ }
273
276
  /**
274
277
  * Wait for initialization to complete.
275
278
  * While initializing is automatic, this helps to catch and report initialization errors.
@@ -339,6 +342,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
339
342
  if (this.closed) {
340
343
  return;
341
344
  }
345
+ await this.iterateAsyncListeners(async (cb) => cb.closing?.());
342
346
  const { disconnect } = options;
343
347
  if (disconnect) {
344
348
  await this.disconnect();
@@ -346,6 +350,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
346
350
  await this.connectionManager.close();
347
351
  await this.database.close();
348
352
  this.closed = true;
353
+ await this.iterateAsyncListeners(async (cb) => cb.closed?.());
349
354
  }
350
355
  /**
351
356
  * Get upload queue size estimate and count.
@@ -592,6 +597,60 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
592
597
  const options = handlerOrOptions;
593
598
  return this.watchWithAsyncGenerator(sql, parameters, options);
594
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
+ }
595
654
  /**
596
655
  * Execute a read query every time the source tables are modified.
597
656
  * Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
@@ -605,39 +664,45 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
605
664
  * @param options Options for configuring watch behavior
606
665
  */
607
666
  watchWithCallback(sql, parameters, handler, options) {
608
- const { onResult, onError = (e) => this.logger.error(e) } = handler ?? {};
667
+ const { onResult, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
609
668
  if (!onResult) {
610
669
  throw new Error('onResult is required');
611
670
  }
612
- const watchQuery = async (abortSignal) => {
613
- try {
614
- const resolvedTables = await this.resolveTables(sql, parameters, options);
615
- // Fetch initial data
616
- const result = await this.executeReadOnly(sql, parameters);
617
- onResult(result);
618
- this.onChangeWithCallback({
619
- onChange: async () => {
620
- try {
621
- const result = await this.executeReadOnly(sql, parameters);
622
- onResult(result);
623
- }
624
- catch (error) {
625
- onError?.(error);
626
- }
627
- },
628
- onError
629
- }, {
630
- ...(options ?? {}),
631
- tables: resolvedTables,
632
- // Override the abort signal since we intercept it
633
- signal: abortSignal
634
- });
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
635
688
  }
636
- catch (error) {
637
- onError?.(error);
689
+ });
690
+ const dispose = watchedQuery.registerListener({
691
+ onData: (data) => {
692
+ if (!data) {
693
+ // This should not happen. We only use null for the initial data.
694
+ return;
695
+ }
696
+ onResult(data);
697
+ },
698
+ onError: (error) => {
699
+ onError(error);
638
700
  }
639
- };
640
- runOnSchemaChange(watchQuery, this, options);
701
+ });
702
+ options?.signal?.addEventListener('abort', () => {
703
+ dispose();
704
+ watchedQuery.close();
705
+ });
641
706
  }
642
707
  /**
643
708
  * Execute a read query every time the source tables are modified.
@@ -711,7 +776,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
711
776
  * @returns A dispose function to stop watching for changes
712
777
  */
713
778
  onChangeWithCallback(handler, options) {
714
- const { onChange, onError = (e) => this.logger.error(e) } = handler ?? {};
779
+ const { onChange, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
715
780
  if (!onChange) {
716
781
  throw new Error('onChange is required');
717
782
  }
@@ -727,6 +792,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
727
792
  return;
728
793
  executor.schedule({ changedTables: intersection });
729
794
  }), throttleMs);
795
+ if (options?.triggerImmediate) {
796
+ executor.schedule({ changedTables: [] });
797
+ }
730
798
  const dispose = this.database.registerListener({
731
799
  tablesUpdated: async (update) => {
732
800
  try {
@@ -0,0 +1,25 @@
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): {
20
+ reportFetching: boolean | undefined;
21
+ throttleMs: number | undefined;
22
+ };
23
+ watch(watchOptions: StandardWatchedQueryOptions<RowType>): OnChangeQueryProcessor<RowType[]>;
24
+ differentialWatch(differentialWatchOptions: DifferentialWatchedQueryOptions<RowType>): DifferentialQueryProcessor<RowType>;
25
+ }
@@ -0,0 +1,41 @@
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
+ };
18
+ }
19
+ watch(watchOptions) {
20
+ return new OnChangeQueryProcessor({
21
+ db: this.options.db,
22
+ comparator: watchOptions?.comparator ?? FalsyComparator,
23
+ placeholderData: watchOptions?.placeholderData ?? [],
24
+ watchOptions: {
25
+ ...this.resolveOptions(watchOptions),
26
+ query: this.options.query
27
+ }
28
+ });
29
+ }
30
+ differentialWatch(differentialWatchOptions) {
31
+ return new DifferentialQueryProcessor({
32
+ db: this.options.db,
33
+ differentiator: differentialWatchOptions?.differentiator,
34
+ placeholderData: differentialWatchOptions?.placeholderData ?? [],
35
+ watchOptions: {
36
+ ...this.resolveOptions(differentialWatchOptions),
37
+ query: this.options.query
38
+ }
39
+ });
40
+ }
41
+ }
@@ -0,0 +1,79 @@
1
+ import { ArrayComparator } from './watched/processors/comparators.js';
2
+ import { DifferentialWatchedQuery, DifferentialWatchedQueryOptions } from './watched/processors/DifferentialQueryProcessor.js';
3
+ import { ComparisonWatchedQuery } 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
+ * Optional comparator which processes the items of an array of rows.
34
+ * The comparator compares the result set rows by index using the {@link ArrayComparatorOptions#compareBy} function.
35
+ * The comparator reports a changed result set as soon as a row does not match the previous result set.
36
+ *
37
+ * @example
38
+ * ```javascript
39
+ * comparator: new ArrayComparator({
40
+ * compareBy: (item) => JSON.stringify(item)
41
+ * })
42
+ * ```
43
+ */
44
+ comparator?: ArrayComparator<RowType>;
45
+ /**
46
+ * The initial data state reported while the query is loading for the first time.
47
+ * @default []
48
+ */
49
+ placeholderData?: RowType[];
50
+ }
51
+ export interface Query<RowType> {
52
+ /**
53
+ * Creates a {@link WatchedQuery} which watches and emits results of the linked query.
54
+ *
55
+ * By default the returned watched query will emit changes whenever a change to the underlying SQLite tables is made.
56
+ * These changes might not be relevant to the query, but the query will emit a new result set.
57
+ *
58
+ * A {@link StandardWatchedQueryOptions#comparator} can be provided to limit the data emissions. The watched query will still
59
+ * query the underlying DB on underlying table changes, but the result will only be emitted if the comparator detects a change in the results.
60
+ *
61
+ * 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,
62
+ * but note that the result set will not maintain internal object references to the previous result set. If internal object references are needed,
63
+ * consider using {@link Query#differentialWatch} instead.
64
+ */
65
+ watch(options?: StandardWatchedQueryOptions<RowType>): ComparisonWatchedQuery<ReadonlyArray<Readonly<RowType>>>;
66
+ /**
67
+ * Creates a {@link WatchedQuery} which watches and emits results of the linked query.
68
+ *
69
+ * This query method watches for changes in the underlying SQLite tables and runs the query on each table change.
70
+ * The difference between the current and previous result set is computed.
71
+ * The watched query will not emit changes if the result set is identical to the previous result set.
72
+ * If the result set is different, the watched query will emit the new result set and provide a detailed diff of the changes.
73
+ *
74
+ * The deep differentiation allows maintaining result set object references between result emissions.
75
+ * The {@link DifferentialWatchedQuery#state} `data` array will contain the previous row references for unchanged rows.
76
+ * A detailed diff of the changes can be accessed via {@link DifferentialWatchedQuery#state} `diff`.
77
+ */
78
+ differentialWatch(options?: DifferentialWatchedQueryOptions<RowType>): DifferentialWatchedQuery<RowType>;
79
+ }
@@ -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>;
@@ -64,11 +64,11 @@ export class SqliteBucketStorage extends BaseObserver {
64
64
  async saveSyncData(batch, fixedKeyFormat = false) {
65
65
  await this.writeTransaction(async (tx) => {
66
66
  for (const b of batch.buckets) {
67
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
67
+ const result = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
68
68
  'save',
69
69
  JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
70
70
  ]);
71
- this.logger.debug(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
71
+ this.logger.debug('saveSyncData', JSON.stringify(result));
72
72
  }
73
73
  });
74
74
  }
@@ -84,7 +84,7 @@ export class SqliteBucketStorage extends BaseObserver {
84
84
  await this.writeTransaction(async (tx) => {
85
85
  await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
86
86
  });
87
- this.logger.debug(`Done deleting bucket ${bucket}`);
87
+ this.logger.debug('done deleting bucket');
88
88
  }
89
89
  async hasCompletedSync() {
90
90
  if (this._hasCompletedSync) {
@@ -106,12 +106,6 @@ export class SqliteBucketStorage extends BaseObserver {
106
106
  }
107
107
  return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
108
108
  }
109
- if (priority == null) {
110
- this.logger.debug(`Validated checksums checkpoint ${checkpoint.last_op_id}`);
111
- }
112
- else {
113
- this.logger.debug(`Validated checksums for partial checkpoint ${checkpoint.last_op_id}, priority ${priority}`);
114
- }
115
109
  let buckets = checkpoint.buckets;
116
110
  if (priority !== undefined) {
117
111
  buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
@@ -128,6 +122,7 @@ export class SqliteBucketStorage extends BaseObserver {
128
122
  });
129
123
  const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
130
124
  if (!valid) {
125
+ this.logger.debug('Not at a consistent checkpoint - cannot update local db');
131
126
  return { ready: false, checkpointValid: true };
132
127
  }
133
128
  return {
@@ -180,6 +175,7 @@ export class SqliteBucketStorage extends BaseObserver {
180
175
  JSON.stringify({ ...checkpoint })
181
176
  ]);
182
177
  const resultItem = rs.rows?.item(0);
178
+ this.logger.debug('validateChecksums priority, checkpoint, result item', priority, checkpoint, resultItem);
183
179
  if (!resultItem) {
184
180
  return {
185
181
  checkpointValid: false,
@@ -212,26 +208,30 @@ export class SqliteBucketStorage extends BaseObserver {
212
208
  }
213
209
  const seqBefore = rs[0]['seq'];
214
210
  const opId = await cb();
211
+ this.logger.debug(`[updateLocalTarget] Updating target to checkpoint ${opId}`);
215
212
  return this.writeTransaction(async (tx) => {
216
213
  const anyData = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
217
214
  if (anyData.rows?.length) {
218
215
  // if isNotEmpty
219
- this.logger.debug(`New data uploaded since write checkpoint ${opId} - need new write checkpoint`);
216
+ this.logger.debug('updateLocalTarget', 'ps crud is not empty');
220
217
  return false;
221
218
  }
222
219
  const rs = await tx.execute("SELECT seq FROM sqlite_sequence WHERE name = 'ps_crud'");
223
220
  if (!rs.rows?.length) {
224
221
  // assert isNotEmpty
225
- throw new Error('SQLite Sequence should not be empty');
222
+ throw new Error('SQlite Sequence should not be empty');
226
223
  }
227
224
  const seqAfter = rs.rows?.item(0)['seq'];
225
+ this.logger.debug('seqAfter', JSON.stringify(rs.rows?.item(0)));
228
226
  if (seqAfter != seqBefore) {
229
- this.logger.debug(`New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)`);
227
+ this.logger.debug('seqAfter != seqBefore', seqAfter, seqBefore);
230
228
  // New crud data may have been uploaded since we got the checkpoint. Abort.
231
229
  return false;
232
230
  }
233
- this.logger.debug(`Updating target write checkpoint to ${opId}`);
234
- await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [opId]);
231
+ const response = await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [
232
+ opId
233
+ ]);
234
+ this.logger.debug(['[updateLocalTarget] Response from updating target_op ', JSON.stringify(response)]);
235
235
  return true;
236
236
  });
237
237
  }
@@ -1,6 +1,6 @@
1
1
  import Logger, { ILogger } from 'js-logger';
2
2
  import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
3
- import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver.js';
3
+ import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
4
4
  import { BucketStorageAdapter } from '../bucket/BucketStorageAdapter.js';
5
5
  import { AbstractRemote, FetchStrategy } from './AbstractRemote.js';
6
6
  import { StreamingSyncRequestParameterType } from './streaming-sync-types.js';
@@ -131,7 +131,7 @@ export interface AdditionalConnectionOptions {
131
131
  }
132
132
  /** @internal */
133
133
  export type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>;
134
- export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener>, Disposable {
134
+ export interface StreamingSyncImplementation extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
135
135
  /**
136
136
  * Connects to the sync service
137
137
  */
@@ -164,6 +164,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
164
164
  protected _lastSyncedAt: Date | null;
165
165
  protected options: AbstractStreamingSyncImplementationOptions;
166
166
  protected abortController: AbortController | null;
167
+ protected uploadAbortController: AbortController | null;
167
168
  protected crudUpdateListener?: () => void;
168
169
  protected streamingSyncPromise?: Promise<void>;
169
170
  private isUploadingCrud;