@powersync/common 1.34.0 → 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.
- package/dist/bundle.cjs +5 -5
- package/dist/bundle.mjs +3 -3
- package/lib/client/AbstractPowerSyncDatabase.d.ts +56 -5
- package/lib/client/AbstractPowerSyncDatabase.js +96 -29
- package/lib/client/CustomQuery.d.ts +22 -0
- package/lib/client/CustomQuery.js +42 -0
- package/lib/client/Query.d.ts +97 -0
- package/lib/client/Query.js +1 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
- package/lib/client/sync/stream/AbstractRemote.js +31 -19
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +3 -2
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +16 -4
- package/lib/client/watched/GetAllQuery.d.ts +32 -0
- package/lib/client/watched/GetAllQuery.js +24 -0
- package/lib/client/watched/WatchedQuery.d.ts +98 -0
- package/lib/client/watched/WatchedQuery.js +12 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.js +135 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +121 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js +166 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js +76 -0
- package/lib/client/watched/processors/comparators.d.ts +30 -0
- package/lib/client/watched/processors/comparators.js +34 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +8 -0
- package/lib/utils/BaseObserver.d.ts +3 -4
- package/lib/utils/BaseObserver.js +3 -0
- package/lib/utils/MetaBaseObserver.d.ts +29 -0
- package/lib/utils/MetaBaseObserver.js +50 -0
- package/package.json +1 -1
|
@@ -6,12 +6,15 @@ 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
|
|
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,7 +97,6 @@ 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
102
|
crudUploadThrottleMs: number;
|
|
@@ -389,6 +404,42 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
389
404
|
* ```
|
|
390
405
|
*/
|
|
391
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>;
|
|
392
443
|
/**
|
|
393
444
|
* Execute a read query every time the source tables are modified.
|
|
394
445
|
* Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
|
|
@@ -437,7 +488,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
437
488
|
* }
|
|
438
489
|
* ```
|
|
439
490
|
*/
|
|
440
|
-
onChange(options?:
|
|
491
|
+
onChange(options?: SQLOnChangeOptions): AsyncIterable<WatchOnChangeEvent>;
|
|
441
492
|
/**
|
|
442
493
|
* See {@link onChangeWithCallback}.
|
|
443
494
|
*
|
|
@@ -452,7 +503,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
452
503
|
* }
|
|
453
504
|
* ```
|
|
454
505
|
*/
|
|
455
|
-
onChange(handler?: WatchOnChangeHandler, options?:
|
|
506
|
+
onChange(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void;
|
|
456
507
|
/**
|
|
457
508
|
* Invoke the provided callback on any changes to any of the specified tables.
|
|
458
509
|
*
|
|
@@ -465,7 +516,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
|
|
|
465
516
|
* @param options Options for configuring watch behavior
|
|
466
517
|
* @returns A dispose function to stop watching for changes
|
|
467
518
|
*/
|
|
468
|
-
onChangeWithCallback(handler?: WatchOnChangeHandler, options?:
|
|
519
|
+
onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void;
|
|
469
520
|
/**
|
|
470
521
|
* Create a Stream of changes to any of the specified tables.
|
|
471
522
|
*
|
|
@@ -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,7 +25,6 @@ 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,
|
|
29
30
|
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
@@ -341,6 +342,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
341
342
|
if (this.closed) {
|
|
342
343
|
return;
|
|
343
344
|
}
|
|
345
|
+
await this.iterateAsyncListeners(async (cb) => cb.closing?.());
|
|
344
346
|
const { disconnect } = options;
|
|
345
347
|
if (disconnect) {
|
|
346
348
|
await this.disconnect();
|
|
@@ -348,6 +350,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
348
350
|
await this.connectionManager.close();
|
|
349
351
|
await this.database.close();
|
|
350
352
|
this.closed = true;
|
|
353
|
+
await this.iterateAsyncListeners(async (cb) => cb.closed?.());
|
|
351
354
|
}
|
|
352
355
|
/**
|
|
353
356
|
* Get upload queue size estimate and count.
|
|
@@ -594,6 +597,60 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
594
597
|
const options = handlerOrOptions;
|
|
595
598
|
return this.watchWithAsyncGenerator(sql, parameters, options);
|
|
596
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
|
+
}
|
|
597
654
|
/**
|
|
598
655
|
* Execute a read query every time the source tables are modified.
|
|
599
656
|
* Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
|
|
@@ -611,35 +668,42 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
611
668
|
if (!onResult) {
|
|
612
669
|
throw new Error('onResult is required');
|
|
613
670
|
}
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
...(options ?? {}),
|
|
633
|
-
tables: resolvedTables,
|
|
634
|
-
// Override the abort signal since we intercept it
|
|
635
|
-
signal: abortSignal
|
|
636
|
-
});
|
|
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
|
|
637
689
|
}
|
|
638
|
-
|
|
639
|
-
|
|
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);
|
|
640
701
|
}
|
|
641
|
-
};
|
|
642
|
-
|
|
702
|
+
});
|
|
703
|
+
options?.signal?.addEventListener('abort', () => {
|
|
704
|
+
dispose();
|
|
705
|
+
watchedQuery.close();
|
|
706
|
+
});
|
|
643
707
|
}
|
|
644
708
|
/**
|
|
645
709
|
* Execute a read query every time the source tables are modified.
|
|
@@ -729,6 +793,9 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
|
|
|
729
793
|
return;
|
|
730
794
|
executor.schedule({ changedTables: intersection });
|
|
731
795
|
}), throttleMs);
|
|
796
|
+
if (options?.triggerImmediate) {
|
|
797
|
+
executor.schedule({ changedTables: [] });
|
|
798
|
+
}
|
|
732
799
|
const dispose = this.database.registerListener({
|
|
733
800
|
tablesUpdated: async (update) => {
|
|
734
801
|
try {
|
|
@@ -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,
|
|
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
|
|
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>;
|
|
@@ -204,6 +204,22 @@ export class AbstractRemote {
|
|
|
204
204
|
// headers with websockets on web. The browser userAgent is however added
|
|
205
205
|
// automatically as a header.
|
|
206
206
|
const userAgent = this.getUserAgent();
|
|
207
|
+
const stream = new DataStream({
|
|
208
|
+
logger: this.logger,
|
|
209
|
+
pressure: {
|
|
210
|
+
lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
|
|
211
|
+
},
|
|
212
|
+
mapLine: map
|
|
213
|
+
});
|
|
214
|
+
// Handle upstream abort
|
|
215
|
+
if (options.abortSignal?.aborted) {
|
|
216
|
+
throw new AbortOperation('Connection request aborted');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
options.abortSignal?.addEventListener('abort', () => {
|
|
220
|
+
stream.close();
|
|
221
|
+
}, { once: true });
|
|
222
|
+
}
|
|
207
223
|
let keepAliveTimeout;
|
|
208
224
|
const resetTimeout = () => {
|
|
209
225
|
clearTimeout(keepAliveTimeout);
|
|
@@ -213,12 +229,22 @@ export class AbstractRemote {
|
|
|
213
229
|
}, SOCKET_TIMEOUT_MS);
|
|
214
230
|
};
|
|
215
231
|
resetTimeout();
|
|
232
|
+
// Typescript complains about this being `never` if it's not assigned here.
|
|
233
|
+
// This is assigned in `wsCreator`.
|
|
234
|
+
let disposeSocketConnectionTimeout = () => { };
|
|
216
235
|
const url = this.options.socketUrlTransformer(request.url);
|
|
217
236
|
const connector = new RSocketConnector({
|
|
218
237
|
transport: new WebsocketClientTransport({
|
|
219
238
|
url,
|
|
220
239
|
wsCreator: (url) => {
|
|
221
240
|
const socket = this.createSocket(url);
|
|
241
|
+
disposeSocketConnectionTimeout = stream.registerListener({
|
|
242
|
+
closed: () => {
|
|
243
|
+
// Allow closing the underlying WebSocket if the stream was closed before the
|
|
244
|
+
// RSocket connect completed. This should effectively abort the request.
|
|
245
|
+
socket.close();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
222
248
|
socket.addEventListener('message', (event) => {
|
|
223
249
|
resetTimeout();
|
|
224
250
|
});
|
|
@@ -242,20 +268,18 @@ export class AbstractRemote {
|
|
|
242
268
|
let rsocket;
|
|
243
269
|
try {
|
|
244
270
|
rsocket = await connector.connect();
|
|
271
|
+
// The connection is established, we no longer need to monitor the initial timeout
|
|
272
|
+
disposeSocketConnectionTimeout();
|
|
245
273
|
}
|
|
246
274
|
catch (ex) {
|
|
247
275
|
this.logger.error(`Failed to connect WebSocket`, ex);
|
|
248
276
|
clearTimeout(keepAliveTimeout);
|
|
277
|
+
if (!stream.closed) {
|
|
278
|
+
await stream.close();
|
|
279
|
+
}
|
|
249
280
|
throw ex;
|
|
250
281
|
}
|
|
251
282
|
resetTimeout();
|
|
252
|
-
const stream = new DataStream({
|
|
253
|
-
logger: this.logger,
|
|
254
|
-
pressure: {
|
|
255
|
-
lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
|
|
256
|
-
},
|
|
257
|
-
mapLine: map
|
|
258
|
-
});
|
|
259
283
|
let socketIsClosed = false;
|
|
260
284
|
const closeSocket = () => {
|
|
261
285
|
clearTimeout(keepAliveTimeout);
|
|
@@ -341,18 +365,6 @@ export class AbstractRemote {
|
|
|
341
365
|
l();
|
|
342
366
|
}
|
|
343
367
|
});
|
|
344
|
-
/**
|
|
345
|
-
* Handle abort operations here.
|
|
346
|
-
* Unfortunately cannot insert them into the connection.
|
|
347
|
-
*/
|
|
348
|
-
if (options.abortSignal?.aborted) {
|
|
349
|
-
stream.close();
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
options.abortSignal?.addEventListener('abort', () => {
|
|
353
|
-
stream.close();
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
368
|
return stream;
|
|
357
369
|
}
|
|
358
370
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { 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';
|
|
@@ -136,7 +136,7 @@ export interface AdditionalConnectionOptions {
|
|
|
136
136
|
}
|
|
137
137
|
/** @internal */
|
|
138
138
|
export type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>;
|
|
139
|
-
export interface StreamingSyncImplementation extends
|
|
139
|
+
export interface StreamingSyncImplementation extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
|
|
140
140
|
/**
|
|
141
141
|
* Connects to the sync service
|
|
142
142
|
*/
|
|
@@ -168,6 +168,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
168
168
|
protected _lastSyncedAt: Date | null;
|
|
169
169
|
protected options: AbstractStreamingSyncImplementationOptions;
|
|
170
170
|
protected abortController: AbortController | null;
|
|
171
|
+
protected uploadAbortController: AbortController | null;
|
|
171
172
|
protected crudUpdateListener?: () => void;
|
|
172
173
|
protected streamingSyncPromise?: Promise<void>;
|
|
173
174
|
protected logger: ILogger;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Logger from 'js-logger';
|
|
2
|
-
import { SyncStatus } from '../../../db/crud/SyncStatus.js';
|
|
3
2
|
import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js';
|
|
3
|
+
import { SyncStatus } from '../../../db/crud/SyncStatus.js';
|
|
4
4
|
import { AbortOperation } from '../../../utils/AbortOperation.js';
|
|
5
5
|
import { BaseObserver } from '../../../utils/BaseObserver.js';
|
|
6
6
|
import { throttleLeadingTrailing } from '../../../utils/async.js';
|
|
@@ -83,6 +83,9 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
83
83
|
_lastSyncedAt;
|
|
84
84
|
options;
|
|
85
85
|
abortController;
|
|
86
|
+
// In rare cases, mostly for tests, uploads can be triggered without being properly connected.
|
|
87
|
+
// This allows ensuring that all upload processes can be aborted.
|
|
88
|
+
uploadAbortController;
|
|
86
89
|
crudUpdateListener;
|
|
87
90
|
streamingSyncPromise;
|
|
88
91
|
logger;
|
|
@@ -158,8 +161,10 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
158
161
|
return this.syncStatus.connected;
|
|
159
162
|
}
|
|
160
163
|
async dispose() {
|
|
164
|
+
super.dispose();
|
|
161
165
|
this.crudUpdateListener?.();
|
|
162
166
|
this.crudUpdateListener = undefined;
|
|
167
|
+
this.uploadAbortController?.abort();
|
|
163
168
|
}
|
|
164
169
|
async hasCompletedSync() {
|
|
165
170
|
return this.options.adapter.hasCompletedSync();
|
|
@@ -180,7 +185,12 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
180
185
|
* Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
|
|
181
186
|
*/
|
|
182
187
|
let checkedCrudItem;
|
|
183
|
-
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
this.uploadAbortController = controller;
|
|
190
|
+
this.abortController?.signal.addEventListener('abort', () => {
|
|
191
|
+
controller.abort();
|
|
192
|
+
}, { once: true });
|
|
193
|
+
while (!controller.signal.aborted) {
|
|
184
194
|
try {
|
|
185
195
|
/**
|
|
186
196
|
* This is the first item in the FIFO CRUD queue.
|
|
@@ -225,7 +235,7 @@ The next upload iteration will be delayed.`);
|
|
|
225
235
|
uploadError: ex
|
|
226
236
|
}
|
|
227
237
|
});
|
|
228
|
-
await this.delayRetry();
|
|
238
|
+
await this.delayRetry(controller.signal);
|
|
229
239
|
if (!this.isConnected) {
|
|
230
240
|
// Exit the upload loop if the sync stream is no longer connected
|
|
231
241
|
break;
|
|
@@ -240,6 +250,7 @@ The next upload iteration will be delayed.`);
|
|
|
240
250
|
});
|
|
241
251
|
}
|
|
242
252
|
}
|
|
253
|
+
this.uploadAbortController = null;
|
|
243
254
|
}
|
|
244
255
|
});
|
|
245
256
|
}
|
|
@@ -439,7 +450,8 @@ The next upload iteration will be delayed.`);
|
|
|
439
450
|
});
|
|
440
451
|
}
|
|
441
452
|
async legacyStreamingSyncIteration(signal, resolvedOptions) {
|
|
442
|
-
|
|
453
|
+
const rawTables = resolvedOptions.serializedSchema?.raw_tables;
|
|
454
|
+
if (rawTables != null && rawTables.length) {
|
|
443
455
|
this.logger.warn('Raw tables require the Rust-based sync client. The JS client will ignore them.');
|
|
444
456
|
}
|
|
445
457
|
this.logger.debug('Streaming sync iteration started');
|