@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.
- package/dist/bundle.cjs +5 -5
- package/dist/bundle.mjs +3 -3
- package/lib/client/AbstractPowerSyncDatabase.d.ts +59 -7
- package/lib/client/AbstractPowerSyncDatabase.js +102 -34
- package/lib/client/CustomQuery.d.ts +25 -0
- package/lib/client/CustomQuery.js +41 -0
- package/lib/client/Query.d.ts +79 -0
- package/lib/client/Query.js +1 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.js +14 -14
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +3 -2
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +19 -25
- package/lib/client/watched/GetAllQuery.d.ts +32 -0
- package/lib/client/watched/GetAllQuery.js +24 -0
- package/lib/client/watched/WatchedQuery.d.ts +93 -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 +136 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +129 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js +175 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +27 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js +74 -0
- package/lib/client/watched/processors/comparators.d.ts +24 -0
- package/lib/client/watched/processors/comparators.js +33 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +7 -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
|
@@ -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
|
isUploadingCrud = false;
|
|
@@ -159,8 +162,10 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
159
162
|
return this.options.logger;
|
|
160
163
|
}
|
|
161
164
|
async dispose() {
|
|
165
|
+
super.dispose();
|
|
162
166
|
this.crudUpdateListener?.();
|
|
163
167
|
this.crudUpdateListener = undefined;
|
|
168
|
+
this.uploadAbortController?.abort();
|
|
164
169
|
}
|
|
165
170
|
async hasCompletedSync() {
|
|
166
171
|
return this.options.adapter.hasCompletedSync();
|
|
@@ -169,9 +174,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
169
174
|
const clientId = await this.options.adapter.getClientId();
|
|
170
175
|
let path = `/write-checkpoint2.json?client_id=${clientId}`;
|
|
171
176
|
const response = await this.options.remote.get(path);
|
|
172
|
-
|
|
173
|
-
this.logger.debug(`Created write checkpoint: ${checkpoint}`);
|
|
174
|
-
return checkpoint;
|
|
177
|
+
return response['data']['write_checkpoint'];
|
|
175
178
|
}
|
|
176
179
|
async _uploadAllCrud() {
|
|
177
180
|
return this.obtainLock({
|
|
@@ -181,7 +184,12 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
181
184
|
* Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
|
|
182
185
|
*/
|
|
183
186
|
let checkedCrudItem;
|
|
184
|
-
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
this.uploadAbortController = controller;
|
|
189
|
+
this.abortController?.signal.addEventListener('abort', () => {
|
|
190
|
+
controller.abort();
|
|
191
|
+
}, { once: true });
|
|
192
|
+
while (!controller.signal.aborted) {
|
|
185
193
|
try {
|
|
186
194
|
/**
|
|
187
195
|
* This is the first item in the FIFO CRUD queue.
|
|
@@ -210,11 +218,7 @@ The next upload iteration will be delayed.`);
|
|
|
210
218
|
}
|
|
211
219
|
else {
|
|
212
220
|
// Uploading is completed
|
|
213
|
-
|
|
214
|
-
if (neededUpdate == false && checkedCrudItem != null) {
|
|
215
|
-
// Only log this if there was something to upload
|
|
216
|
-
this.logger.debug('Upload complete, no write checkpoint needed.');
|
|
217
|
-
}
|
|
221
|
+
await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
|
|
218
222
|
break;
|
|
219
223
|
}
|
|
220
224
|
}
|
|
@@ -226,7 +230,7 @@ The next upload iteration will be delayed.`);
|
|
|
226
230
|
uploadError: ex
|
|
227
231
|
}
|
|
228
232
|
});
|
|
229
|
-
await this.delayRetry();
|
|
233
|
+
await this.delayRetry(controller.signal);
|
|
230
234
|
if (!this.isConnected) {
|
|
231
235
|
// Exit the upload loop if the sync stream is no longer connected
|
|
232
236
|
break;
|
|
@@ -241,6 +245,7 @@ The next upload iteration will be delayed.`);
|
|
|
241
245
|
});
|
|
242
246
|
}
|
|
243
247
|
}
|
|
248
|
+
this.uploadAbortController = null;
|
|
244
249
|
}
|
|
245
250
|
});
|
|
246
251
|
}
|
|
@@ -521,8 +526,6 @@ The next upload iteration will be delayed.`);
|
|
|
521
526
|
}
|
|
522
527
|
if (isStreamingSyncCheckpoint(line)) {
|
|
523
528
|
targetCheckpoint = line.checkpoint;
|
|
524
|
-
// New checkpoint - existing validated checkpoint is no longer valid
|
|
525
|
-
pendingValidatedCheckpoint = null;
|
|
526
529
|
const bucketsToDelete = new Set(bucketMap.keys());
|
|
527
530
|
const newBuckets = new Map();
|
|
528
531
|
for (const checksum of line.checkpoint.buckets) {
|
|
@@ -546,15 +549,8 @@ The next upload iteration will be delayed.`);
|
|
|
546
549
|
return;
|
|
547
550
|
}
|
|
548
551
|
else if (!result.applied) {
|
|
549
|
-
// "Could not apply checkpoint due to local data". We need to retry after
|
|
550
|
-
// finishing uploads.
|
|
551
552
|
pendingValidatedCheckpoint = targetCheckpoint;
|
|
552
553
|
}
|
|
553
|
-
else {
|
|
554
|
-
// Nothing to retry later. This would likely already be null from the last
|
|
555
|
-
// checksum or checksum_diff operation, but we make sure.
|
|
556
|
-
pendingValidatedCheckpoint = null;
|
|
557
|
-
}
|
|
558
554
|
}
|
|
559
555
|
else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
|
|
560
556
|
const priority = line.partial_checkpoint_complete.priority;
|
|
@@ -591,8 +587,6 @@ The next upload iteration will be delayed.`);
|
|
|
591
587
|
if (targetCheckpoint == null) {
|
|
592
588
|
throw new Error('Checkpoint diff without previous checkpoint');
|
|
593
589
|
}
|
|
594
|
-
// New checkpoint - existing validated checkpoint is no longer valid
|
|
595
|
-
pendingValidatedCheckpoint = null;
|
|
596
590
|
const diff = line.checkpoint_diff;
|
|
597
591
|
const newBuckets = new Map();
|
|
598
592
|
for (const checksum of targetCheckpoint.buckets) {
|
|
@@ -885,17 +879,17 @@ The next upload iteration will be delayed.`);
|
|
|
885
879
|
async applyCheckpoint(checkpoint) {
|
|
886
880
|
let result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
887
881
|
if (!result.checkpointValid) {
|
|
888
|
-
this.logger.debug(
|
|
882
|
+
this.logger.debug('Checksum mismatch in checkpoint, will reconnect');
|
|
889
883
|
// This means checksums failed. Start again with a new checkpoint.
|
|
890
884
|
// TODO: better back-off
|
|
891
885
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
892
886
|
return { applied: false, endIteration: true };
|
|
893
887
|
}
|
|
894
888
|
else if (!result.ready) {
|
|
895
|
-
this.logger.debug(
|
|
889
|
+
this.logger.debug('Could not apply checkpoint due to local data. We will retry applying the checkpoint after that upload is completed.');
|
|
896
890
|
return { applied: false, endIteration: false };
|
|
897
891
|
}
|
|
898
|
-
this.logger.debug(
|
|
892
|
+
this.logger.debug('validated checkpoint', checkpoint);
|
|
899
893
|
this.updateSyncStatus({
|
|
900
894
|
connected: true,
|
|
901
895
|
lastSyncedAt: new Date(),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CompiledQuery } from '../../types/types.js';
|
|
2
|
+
import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
|
|
3
|
+
import { WatchCompatibleQuery } from './WatchedQuery.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options for {@link GetAllQuery}.
|
|
6
|
+
*/
|
|
7
|
+
export type GetAllQueryOptions<RowType = unknown> = {
|
|
8
|
+
sql: string;
|
|
9
|
+
parameters?: ReadonlyArray<unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Optional mapper function to convert raw rows into the desired RowType.
|
|
12
|
+
* @example
|
|
13
|
+
* ```javascript
|
|
14
|
+
* (rawRow) => ({
|
|
15
|
+
* id: rawRow.id,
|
|
16
|
+
* created_at: new Date(rawRow.created_at),
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
mapper?: (rawRow: Record<string, unknown>) => RowType;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query.
|
|
24
|
+
*/
|
|
25
|
+
export declare class GetAllQuery<RowType = unknown> implements WatchCompatibleQuery<RowType[]> {
|
|
26
|
+
protected options: GetAllQueryOptions<RowType>;
|
|
27
|
+
constructor(options: GetAllQueryOptions<RowType>);
|
|
28
|
+
compile(): CompiledQuery;
|
|
29
|
+
execute(options: {
|
|
30
|
+
db: AbstractPowerSyncDatabase;
|
|
31
|
+
}): Promise<RowType[]>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query.
|
|
3
|
+
*/
|
|
4
|
+
export class GetAllQuery {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
compile() {
|
|
10
|
+
return {
|
|
11
|
+
sql: this.options.sql,
|
|
12
|
+
parameters: this.options.parameters ?? []
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async execute(options) {
|
|
16
|
+
const { db } = options;
|
|
17
|
+
const { sql, parameters = [] } = this.compile();
|
|
18
|
+
const rawResult = await db.getAll(sql, [...parameters]);
|
|
19
|
+
if (this.options.mapper) {
|
|
20
|
+
return rawResult.map(this.options.mapper);
|
|
21
|
+
}
|
|
22
|
+
return rawResult;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CompiledQuery } from '../../types/types.js';
|
|
2
|
+
import { BaseListener } from '../../utils/BaseObserver.js';
|
|
3
|
+
import { MetaBaseObserverInterface } from '../../utils/MetaBaseObserver.js';
|
|
4
|
+
import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
|
|
5
|
+
/**
|
|
6
|
+
* State for {@link WatchedQuery} instances.
|
|
7
|
+
*/
|
|
8
|
+
export interface WatchedQueryState<Data> {
|
|
9
|
+
/**
|
|
10
|
+
* Indicates the initial loading state (hard loading).
|
|
11
|
+
* Loading becomes false once the first set of results from the watched query is available or an error occurs.
|
|
12
|
+
*/
|
|
13
|
+
readonly isLoading: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Indicates whether the query is currently fetching data, is true during the initial load
|
|
16
|
+
* and any time when the query is re-evaluating (useful for large queries).
|
|
17
|
+
*/
|
|
18
|
+
readonly isFetching: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The last error that occurred while executing the query.
|
|
21
|
+
*/
|
|
22
|
+
readonly error: Error | null;
|
|
23
|
+
/**
|
|
24
|
+
* The last time the query was updated.
|
|
25
|
+
*/
|
|
26
|
+
readonly lastUpdated: Date | null;
|
|
27
|
+
/**
|
|
28
|
+
* The last data returned by the query.
|
|
29
|
+
*/
|
|
30
|
+
readonly data: Data;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Options provided to the `execute` method of a {@link WatchCompatibleQuery}.
|
|
34
|
+
*/
|
|
35
|
+
export interface WatchExecuteOptions {
|
|
36
|
+
sql: string;
|
|
37
|
+
parameters: any[];
|
|
38
|
+
db: AbstractPowerSyncDatabase;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Similar to {@link CompatibleQuery}, except the `execute` method
|
|
42
|
+
* does not enforce an Array result type.
|
|
43
|
+
*/
|
|
44
|
+
export interface WatchCompatibleQuery<ResultType> {
|
|
45
|
+
execute(options: WatchExecuteOptions): Promise<ResultType>;
|
|
46
|
+
compile(): CompiledQuery;
|
|
47
|
+
}
|
|
48
|
+
export interface WatchedQueryOptions {
|
|
49
|
+
/** The minimum interval between queries. */
|
|
50
|
+
throttleMs?: number;
|
|
51
|
+
/**
|
|
52
|
+
* If true (default) the watched query will update its state to report
|
|
53
|
+
* on the fetching state of the query.
|
|
54
|
+
* Setting to false reduces the number of state changes if the fetch status
|
|
55
|
+
* is not relevant to the consumer.
|
|
56
|
+
*/
|
|
57
|
+
reportFetching?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare enum WatchedQueryListenerEvent {
|
|
60
|
+
ON_DATA = "onData",
|
|
61
|
+
ON_ERROR = "onError",
|
|
62
|
+
ON_STATE_CHANGE = "onStateChange",
|
|
63
|
+
CLOSED = "closed"
|
|
64
|
+
}
|
|
65
|
+
export interface WatchedQueryListener<Data> extends BaseListener {
|
|
66
|
+
[WatchedQueryListenerEvent.ON_DATA]?: (data: Data) => void | Promise<void>;
|
|
67
|
+
[WatchedQueryListenerEvent.ON_ERROR]?: (error: Error) => void | Promise<void>;
|
|
68
|
+
[WatchedQueryListenerEvent.ON_STATE_CHANGE]?: (state: WatchedQueryState<Data>) => void | Promise<void>;
|
|
69
|
+
[WatchedQueryListenerEvent.CLOSED]?: () => void | Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
|
|
72
|
+
export declare const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions;
|
|
73
|
+
export interface WatchedQuery<Data = unknown, Settings extends WatchedQueryOptions = WatchedQueryOptions, Listener extends WatchedQueryListener<Data> = WatchedQueryListener<Data>> extends MetaBaseObserverInterface<Listener> {
|
|
74
|
+
/**
|
|
75
|
+
* Current state of the watched query.
|
|
76
|
+
*/
|
|
77
|
+
readonly state: WatchedQueryState<Data>;
|
|
78
|
+
readonly closed: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Subscribe to watched query events.
|
|
81
|
+
* @returns A function to unsubscribe from the events.
|
|
82
|
+
*/
|
|
83
|
+
registerListener(listener: Listener): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Updates the underlying query options.
|
|
86
|
+
* This will trigger a re-evaluation of the query and update the state.
|
|
87
|
+
*/
|
|
88
|
+
updateSettings(options: Settings): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Close the watched query and end all subscriptions.
|
|
91
|
+
*/
|
|
92
|
+
close(): Promise<void>;
|
|
93
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export var WatchedQueryListenerEvent;
|
|
2
|
+
(function (WatchedQueryListenerEvent) {
|
|
3
|
+
WatchedQueryListenerEvent["ON_DATA"] = "onData";
|
|
4
|
+
WatchedQueryListenerEvent["ON_ERROR"] = "onError";
|
|
5
|
+
WatchedQueryListenerEvent["ON_STATE_CHANGE"] = "onStateChange";
|
|
6
|
+
WatchedQueryListenerEvent["CLOSED"] = "closed";
|
|
7
|
+
})(WatchedQueryListenerEvent || (WatchedQueryListenerEvent = {}));
|
|
8
|
+
export const DEFAULT_WATCH_THROTTLE_MS = 30;
|
|
9
|
+
export const DEFAULT_WATCH_QUERY_OPTIONS = {
|
|
10
|
+
throttleMs: DEFAULT_WATCH_THROTTLE_MS,
|
|
11
|
+
reportFetching: true
|
|
12
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AbstractPowerSyncDatabase } from '../../../client/AbstractPowerSyncDatabase.js';
|
|
2
|
+
import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js';
|
|
3
|
+
import { WatchedQuery, WatchedQueryListener, WatchedQueryOptions, WatchedQueryState } from '../WatchedQuery.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export interface AbstractQueryProcessorOptions<Data, Settings extends WatchedQueryOptions = WatchedQueryOptions> {
|
|
8
|
+
db: AbstractPowerSyncDatabase;
|
|
9
|
+
watchOptions: Settings;
|
|
10
|
+
placeholderData: Data;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export interface LinkQueryOptions<Data, Settings extends WatchedQueryOptions = WatchedQueryOptions> {
|
|
16
|
+
abortSignal: AbortSignal;
|
|
17
|
+
settings: Settings;
|
|
18
|
+
}
|
|
19
|
+
type MutableDeep<T> = T extends ReadonlyArray<infer U> ? U[] : T;
|
|
20
|
+
/**
|
|
21
|
+
* @internal Mutable version of {@link WatchedQueryState}.
|
|
22
|
+
* This is used internally to allow updates to the state.
|
|
23
|
+
*/
|
|
24
|
+
export type MutableWatchedQueryState<Data> = {
|
|
25
|
+
-readonly [P in keyof WatchedQueryState<Data>]: MutableDeep<WatchedQueryState<Data>[P]>;
|
|
26
|
+
};
|
|
27
|
+
type WatchedQueryProcessorListener<Data> = WatchedQueryListener<Data>;
|
|
28
|
+
/**
|
|
29
|
+
* Performs underlying watching and yields a stream of results.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export declare abstract class AbstractQueryProcessor<Data = unknown[], Settings extends WatchedQueryOptions = WatchedQueryOptions> extends MetaBaseObserver<WatchedQueryProcessorListener<Data>> implements WatchedQuery<Data, Settings> {
|
|
33
|
+
protected options: AbstractQueryProcessorOptions<Data, Settings>;
|
|
34
|
+
readonly state: WatchedQueryState<Data>;
|
|
35
|
+
protected abortController: AbortController;
|
|
36
|
+
protected initialized: Promise<void>;
|
|
37
|
+
protected _closed: boolean;
|
|
38
|
+
protected disposeListeners: (() => void) | null;
|
|
39
|
+
get closed(): boolean;
|
|
40
|
+
constructor(options: AbstractQueryProcessorOptions<Data, Settings>);
|
|
41
|
+
protected constructInitialState(): WatchedQueryState<Data>;
|
|
42
|
+
protected get reportFetching(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Updates the underlying query.
|
|
45
|
+
*/
|
|
46
|
+
updateSettings(settings: Settings): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* This method is used to link a query to the subscribers of this listener class.
|
|
49
|
+
* This method should perform actual query watching and report results via {@link updateState} method.
|
|
50
|
+
*/
|
|
51
|
+
protected abstract linkQuery(options: LinkQueryOptions<Data>): Promise<void>;
|
|
52
|
+
protected updateState(update: Partial<MutableWatchedQueryState<Data>>): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Configures base DB listeners and links the query to listeners.
|
|
55
|
+
*/
|
|
56
|
+
protected init(): Promise<void>;
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Runs a callback and reports errors to the error listeners.
|
|
60
|
+
*/
|
|
61
|
+
protected runWithReporting<T>(callback: () => Promise<T>): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Iterate listeners and reports errors to onError handlers.
|
|
64
|
+
*/
|
|
65
|
+
protected iterateAsyncListenersWithError(callback: (listener: Partial<WatchedQueryProcessorListener<Data>>) => Promise<void> | void): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js';
|
|
2
|
+
/**
|
|
3
|
+
* Performs underlying watching and yields a stream of results.
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export class AbstractQueryProcessor extends MetaBaseObserver {
|
|
7
|
+
options;
|
|
8
|
+
state;
|
|
9
|
+
abortController;
|
|
10
|
+
initialized;
|
|
11
|
+
_closed;
|
|
12
|
+
disposeListeners;
|
|
13
|
+
get closed() {
|
|
14
|
+
return this._closed;
|
|
15
|
+
}
|
|
16
|
+
constructor(options) {
|
|
17
|
+
super();
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.abortController = new AbortController();
|
|
20
|
+
this._closed = false;
|
|
21
|
+
this.state = this.constructInitialState();
|
|
22
|
+
this.disposeListeners = null;
|
|
23
|
+
this.initialized = this.init();
|
|
24
|
+
}
|
|
25
|
+
constructInitialState() {
|
|
26
|
+
return {
|
|
27
|
+
isLoading: true,
|
|
28
|
+
isFetching: this.reportFetching, // Only set to true if we will report updates in future
|
|
29
|
+
error: null,
|
|
30
|
+
lastUpdated: null,
|
|
31
|
+
data: this.options.placeholderData
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
get reportFetching() {
|
|
35
|
+
return this.options.watchOptions.reportFetching ?? true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Updates the underlying query.
|
|
39
|
+
*/
|
|
40
|
+
async updateSettings(settings) {
|
|
41
|
+
await this.initialized;
|
|
42
|
+
if (!this.state.isLoading) {
|
|
43
|
+
await this.updateState({
|
|
44
|
+
isLoading: true,
|
|
45
|
+
isFetching: this.reportFetching ? true : false
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
this.options.watchOptions = settings;
|
|
49
|
+
this.abortController.abort();
|
|
50
|
+
this.abortController = new AbortController();
|
|
51
|
+
await this.runWithReporting(() => this.linkQuery({
|
|
52
|
+
abortSignal: this.abortController.signal,
|
|
53
|
+
settings
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
async updateState(update) {
|
|
57
|
+
if (typeof update.error !== 'undefined') {
|
|
58
|
+
await this.iterateAsyncListenersWithError(async (l) => l.onError?.(update.error));
|
|
59
|
+
// An error always stops for the current fetching state
|
|
60
|
+
update.isFetching = false;
|
|
61
|
+
update.isLoading = false;
|
|
62
|
+
}
|
|
63
|
+
Object.assign(this.state, { lastUpdated: new Date() }, update);
|
|
64
|
+
if (typeof update.data !== 'undefined') {
|
|
65
|
+
await this.iterateAsyncListenersWithError(async (l) => l.onData?.(this.state.data));
|
|
66
|
+
}
|
|
67
|
+
await this.iterateAsyncListenersWithError(async (l) => l.onStateChange?.(this.state));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Configures base DB listeners and links the query to listeners.
|
|
71
|
+
*/
|
|
72
|
+
async init() {
|
|
73
|
+
const { db } = this.options;
|
|
74
|
+
const disposeCloseListener = db.registerListener({
|
|
75
|
+
closing: async () => {
|
|
76
|
+
await this.close();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Wait for the schema to be set before listening to changes
|
|
80
|
+
await db.waitForReady();
|
|
81
|
+
const disposeSchemaListener = db.registerListener({
|
|
82
|
+
schemaChanged: async () => {
|
|
83
|
+
await this.runWithReporting(async () => {
|
|
84
|
+
await this.updateSettings(this.options.watchOptions);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.disposeListeners = () => {
|
|
89
|
+
disposeCloseListener();
|
|
90
|
+
disposeSchemaListener();
|
|
91
|
+
};
|
|
92
|
+
// Initial setup
|
|
93
|
+
this.runWithReporting(async () => {
|
|
94
|
+
await this.updateSettings(this.options.watchOptions);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async close() {
|
|
98
|
+
await this.initialized;
|
|
99
|
+
this.abortController.abort();
|
|
100
|
+
this.disposeListeners?.();
|
|
101
|
+
this.disposeListeners = null;
|
|
102
|
+
this._closed = true;
|
|
103
|
+
this.iterateListeners((l) => l.closed?.());
|
|
104
|
+
this.listeners.clear();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Runs a callback and reports errors to the error listeners.
|
|
108
|
+
*/
|
|
109
|
+
async runWithReporting(callback) {
|
|
110
|
+
try {
|
|
111
|
+
await callback();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// This will update the error on the state and iterate error listeners
|
|
115
|
+
await this.updateState({ error });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Iterate listeners and reports errors to onError handlers.
|
|
120
|
+
*/
|
|
121
|
+
async iterateAsyncListenersWithError(callback) {
|
|
122
|
+
try {
|
|
123
|
+
await this.iterateAsyncListeners(async (l) => callback(l));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
await this.iterateAsyncListeners(async (l) => l.onError?.(error));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// Errors here are ignored
|
|
131
|
+
// since we are already in an error state
|
|
132
|
+
this.options.db.logger.error('Watched query error handler threw an Error', error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { WatchCompatibleQuery, WatchedQuery, WatchedQueryListener, WatchedQueryOptions, WatchedQueryState } from '../WatchedQuery.js';
|
|
2
|
+
import { AbstractQueryProcessor, AbstractQueryProcessorOptions, LinkQueryOptions } from './AbstractQueryProcessor.js';
|
|
3
|
+
/**
|
|
4
|
+
* Represents an updated row in a differential watched query.
|
|
5
|
+
* It contains both the current and previous state of the row.
|
|
6
|
+
*/
|
|
7
|
+
export interface WatchedQueryRowDifferential<RowType> {
|
|
8
|
+
readonly current: RowType;
|
|
9
|
+
readonly previous: RowType;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Represents the result of a watched query that has been diffed.
|
|
13
|
+
* {@link DifferentialWatchedQueryState#diff} is of the {@link WatchedQueryDifferential} form.
|
|
14
|
+
*/
|
|
15
|
+
export interface WatchedQueryDifferential<RowType> {
|
|
16
|
+
readonly added: ReadonlyArray<Readonly<RowType>>;
|
|
17
|
+
/**
|
|
18
|
+
* The entire current result set.
|
|
19
|
+
* Array item object references are preserved between updates if the item is unchanged.
|
|
20
|
+
*
|
|
21
|
+
* e.g. In the query
|
|
22
|
+
* ```sql
|
|
23
|
+
* SELECT name, make FROM assets ORDER BY make ASC;
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* If a previous result set contains an item (A) `{name: 'pc', make: 'Cool PC'}` and
|
|
27
|
+
* an update has been made which adds another item (B) to the result set (the item A is unchanged) - then
|
|
28
|
+
* the updated result set will be contain the same object reference, to item A, as the previous result set.
|
|
29
|
+
* This is regardless of the item A's position in the updated result set.
|
|
30
|
+
*/
|
|
31
|
+
readonly all: ReadonlyArray<Readonly<RowType>>;
|
|
32
|
+
readonly removed: ReadonlyArray<Readonly<RowType>>;
|
|
33
|
+
readonly updated: ReadonlyArray<WatchedQueryRowDifferential<Readonly<RowType>>>;
|
|
34
|
+
readonly unchanged: ReadonlyArray<Readonly<RowType>>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Differentiator for incremental watched queries which allows to identify and compare items in the result set.
|
|
38
|
+
*/
|
|
39
|
+
export interface WatchedQueryDifferentiator<RowType> {
|
|
40
|
+
/**
|
|
41
|
+
* Unique identifier for the item.
|
|
42
|
+
*/
|
|
43
|
+
identify: (item: RowType) => string;
|
|
44
|
+
/**
|
|
45
|
+
* Generates a key for comparing items with matching identifiers.
|
|
46
|
+
*/
|
|
47
|
+
compareBy: (item: RowType) => string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Options for building a differential watched query with the {@link Query} builder.
|
|
51
|
+
*/
|
|
52
|
+
export interface DifferentialWatchedQueryOptions<RowType> extends WatchedQueryOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Initial result data which is presented while the initial loading is executing.
|
|
55
|
+
*/
|
|
56
|
+
placeholderData?: RowType[];
|
|
57
|
+
/**
|
|
58
|
+
* Differentiator used to identify and compare items in the result set.
|
|
59
|
+
* If not provided, the default differentiator will be used which identifies items by their `id` property if available,
|
|
60
|
+
* otherwise it uses JSON stringification of the entire item for identification and comparison.
|
|
61
|
+
* @defaultValue {@link DEFAULT_WATCHED_QUERY_DIFFERENTIATOR}
|
|
62
|
+
*/
|
|
63
|
+
differentiator?: WatchedQueryDifferentiator<RowType>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Settings for differential incremental watched queries using.
|
|
67
|
+
*/
|
|
68
|
+
export interface DifferentialWatchedQuerySettings<RowType> extends DifferentialWatchedQueryOptions<RowType> {
|
|
69
|
+
/**
|
|
70
|
+
* The query here must return an array of items that can be differentiated.
|
|
71
|
+
*/
|
|
72
|
+
query: WatchCompatibleQuery<RowType[]>;
|
|
73
|
+
}
|
|
74
|
+
export interface DifferentialWatchedQueryState<RowType> extends WatchedQueryState<ReadonlyArray<Readonly<RowType>>> {
|
|
75
|
+
/**
|
|
76
|
+
* The difference between the current and previous result set.
|
|
77
|
+
*/
|
|
78
|
+
readonly diff: WatchedQueryDifferential<RowType>;
|
|
79
|
+
}
|
|
80
|
+
export interface DifferentialWatchedQueryListener<RowType> extends WatchedQueryListener<ReadonlyArray<Readonly<RowType>>> {
|
|
81
|
+
onDiff?: (diff: WatchedQueryDifferential<RowType>) => void | Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
export type DifferentialWatchedQuery<RowType> = WatchedQuery<ReadonlyArray<Readonly<RowType>>, DifferentialWatchedQuerySettings<RowType>, DifferentialWatchedQueryListener<RowType>>;
|
|
84
|
+
/**
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export interface DifferentialQueryProcessorOptions<RowType> extends AbstractQueryProcessorOptions<RowType[], DifferentialWatchedQuerySettings<RowType>> {
|
|
88
|
+
differentiator?: WatchedQueryDifferentiator<RowType>;
|
|
89
|
+
}
|
|
90
|
+
type DataHashMap<RowType> = Map<string, {
|
|
91
|
+
hash: string;
|
|
92
|
+
item: RowType;
|
|
93
|
+
}>;
|
|
94
|
+
/**
|
|
95
|
+
* An empty differential result set.
|
|
96
|
+
* This is used as the initial state for differential incrementally watched queries.
|
|
97
|
+
*/
|
|
98
|
+
export declare const EMPTY_DIFFERENTIAL: {
|
|
99
|
+
added: never[];
|
|
100
|
+
all: never[];
|
|
101
|
+
removed: never[];
|
|
102
|
+
updated: never[];
|
|
103
|
+
unchanged: never[];
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Default implementation of the {@link Differentiator} for watched queries.
|
|
107
|
+
* It identifies items by their `id` property if available, otherwise it uses JSON stringification
|
|
108
|
+
* of the entire item for identification and comparison.
|
|
109
|
+
*/
|
|
110
|
+
export declare const DEFAULT_WATCHED_QUERY_DIFFERENTIATOR: WatchedQueryDifferentiator<any>;
|
|
111
|
+
/**
|
|
112
|
+
* Uses the PowerSync onChange event to trigger watched queries.
|
|
113
|
+
* Results are emitted on every change of the relevant tables.
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export declare class DifferentialQueryProcessor<RowType> extends AbstractQueryProcessor<ReadonlyArray<Readonly<RowType>>, DifferentialWatchedQuerySettings<RowType>> implements DifferentialWatchedQuery<RowType> {
|
|
117
|
+
protected options: DifferentialQueryProcessorOptions<RowType>;
|
|
118
|
+
readonly state: DifferentialWatchedQueryState<RowType>;
|
|
119
|
+
protected differentiator: WatchedQueryDifferentiator<RowType>;
|
|
120
|
+
constructor(options: DifferentialQueryProcessorOptions<RowType>);
|
|
121
|
+
protected constructInitialState(): DifferentialWatchedQueryState<RowType>;
|
|
122
|
+
protected differentiate(current: RowType[], previousMap: DataHashMap<RowType>): {
|
|
123
|
+
diff: WatchedQueryDifferential<RowType>;
|
|
124
|
+
map: DataHashMap<RowType>;
|
|
125
|
+
hasChanged: boolean;
|
|
126
|
+
};
|
|
127
|
+
protected linkQuery(options: LinkQueryOptions<WatchedQueryDifferential<RowType>>): Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
export {};
|