@powersync/common 1.33.2 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.cjs +5 -5
- package/dist/bundle.mjs +3 -3
- package/lib/client/AbstractPowerSyncDatabase.d.ts +58 -13
- package/lib/client/AbstractPowerSyncDatabase.js +107 -50
- package/lib/client/ConnectionManager.d.ts +4 -4
- package/lib/client/CustomQuery.d.ts +22 -0
- package/lib/client/CustomQuery.js +42 -0
- package/lib/client/Query.d.ts +97 -0
- package/lib/client/Query.js +1 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
- package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -3
- package/lib/client/sync/bucket/SqliteBucketStorage.js +15 -17
- package/lib/client/sync/stream/AbstractRemote.d.ts +1 -10
- package/lib/client/sync/stream/AbstractRemote.js +31 -35
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +13 -8
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +112 -82
- package/lib/client/sync/stream/streaming-sync-types.d.ts +4 -0
- package/lib/client/watched/GetAllQuery.d.ts +32 -0
- package/lib/client/watched/GetAllQuery.js +24 -0
- package/lib/client/watched/WatchedQuery.d.ts +98 -0
- package/lib/client/watched/WatchedQuery.js +12 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.js +135 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +121 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js +166 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js +76 -0
- package/lib/client/watched/processors/comparators.d.ts +30 -0
- package/lib/client/watched/processors/comparators.js +34 -0
- package/lib/db/schema/RawTable.d.ts +61 -0
- package/lib/db/schema/RawTable.js +32 -0
- package/lib/db/schema/Schema.d.ts +14 -0
- package/lib/db/schema/Schema.js +20 -1
- package/lib/index.d.ts +8 -0
- package/lib/index.js +8 -0
- package/lib/utils/BaseObserver.d.ts +3 -4
- package/lib/utils/BaseObserver.js +3 -0
- package/lib/utils/MetaBaseObserver.d.ts +29 -0
- package/lib/utils/MetaBaseObserver.js +50 -0
- package/lib/utils/async.d.ts +0 -1
- package/lib/utils/async.js +0 -10
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
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
|
-
import {
|
|
6
|
+
import { throttleLeadingTrailing } from '../../../utils/async.js';
|
|
7
7
|
import { PowerSyncControlCommand } from '../bucket/BucketStorageAdapter.js';
|
|
8
8
|
import { SyncDataBucket } from '../bucket/SyncDataBucket.js';
|
|
9
9
|
import { FetchStrategy } from './AbstractRemote.js';
|
|
@@ -65,14 +65,14 @@ 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 = {
|
|
72
71
|
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
|
|
73
72
|
clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION,
|
|
74
73
|
fetchStrategy: FetchStrategy.Buffered,
|
|
75
|
-
params: {}
|
|
74
|
+
params: {},
|
|
75
|
+
serializedSchema: undefined
|
|
76
76
|
};
|
|
77
77
|
// The priority we assume when we receive checkpoint lines where no priority is set.
|
|
78
78
|
// This is the default priority used by the sync service, but can be set to an arbitrary
|
|
@@ -83,15 +83,20 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
83
83
|
_lastSyncedAt;
|
|
84
84
|
options;
|
|
85
85
|
abortController;
|
|
86
|
+
// In rare cases, mostly for tests, uploads can be triggered without being properly connected.
|
|
87
|
+
// This allows ensuring that all upload processes can be aborted.
|
|
88
|
+
uploadAbortController;
|
|
86
89
|
crudUpdateListener;
|
|
87
90
|
streamingSyncPromise;
|
|
88
|
-
|
|
91
|
+
logger;
|
|
92
|
+
isUploadingCrud = false;
|
|
89
93
|
notifyCompletedUploads;
|
|
90
94
|
syncStatus;
|
|
91
95
|
triggerCrudUpload;
|
|
92
96
|
constructor(options) {
|
|
93
97
|
super();
|
|
94
98
|
this.options = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...options };
|
|
99
|
+
this.logger = options.logger ?? Logger.get('PowerSyncStream');
|
|
95
100
|
this.syncStatus = new SyncStatus({
|
|
96
101
|
connected: false,
|
|
97
102
|
connecting: false,
|
|
@@ -103,15 +108,13 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
103
108
|
});
|
|
104
109
|
this.abortController = null;
|
|
105
110
|
this.triggerCrudUpload = throttleLeadingTrailing(() => {
|
|
106
|
-
if (!this.syncStatus.connected || this.
|
|
111
|
+
if (!this.syncStatus.connected || this.isUploadingCrud) {
|
|
107
112
|
return;
|
|
108
113
|
}
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
resolve();
|
|
114
|
-
});
|
|
114
|
+
this.isUploadingCrud = true;
|
|
115
|
+
this._uploadAllCrud().finally(() => {
|
|
116
|
+
this.notifyCompletedUploads?.();
|
|
117
|
+
this.isUploadingCrud = false;
|
|
115
118
|
});
|
|
116
119
|
}, this.options.crudUploadThrottleMs);
|
|
117
120
|
}
|
|
@@ -157,12 +160,11 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
157
160
|
get isConnected() {
|
|
158
161
|
return this.syncStatus.connected;
|
|
159
162
|
}
|
|
160
|
-
get logger() {
|
|
161
|
-
return this.options.logger;
|
|
162
|
-
}
|
|
163
163
|
async dispose() {
|
|
164
|
+
super.dispose();
|
|
164
165
|
this.crudUpdateListener?.();
|
|
165
166
|
this.crudUpdateListener = undefined;
|
|
167
|
+
this.uploadAbortController?.abort();
|
|
166
168
|
}
|
|
167
169
|
async hasCompletedSync() {
|
|
168
170
|
return this.options.adapter.hasCompletedSync();
|
|
@@ -171,7 +173,9 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
|
|
|
171
173
|
const clientId = await this.options.adapter.getClientId();
|
|
172
174
|
let path = `/write-checkpoint2.json?client_id=${clientId}`;
|
|
173
175
|
const response = await this.options.remote.get(path);
|
|
174
|
-
|
|
176
|
+
const checkpoint = response['data']['write_checkpoint'];
|
|
177
|
+
this.logger.debug(`Created write checkpoint: ${checkpoint}`);
|
|
178
|
+
return 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,7 +219,11 @@ The next upload iteration will be delayed.`);
|
|
|
210
219
|
}
|
|
211
220
|
else {
|
|
212
221
|
// Uploading is completed
|
|
213
|
-
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
|
+
}
|
|
214
227
|
break;
|
|
215
228
|
}
|
|
216
229
|
}
|
|
@@ -222,7 +235,7 @@ The next upload iteration will be delayed.`);
|
|
|
222
235
|
uploadError: ex
|
|
223
236
|
}
|
|
224
237
|
});
|
|
225
|
-
await this.delayRetry();
|
|
238
|
+
await this.delayRetry(controller.signal);
|
|
226
239
|
if (!this.isConnected) {
|
|
227
240
|
// Exit the upload loop if the sync stream is no longer connected
|
|
228
241
|
break;
|
|
@@ -237,6 +250,7 @@ The next upload iteration will be delayed.`);
|
|
|
237
250
|
});
|
|
238
251
|
}
|
|
239
252
|
}
|
|
253
|
+
this.uploadAbortController = null;
|
|
240
254
|
}
|
|
241
255
|
});
|
|
242
256
|
}
|
|
@@ -361,6 +375,7 @@ The next upload iteration will be delayed.`);
|
|
|
361
375
|
});
|
|
362
376
|
}
|
|
363
377
|
finally {
|
|
378
|
+
this.notifyCompletedUploads = undefined;
|
|
364
379
|
if (!signal.aborted) {
|
|
365
380
|
nestedAbortController.abort(new AbortOperation('Closing sync stream network requests before retry.'));
|
|
366
381
|
nestedAbortController = new AbortController();
|
|
@@ -435,13 +450,16 @@ The next upload iteration will be delayed.`);
|
|
|
435
450
|
});
|
|
436
451
|
}
|
|
437
452
|
async legacyStreamingSyncIteration(signal, resolvedOptions) {
|
|
453
|
+
const rawTables = resolvedOptions.serializedSchema?.raw_tables;
|
|
454
|
+
if (rawTables != null && rawTables.length) {
|
|
455
|
+
this.logger.warn('Raw tables require the Rust-based sync client. The JS client will ignore them.');
|
|
456
|
+
}
|
|
438
457
|
this.logger.debug('Streaming sync iteration started');
|
|
439
458
|
this.options.adapter.startSession();
|
|
440
459
|
let [req, bucketMap] = await this.collectLocalBucketState();
|
|
441
|
-
// These are compared by reference
|
|
442
460
|
let targetCheckpoint = null;
|
|
443
|
-
|
|
444
|
-
let
|
|
461
|
+
// A checkpoint that has been validated but not applied (e.g. due to pending local writes)
|
|
462
|
+
let pendingValidatedCheckpoint = null;
|
|
445
463
|
const clientId = await this.options.adapter.getClientId();
|
|
446
464
|
const usingFixedKeyFormat = await this.requireKeyFormat(false);
|
|
447
465
|
this.logger.debug('Requesting stream from server');
|
|
@@ -458,21 +476,55 @@ The next upload iteration will be delayed.`);
|
|
|
458
476
|
};
|
|
459
477
|
let stream;
|
|
460
478
|
if (resolvedOptions?.connectionMethod == SyncStreamConnectionMethod.HTTP) {
|
|
461
|
-
stream = await this.options.remote.
|
|
479
|
+
stream = await this.options.remote.postStreamRaw(syncOptions, (line) => {
|
|
480
|
+
if (typeof line == 'string') {
|
|
481
|
+
return JSON.parse(line);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
// Directly enqueued by us
|
|
485
|
+
return line;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
462
488
|
}
|
|
463
489
|
else {
|
|
464
|
-
|
|
490
|
+
const bson = await this.options.remote.getBSON();
|
|
491
|
+
stream = await this.options.remote.socketStreamRaw({
|
|
465
492
|
...syncOptions,
|
|
466
493
|
...{ fetchStrategy: resolvedOptions.fetchStrategy }
|
|
467
|
-
})
|
|
494
|
+
}, (payload) => {
|
|
495
|
+
if (payload instanceof Uint8Array) {
|
|
496
|
+
return bson.deserialize(payload);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
// Directly enqueued by us
|
|
500
|
+
return payload;
|
|
501
|
+
}
|
|
502
|
+
}, bson);
|
|
468
503
|
}
|
|
469
504
|
this.logger.debug('Stream established. Processing events');
|
|
505
|
+
this.notifyCompletedUploads = () => {
|
|
506
|
+
if (!stream.closed) {
|
|
507
|
+
stream.enqueueData({ crud_upload_completed: null });
|
|
508
|
+
}
|
|
509
|
+
};
|
|
470
510
|
while (!stream.closed) {
|
|
471
511
|
const line = await stream.read();
|
|
472
512
|
if (!line) {
|
|
473
513
|
// The stream has closed while waiting
|
|
474
514
|
return;
|
|
475
515
|
}
|
|
516
|
+
if ('crud_upload_completed' in line) {
|
|
517
|
+
if (pendingValidatedCheckpoint != null) {
|
|
518
|
+
const { applied, endIteration } = await this.applyCheckpoint(pendingValidatedCheckpoint);
|
|
519
|
+
if (applied) {
|
|
520
|
+
pendingValidatedCheckpoint = null;
|
|
521
|
+
}
|
|
522
|
+
else if (endIteration) {
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
476
528
|
// A connection is active and messages are being received
|
|
477
529
|
if (!this.syncStatus.connected) {
|
|
478
530
|
// There is a connection now
|
|
@@ -483,6 +535,8 @@ The next upload iteration will be delayed.`);
|
|
|
483
535
|
}
|
|
484
536
|
if (isStreamingSyncCheckpoint(line)) {
|
|
485
537
|
targetCheckpoint = line.checkpoint;
|
|
538
|
+
// New checkpoint - existing validated checkpoint is no longer valid
|
|
539
|
+
pendingValidatedCheckpoint = null;
|
|
486
540
|
const bucketsToDelete = new Set(bucketMap.keys());
|
|
487
541
|
const newBuckets = new Map();
|
|
488
542
|
for (const checksum of line.checkpoint.buckets) {
|
|
@@ -501,14 +555,20 @@ The next upload iteration will be delayed.`);
|
|
|
501
555
|
await this.updateSyncStatusForStartingCheckpoint(targetCheckpoint);
|
|
502
556
|
}
|
|
503
557
|
else if (isStreamingSyncCheckpointComplete(line)) {
|
|
504
|
-
const result = await this.applyCheckpoint(targetCheckpoint
|
|
558
|
+
const result = await this.applyCheckpoint(targetCheckpoint);
|
|
505
559
|
if (result.endIteration) {
|
|
506
560
|
return;
|
|
507
561
|
}
|
|
508
|
-
else if (result.applied) {
|
|
509
|
-
|
|
562
|
+
else if (!result.applied) {
|
|
563
|
+
// "Could not apply checkpoint due to local data". We need to retry after
|
|
564
|
+
// finishing uploads.
|
|
565
|
+
pendingValidatedCheckpoint = targetCheckpoint;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
// Nothing to retry later. This would likely already be null from the last
|
|
569
|
+
// checksum or checksum_diff operation, but we make sure.
|
|
570
|
+
pendingValidatedCheckpoint = null;
|
|
510
571
|
}
|
|
511
|
-
validatedCheckpoint = targetCheckpoint;
|
|
512
572
|
}
|
|
513
573
|
else if (isStreamingSyncCheckpointPartiallyComplete(line)) {
|
|
514
574
|
const priority = line.partial_checkpoint_complete.priority;
|
|
@@ -545,6 +605,8 @@ The next upload iteration will be delayed.`);
|
|
|
545
605
|
if (targetCheckpoint == null) {
|
|
546
606
|
throw new Error('Checkpoint diff without previous checkpoint');
|
|
547
607
|
}
|
|
608
|
+
// New checkpoint - existing validated checkpoint is no longer valid
|
|
609
|
+
pendingValidatedCheckpoint = null;
|
|
548
610
|
const diff = line.checkpoint_diff;
|
|
549
611
|
const newBuckets = new Map();
|
|
550
612
|
for (const checksum of targetCheckpoint.buckets) {
|
|
@@ -618,26 +680,7 @@ The next upload iteration will be delayed.`);
|
|
|
618
680
|
this.triggerCrudUpload();
|
|
619
681
|
}
|
|
620
682
|
else {
|
|
621
|
-
this.logger.debug('
|
|
622
|
-
if (targetCheckpoint === appliedCheckpoint) {
|
|
623
|
-
this.updateSyncStatus({
|
|
624
|
-
connected: true,
|
|
625
|
-
lastSyncedAt: new Date(),
|
|
626
|
-
priorityStatusEntries: [],
|
|
627
|
-
dataFlow: {
|
|
628
|
-
downloadError: undefined
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
else if (validatedCheckpoint === targetCheckpoint) {
|
|
633
|
-
const result = await this.applyCheckpoint(targetCheckpoint, signal);
|
|
634
|
-
if (result.endIteration) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
else if (result.applied) {
|
|
638
|
-
appliedCheckpoint = targetCheckpoint;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
683
|
+
this.logger.debug('Received unknown sync line', line);
|
|
641
684
|
}
|
|
642
685
|
}
|
|
643
686
|
this.logger.debug('Stream input empty');
|
|
@@ -803,9 +846,11 @@ The next upload iteration will be delayed.`);
|
|
|
803
846
|
}
|
|
804
847
|
}
|
|
805
848
|
try {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
849
|
+
const options = { parameters: resolvedOptions.params };
|
|
850
|
+
if (resolvedOptions.serializedSchema) {
|
|
851
|
+
options.schema = resolvedOptions.serializedSchema;
|
|
852
|
+
}
|
|
853
|
+
await control(PowerSyncControlCommand.START, JSON.stringify(options));
|
|
809
854
|
this.notifyCompletedUploads = () => {
|
|
810
855
|
controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
|
|
811
856
|
};
|
|
@@ -853,45 +898,30 @@ The next upload iteration will be delayed.`);
|
|
|
853
898
|
}
|
|
854
899
|
});
|
|
855
900
|
}
|
|
856
|
-
async applyCheckpoint(checkpoint
|
|
901
|
+
async applyCheckpoint(checkpoint) {
|
|
857
902
|
let result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
858
|
-
const pending = this.pendingCrudUpload;
|
|
859
903
|
if (!result.checkpointValid) {
|
|
860
|
-
this.logger.debug(
|
|
904
|
+
this.logger.debug(`Checksum mismatch in checkpoint ${checkpoint.last_op_id}, will reconnect`);
|
|
861
905
|
// This means checksums failed. Start again with a new checkpoint.
|
|
862
906
|
// TODO: better back-off
|
|
863
907
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
864
908
|
return { applied: false, endIteration: true };
|
|
865
909
|
}
|
|
866
|
-
else if (!result.ready
|
|
867
|
-
|
|
868
|
-
// checkpoint, which prevented this checkpoint from applying. Wait for that to complete and
|
|
869
|
-
// try again.
|
|
870
|
-
this.logger.debug('Could not apply checkpoint due to local data. Waiting for in-progress upload before retrying.');
|
|
871
|
-
await Promise.race([pending, onAbortPromise(abort)]);
|
|
872
|
-
if (abort.aborted) {
|
|
873
|
-
return { applied: false, endIteration: true };
|
|
874
|
-
}
|
|
875
|
-
// Try again now that uploads have completed.
|
|
876
|
-
result = await this.options.adapter.syncLocalDatabase(checkpoint);
|
|
877
|
-
}
|
|
878
|
-
if (result.checkpointValid && result.ready) {
|
|
879
|
-
this.logger.debug('validated checkpoint', checkpoint);
|
|
880
|
-
this.updateSyncStatus({
|
|
881
|
-
connected: true,
|
|
882
|
-
lastSyncedAt: new Date(),
|
|
883
|
-
dataFlow: {
|
|
884
|
-
downloading: false,
|
|
885
|
-
downloadProgress: null,
|
|
886
|
-
downloadError: undefined
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
return { applied: true, endIteration: false };
|
|
890
|
-
}
|
|
891
|
-
else {
|
|
892
|
-
this.logger.debug('Could not apply checkpoint. Waiting for next sync complete line.');
|
|
910
|
+
else if (!result.ready) {
|
|
911
|
+
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
912
|
return { applied: false, endIteration: false };
|
|
894
913
|
}
|
|
914
|
+
this.logger.debug(`Applied checkpoint ${checkpoint.last_op_id}`, checkpoint);
|
|
915
|
+
this.updateSyncStatus({
|
|
916
|
+
connected: true,
|
|
917
|
+
lastSyncedAt: new Date(),
|
|
918
|
+
dataFlow: {
|
|
919
|
+
downloading: false,
|
|
920
|
+
downloadProgress: null,
|
|
921
|
+
downloadError: undefined
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
return { applied: true, endIteration: false };
|
|
895
925
|
}
|
|
896
926
|
updateSyncStatus(options) {
|
|
897
927
|
const updatedStatus = new SyncStatus({
|
|
@@ -101,6 +101,10 @@ export interface StreamingSyncKeepalive {
|
|
|
101
101
|
token_expires_in: number;
|
|
102
102
|
}
|
|
103
103
|
export type StreamingSyncLine = StreamingSyncDataJSON | StreamingSyncCheckpoint | StreamingSyncCheckpointDiff | StreamingSyncCheckpointComplete | StreamingSyncCheckpointPartiallyComplete | StreamingSyncKeepalive;
|
|
104
|
+
export type CrudUploadNotification = {
|
|
105
|
+
crud_upload_completed: null;
|
|
106
|
+
};
|
|
107
|
+
export type StreamingSyncLineOrCrudUploadComplete = StreamingSyncLine | CrudUploadNotification;
|
|
104
108
|
export interface BucketRequest {
|
|
105
109
|
name: string;
|
|
106
110
|
/**
|
|
@@ -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,98 @@
|
|
|
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
|
+
* By default, watched queries requery the database on any change to any dependent table of the query.
|
|
60
|
+
* Supplying an override here can be used to limit the tables which trigger querying the database.
|
|
61
|
+
*/
|
|
62
|
+
triggerOnTables?: string[];
|
|
63
|
+
}
|
|
64
|
+
export declare enum WatchedQueryListenerEvent {
|
|
65
|
+
ON_DATA = "onData",
|
|
66
|
+
ON_ERROR = "onError",
|
|
67
|
+
ON_STATE_CHANGE = "onStateChange",
|
|
68
|
+
CLOSED = "closed"
|
|
69
|
+
}
|
|
70
|
+
export interface WatchedQueryListener<Data> extends BaseListener {
|
|
71
|
+
[WatchedQueryListenerEvent.ON_DATA]?: (data: Data) => void | Promise<void>;
|
|
72
|
+
[WatchedQueryListenerEvent.ON_ERROR]?: (error: Error) => void | Promise<void>;
|
|
73
|
+
[WatchedQueryListenerEvent.ON_STATE_CHANGE]?: (state: WatchedQueryState<Data>) => void | Promise<void>;
|
|
74
|
+
[WatchedQueryListenerEvent.CLOSED]?: () => void | Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
|
|
77
|
+
export declare const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions;
|
|
78
|
+
export interface WatchedQuery<Data = unknown, Settings extends WatchedQueryOptions = WatchedQueryOptions, Listener extends WatchedQueryListener<Data> = WatchedQueryListener<Data>> extends MetaBaseObserverInterface<Listener> {
|
|
79
|
+
/**
|
|
80
|
+
* Current state of the watched query.
|
|
81
|
+
*/
|
|
82
|
+
readonly state: WatchedQueryState<Data>;
|
|
83
|
+
readonly closed: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Subscribe to watched query events.
|
|
86
|
+
* @returns A function to unsubscribe from the events.
|
|
87
|
+
*/
|
|
88
|
+
registerListener(listener: Listener): () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Updates the underlying query options.
|
|
91
|
+
* This will trigger a re-evaluation of the query and update the state.
|
|
92
|
+
*/
|
|
93
|
+
updateSettings(options: Settings): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Close the watched query and end all subscriptions.
|
|
96
|
+
*/
|
|
97
|
+
close(): Promise<void>;
|
|
98
|
+
}
|
|
@@ -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 {};
|