@powersync/common 0.0.0-dev-20250714144421 → 0.0.0-dev-20250715080712
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 +105 -35
- package/lib/client/ConnectionManager.d.ts +4 -4
- 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 +10 -4
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +26 -18
- 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/db/schema/RawTable.d.ts +57 -0
- package/lib/db/schema/RawTable.js +28 -0
- package/lib/db/schema/Schema.d.ts +14 -0
- package/lib/db/schema/Schema.js +20 -1
- 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
|
@@ -64,11 +64,11 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
64
64
|
async saveSyncData(batch, fixedKeyFormat = false) {
|
|
65
65
|
await this.writeTransaction(async (tx) => {
|
|
66
66
|
for (const b of batch.buckets) {
|
|
67
|
-
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
|
|
67
|
+
const result = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
|
|
68
68
|
'save',
|
|
69
69
|
JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
|
|
70
70
|
]);
|
|
71
|
-
this.logger.debug(
|
|
71
|
+
this.logger.debug('saveSyncData', JSON.stringify(result));
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
}
|
|
@@ -84,7 +84,7 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
84
84
|
await this.writeTransaction(async (tx) => {
|
|
85
85
|
await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
|
|
86
86
|
});
|
|
87
|
-
this.logger.debug(
|
|
87
|
+
this.logger.debug('done deleting bucket');
|
|
88
88
|
}
|
|
89
89
|
async hasCompletedSync() {
|
|
90
90
|
if (this._hasCompletedSync) {
|
|
@@ -106,12 +106,6 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
106
106
|
}
|
|
107
107
|
return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
|
|
108
108
|
}
|
|
109
|
-
if (priority == null) {
|
|
110
|
-
this.logger.debug(`Validated checksums checkpoint ${checkpoint.last_op_id}`);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
this.logger.debug(`Validated checksums for partial checkpoint ${checkpoint.last_op_id}, priority ${priority}`);
|
|
114
|
-
}
|
|
115
109
|
let buckets = checkpoint.buckets;
|
|
116
110
|
if (priority !== undefined) {
|
|
117
111
|
buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
|
|
@@ -128,6 +122,7 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
128
122
|
});
|
|
129
123
|
const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
|
|
130
124
|
if (!valid) {
|
|
125
|
+
this.logger.debug('Not at a consistent checkpoint - cannot update local db');
|
|
131
126
|
return { ready: false, checkpointValid: true };
|
|
132
127
|
}
|
|
133
128
|
return {
|
|
@@ -180,6 +175,7 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
180
175
|
JSON.stringify({ ...checkpoint })
|
|
181
176
|
]);
|
|
182
177
|
const resultItem = rs.rows?.item(0);
|
|
178
|
+
this.logger.debug('validateChecksums priority, checkpoint, result item', priority, checkpoint, resultItem);
|
|
183
179
|
if (!resultItem) {
|
|
184
180
|
return {
|
|
185
181
|
checkpointValid: false,
|
|
@@ -212,26 +208,30 @@ export class SqliteBucketStorage extends BaseObserver {
|
|
|
212
208
|
}
|
|
213
209
|
const seqBefore = rs[0]['seq'];
|
|
214
210
|
const opId = await cb();
|
|
211
|
+
this.logger.debug(`[updateLocalTarget] Updating target to checkpoint ${opId}`);
|
|
215
212
|
return this.writeTransaction(async (tx) => {
|
|
216
213
|
const anyData = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
|
|
217
214
|
if (anyData.rows?.length) {
|
|
218
215
|
// if isNotEmpty
|
|
219
|
-
this.logger.debug(
|
|
216
|
+
this.logger.debug('updateLocalTarget', 'ps crud is not empty');
|
|
220
217
|
return false;
|
|
221
218
|
}
|
|
222
219
|
const rs = await tx.execute("SELECT seq FROM sqlite_sequence WHERE name = 'ps_crud'");
|
|
223
220
|
if (!rs.rows?.length) {
|
|
224
221
|
// assert isNotEmpty
|
|
225
|
-
throw new Error('
|
|
222
|
+
throw new Error('SQlite Sequence should not be empty');
|
|
226
223
|
}
|
|
227
224
|
const seqAfter = rs.rows?.item(0)['seq'];
|
|
225
|
+
this.logger.debug('seqAfter', JSON.stringify(rs.rows?.item(0)));
|
|
228
226
|
if (seqAfter != seqBefore) {
|
|
229
|
-
this.logger.debug(
|
|
227
|
+
this.logger.debug('seqAfter != seqBefore', seqAfter, seqBefore);
|
|
230
228
|
// New crud data may have been uploaded since we got the checkpoint. Abort.
|
|
231
229
|
return false;
|
|
232
230
|
}
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
const response = await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [
|
|
232
|
+
opId
|
|
233
|
+
]);
|
|
234
|
+
this.logger.debug(['[updateLocalTarget] Response from updating target_op ', JSON.stringify(response)]);
|
|
235
235
|
return true;
|
|
236
236
|
});
|
|
237
237
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Logger, { ILogger } from 'js-logger';
|
|
2
2
|
import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
|
|
3
|
-
import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver.js';
|
|
3
|
+
import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
|
|
4
4
|
import { BucketStorageAdapter } from '../bucket/BucketStorageAdapter.js';
|
|
5
5
|
import { AbstractRemote, FetchStrategy } from './AbstractRemote.js';
|
|
6
6
|
import { StreamingSyncRequestParameterType } from './streaming-sync-types.js';
|
|
@@ -88,7 +88,8 @@ export interface StreamingSyncImplementationListener extends BaseListener {
|
|
|
88
88
|
* Configurable options to be used when connecting to the PowerSync
|
|
89
89
|
* backend instance.
|
|
90
90
|
*/
|
|
91
|
-
export
|
|
91
|
+
export type PowerSyncConnectionOptions = Omit<InternalConnectionOptions, 'serializedSchema'>;
|
|
92
|
+
export interface InternalConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {
|
|
92
93
|
}
|
|
93
94
|
/** @internal */
|
|
94
95
|
export interface BaseConnectionOptions {
|
|
@@ -114,6 +115,10 @@ export interface BaseConnectionOptions {
|
|
|
114
115
|
* These parameters are passed to the sync rules, and will be available under the`user_parameters` object.
|
|
115
116
|
*/
|
|
116
117
|
params?: Record<string, StreamingSyncRequestParameterType>;
|
|
118
|
+
/**
|
|
119
|
+
* The serialized schema - mainly used to forward information about raw tables to the sync client.
|
|
120
|
+
*/
|
|
121
|
+
serializedSchema?: any;
|
|
117
122
|
}
|
|
118
123
|
/** @internal */
|
|
119
124
|
export interface AdditionalConnectionOptions {
|
|
@@ -131,11 +136,11 @@ export interface AdditionalConnectionOptions {
|
|
|
131
136
|
}
|
|
132
137
|
/** @internal */
|
|
133
138
|
export type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>;
|
|
134
|
-
export interface StreamingSyncImplementation extends
|
|
139
|
+
export interface StreamingSyncImplementation extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
|
|
135
140
|
/**
|
|
136
141
|
* Connects to the sync service
|
|
137
142
|
*/
|
|
138
|
-
connect(options?:
|
|
143
|
+
connect(options?: InternalConnectionOptions): Promise<void>;
|
|
139
144
|
/**
|
|
140
145
|
* Disconnects from the sync services.
|
|
141
146
|
* @throws if not connected or if abort is not controlled internally
|
|
@@ -164,6 +169,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
|
|
|
164
169
|
protected _lastSyncedAt: Date | null;
|
|
165
170
|
protected options: AbstractStreamingSyncImplementationOptions;
|
|
166
171
|
protected abortController: AbortController | null;
|
|
172
|
+
protected uploadAbortController: AbortController | null;
|
|
167
173
|
protected crudUpdateListener?: () => void;
|
|
168
174
|
protected streamingSyncPromise?: Promise<void>;
|
|
169
175
|
private isUploadingCrud;
|
|
@@ -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';
|
|
@@ -72,7 +72,8 @@ export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
|
|
|
72
72
|
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
|
|
73
73
|
clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION,
|
|
74
74
|
fetchStrategy: FetchStrategy.Buffered,
|
|
75
|
-
params: {}
|
|
75
|
+
params: {},
|
|
76
|
+
serializedSchema: undefined
|
|
76
77
|
};
|
|
77
78
|
// The priority we assume when we receive checkpoint lines where no priority is set.
|
|
78
79
|
// This is the default priority used by the sync service, but can be set to an arbitrary
|
|
@@ -83,6 +84,9 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
83
84
|
_lastSyncedAt;
|
|
84
85
|
options;
|
|
85
86
|
abortController;
|
|
87
|
+
// In rare cases, mostly for tests, uploads can be triggered without being properly connected.
|
|
88
|
+
// This allows ensuring that all upload processes can be aborted.
|
|
89
|
+
uploadAbortController;
|
|
86
90
|
crudUpdateListener;
|
|
87
91
|
streamingSyncPromise;
|
|
88
92
|
isUploadingCrud = false;
|
|
@@ -159,8 +163,10 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
159
163
|
return this.options.logger;
|
|
160
164
|
}
|
|
161
165
|
async dispose() {
|
|
166
|
+
super.dispose();
|
|
162
167
|
this.crudUpdateListener?.();
|
|
163
168
|
this.crudUpdateListener = undefined;
|
|
169
|
+
this.uploadAbortController?.abort();
|
|
164
170
|
}
|
|
165
171
|
async hasCompletedSync() {
|
|
166
172
|
return this.options.adapter.hasCompletedSync();
|
|
@@ -169,9 +175,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
169
175
|
const clientId = await this.options.adapter.getClientId();
|
|
170
176
|
let path = `/write-checkpoint2.json?client_id=${clientId}`;
|
|
171
177
|
const response = await this.options.remote.get(path);
|
|
172
|
-
|
|
173
|
-
this.logger.debug(`Created write checkpoint: ${checkpoint}`);
|
|
174
|
-
return checkpoint;
|
|
178
|
+
return response['data']['write_checkpoint'];
|
|
175
179
|
}
|
|
176
180
|
async _uploadAllCrud() {
|
|
177
181
|
return this.obtainLock({
|
|
@@ -181,7 +185,12 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
181
185
|
* Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
|
|
182
186
|
*/
|
|
183
187
|
let checkedCrudItem;
|
|
184
|
-
|
|
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) {
|
|
185
194
|
try {
|
|
186
195
|
/**
|
|
187
196
|
* This is the first item in the FIFO CRUD queue.
|
|
@@ -210,11 +219,7 @@ The next upload iteration will be delayed.`);
|
|
|
210
219
|
}
|
|
211
220
|
else {
|
|
212
221
|
// 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
|
-
}
|
|
222
|
+
await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
|
|
218
223
|
break;
|
|
219
224
|
}
|
|
220
225
|
}
|
|
@@ -226,7 +231,7 @@ The next upload iteration will be delayed.`);
|
|
|
226
231
|
uploadError: ex
|
|
227
232
|
}
|
|
228
233
|
});
|
|
229
|
-
await this.delayRetry();
|
|
234
|
+
await this.delayRetry(controller.signal);
|
|
230
235
|
if (!this.isConnected) {
|
|
231
236
|
// Exit the upload loop if the sync stream is no longer connected
|
|
232
237
|
break;
|
|
@@ -241,6 +246,7 @@ The next upload iteration will be delayed.`);
|
|
|
241
246
|
});
|
|
242
247
|
}
|
|
243
248
|
}
|
|
249
|
+
this.uploadAbortController = null;
|
|
244
250
|
}
|
|
245
251
|
});
|
|
246
252
|
}
|
|
@@ -832,9 +838,11 @@ The next upload iteration will be delayed.`);
|
|
|
832
838
|
}
|
|
833
839
|
}
|
|
834
840
|
try {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
841
|
+
const options = { parameters: resolvedOptions.params };
|
|
842
|
+
if (resolvedOptions.serializedSchema) {
|
|
843
|
+
options.schema = resolvedOptions.serializedSchema;
|
|
844
|
+
}
|
|
845
|
+
await control(PowerSyncControlCommand.START, JSON.stringify(options));
|
|
838
846
|
this.notifyCompletedUploads = () => {
|
|
839
847
|
controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
840
848
|
};
|
|
@@ -885,17 +893,17 @@ The next upload iteration will be delayed.`);
|
|
|
885
893
|
async applyCheckpoint(checkpoint) {
|
|
886
894
|
let result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
887
895
|
if (!result.checkpointValid) {
|
|
888
|
-
this.logger.debug(
|
|
896
|
+
this.logger.debug('Checksum mismatch in checkpoint, will reconnect');
|
|
889
897
|
// This means checksums failed. Start again with a new checkpoint.
|
|
890
898
|
// TODO: better back-off
|
|
891
899
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
892
900
|
return { applied: false, endIteration: true };
|
|
893
901
|
}
|
|
894
902
|
else if (!result.ready) {
|
|
895
|
-
this.logger.debug(
|
|
903
|
+
this.logger.debug('Could not apply checkpoint due to local data. We will retry applying the checkpoint after that upload is completed.');
|
|
896
904
|
return { applied: false, endIteration: false };
|
|
897
905
|
}
|
|
898
|
-
this.logger.debug(
|
|
906
|
+
this.logger.debug('validated checkpoint', checkpoint);
|
|
899
907
|
this.updateSyncStatus({
|
|
900
908
|
connected: true,
|
|
901
909
|
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
|
+
}
|