@powersync/common 0.0.0-dev-20250715104704 → 0.0.0-dev-20250722092404
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 +58 -8
- package/lib/client/AbstractPowerSyncDatabase.js +100 -36
- package/lib/client/CustomQuery.d.ts +25 -0
- package/lib/client/CustomQuery.js +41 -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/bucket/SqliteBucketStorage.js +14 -14
- package/lib/client/sync/stream/AbstractRemote.js +31 -19
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +5 -5
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +30 -12
- 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 +121 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js +164 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js +74 -0
- package/lib/client/watched/processors/comparators.d.ts +30 -0
- package/lib/client/watched/processors/comparators.js +34 -0
- package/lib/db/schema/RawTable.d.ts +4 -0
- package/lib/db/schema/RawTable.js +4 -0
- package/lib/index.d.ts +7 -1
- package/lib/index.js +7 -1
- 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
|
@@ -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
|
-
import
|
|
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
|
*/
|
|
@@ -160,7 +160,6 @@ export declare const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
|
|
|
160
160
|
export declare const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
161
161
|
export declare const DEFAULT_STREAMING_SYNC_OPTIONS: {
|
|
162
162
|
retryDelayMs: number;
|
|
163
|
-
logger: Logger.ILogger;
|
|
164
163
|
crudUploadThrottleMs: number;
|
|
165
164
|
};
|
|
166
165
|
export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
|
|
@@ -169,8 +168,10 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
169
168
|
protected _lastSyncedAt: Date | null;
|
|
170
169
|
protected options: AbstractStreamingSyncImplementationOptions;
|
|
171
170
|
protected abortController: AbortController | null;
|
|
171
|
+
protected uploadAbortController: AbortController | null;
|
|
172
172
|
protected crudUpdateListener?: () => void;
|
|
173
173
|
protected streamingSyncPromise?: Promise<void>;
|
|
174
|
+
protected logger: ILogger;
|
|
174
175
|
private isUploadingCrud;
|
|
175
176
|
private notifyCompletedUploads?;
|
|
176
177
|
syncStatus: SyncStatus;
|
|
@@ -181,7 +182,6 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
181
182
|
waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
|
|
182
183
|
get lastSyncedAt(): Date | undefined;
|
|
183
184
|
get isConnected(): boolean;
|
|
184
|
-
protected get logger(): Logger.ILogger;
|
|
185
185
|
dispose(): Promise<void>;
|
|
186
186
|
abstract obtainLock<T>(lockOptions: LockOptions<T>): Promise<T>;
|
|
187
187
|
hasCompletedSync(): Promise<boolean>;
|
|
@@ -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';
|
|
@@ -65,7 +65,6 @@ export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
|
|
|
65
65
|
export const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
66
66
|
export const DEFAULT_STREAMING_SYNC_OPTIONS = {
|
|
67
67
|
retryDelayMs: DEFAULT_RETRY_DELAY_MS,
|
|
68
|
-
logger: Logger.get('PowerSyncStream'),
|
|
69
68
|
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
70
69
|
};
|
|
71
70
|
export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
|
|
@@ -84,8 +83,12 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
84
83
|
_lastSyncedAt;
|
|
85
84
|
options;
|
|
86
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;
|
|
87
89
|
crudUpdateListener;
|
|
88
90
|
streamingSyncPromise;
|
|
91
|
+
logger;
|
|
89
92
|
isUploadingCrud = false;
|
|
90
93
|
notifyCompletedUploads;
|
|
91
94
|
syncStatus;
|
|
@@ -93,6 +96,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
93
96
|
constructor(options) {
|
|
94
97
|
super();
|
|
95
98
|
this.options = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...options };
|
|
99
|
+
this.logger = options.logger ?? Logger.get('PowerSyncStream');
|
|
96
100
|
this.syncStatus = new SyncStatus({
|
|
97
101
|
connected: false,
|
|
98
102
|
connecting: false,
|
|
@@ -156,12 +160,11 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
156
160
|
get isConnected() {
|
|
157
161
|
return this.syncStatus.connected;
|
|
158
162
|
}
|
|
159
|
-
get logger() {
|
|
160
|
-
return this.options.logger;
|
|
161
|
-
}
|
|
162
163
|
async dispose() {
|
|
164
|
+
super.dispose();
|
|
163
165
|
this.crudUpdateListener?.();
|
|
164
166
|
this.crudUpdateListener = undefined;
|
|
167
|
+
this.uploadAbortController?.abort();
|
|
165
168
|
}
|
|
166
169
|
async hasCompletedSync() {
|
|
167
170
|
return this.options.adapter.hasCompletedSync();
|
|
@@ -170,7 +173,9 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
170
173
|
const clientId = await this.options.adapter.getClientId();
|
|
171
174
|
let path = `/write-checkpoint2.json?client_id=${clientId}`;
|
|
172
175
|
const response = await this.options.remote.get(path);
|
|
173
|
-
|
|
176
|
+
const checkpoint = response['data']['write_checkpoint'];
|
|
177
|
+
this.logger.debug(`Created write checkpoint: ${checkpoint}`);
|
|
178
|
+
return checkpoint;
|
|
174
179
|
}
|
|
175
180
|
async _uploadAllCrud() {
|
|
176
181
|
return this.obtainLock({
|
|
@@ -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.
|
|
@@ -209,7 +219,11 @@ The next upload iteration will be delayed.`);
|
|
|
209
219
|
}
|
|
210
220
|
else {
|
|
211
221
|
// Uploading is completed
|
|
212
|
-
await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
|
|
222
|
+
const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
|
|
223
|
+
if (neededUpdate == false && checkedCrudItem != null) {
|
|
224
|
+
// Only log this if there was something to upload
|
|
225
|
+
this.logger.debug('Upload complete, no write checkpoint needed.');
|
|
226
|
+
}
|
|
213
227
|
break;
|
|
214
228
|
}
|
|
215
229
|
}
|
|
@@ -221,7 +235,7 @@ The next upload iteration will be delayed.`);
|
|
|
221
235
|
uploadError: ex
|
|
222
236
|
}
|
|
223
237
|
});
|
|
224
|
-
await this.delayRetry();
|
|
238
|
+
await this.delayRetry(controller.signal);
|
|
225
239
|
if (!this.isConnected) {
|
|
226
240
|
// Exit the upload loop if the sync stream is no longer connected
|
|
227
241
|
break;
|
|
@@ -236,6 +250,7 @@ The next upload iteration will be delayed.`);
|
|
|
236
250
|
});
|
|
237
251
|
}
|
|
238
252
|
}
|
|
253
|
+
this.uploadAbortController = null;
|
|
239
254
|
}
|
|
240
255
|
});
|
|
241
256
|
}
|
|
@@ -435,6 +450,9 @@ The next upload iteration will be delayed.`);
|
|
|
435
450
|
});
|
|
436
451
|
}
|
|
437
452
|
async legacyStreamingSyncIteration(signal, resolvedOptions) {
|
|
453
|
+
if (resolvedOptions.serializedSchema?.raw_tables != null) {
|
|
454
|
+
this.logger.warn('Raw tables require the Rust-based sync client. The JS client will ignore them.');
|
|
455
|
+
}
|
|
438
456
|
this.logger.debug('Streaming sync iteration started');
|
|
439
457
|
this.options.adapter.startSession();
|
|
440
458
|
let [req, bucketMap] = await this.collectLocalBucketState();
|
|
@@ -882,17 +900,17 @@ The next upload iteration will be delayed.`);
|
|
|
882
900
|
async applyCheckpoint(checkpoint) {
|
|
883
901
|
let result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
884
902
|
if (!result.checkpointValid) {
|
|
885
|
-
this.logger.debug(
|
|
903
|
+
this.logger.debug(`Checksum mismatch in checkpoint ${checkpoint.last_op_id}, will reconnect`);
|
|
886
904
|
// This means checksums failed. Start again with a new checkpoint.
|
|
887
905
|
// TODO: better back-off
|
|
888
906
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
889
907
|
return { applied: false, endIteration: true };
|
|
890
908
|
}
|
|
891
909
|
else if (!result.ready) {
|
|
892
|
-
this.logger.debug(
|
|
910
|
+
this.logger.debug(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. We will retry applying the checkpoint after that upload is completed.`);
|
|
893
911
|
return { applied: false, endIteration: false };
|
|
894
912
|
}
|
|
895
|
-
this.logger.debug(
|
|
913
|
+
this.logger.debug(`Applied checkpoint ${checkpoint.last_op_id}`, checkpoint);
|
|
896
914
|
this.updateSyncStatus({
|
|
897
915
|
connected: true,
|
|
898
916
|
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
|
+
}
|