@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.
Files changed (42) hide show
  1. package/dist/bundle.cjs +5 -5
  2. package/dist/bundle.mjs +3 -3
  3. package/lib/client/AbstractPowerSyncDatabase.d.ts +58 -13
  4. package/lib/client/AbstractPowerSyncDatabase.js +107 -50
  5. package/lib/client/ConnectionManager.d.ts +4 -4
  6. package/lib/client/CustomQuery.d.ts +22 -0
  7. package/lib/client/CustomQuery.js +42 -0
  8. package/lib/client/Query.d.ts +97 -0
  9. package/lib/client/Query.js +1 -0
  10. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
  11. package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -3
  12. package/lib/client/sync/bucket/SqliteBucketStorage.js +15 -17
  13. package/lib/client/sync/stream/AbstractRemote.d.ts +1 -10
  14. package/lib/client/sync/stream/AbstractRemote.js +31 -35
  15. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +13 -8
  16. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +112 -82
  17. package/lib/client/sync/stream/streaming-sync-types.d.ts +4 -0
  18. package/lib/client/watched/GetAllQuery.d.ts +32 -0
  19. package/lib/client/watched/GetAllQuery.js +24 -0
  20. package/lib/client/watched/WatchedQuery.d.ts +98 -0
  21. package/lib/client/watched/WatchedQuery.js +12 -0
  22. package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
  23. package/lib/client/watched/processors/AbstractQueryProcessor.js +135 -0
  24. package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +121 -0
  25. package/lib/client/watched/processors/DifferentialQueryProcessor.js +166 -0
  26. package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
  27. package/lib/client/watched/processors/OnChangeQueryProcessor.js +76 -0
  28. package/lib/client/watched/processors/comparators.d.ts +30 -0
  29. package/lib/client/watched/processors/comparators.js +34 -0
  30. package/lib/db/schema/RawTable.d.ts +61 -0
  31. package/lib/db/schema/RawTable.js +32 -0
  32. package/lib/db/schema/Schema.d.ts +14 -0
  33. package/lib/db/schema/Schema.js +20 -1
  34. package/lib/index.d.ts +8 -0
  35. package/lib/index.js +8 -0
  36. package/lib/utils/BaseObserver.d.ts +3 -4
  37. package/lib/utils/BaseObserver.js +3 -0
  38. package/lib/utils/MetaBaseObserver.d.ts +29 -0
  39. package/lib/utils/MetaBaseObserver.js +50 -0
  40. package/lib/utils/async.d.ts +0 -1
  41. package/lib/utils/async.js +0 -10
  42. 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 { onAbortPromise, throttleLeadingTrailing } from '../../../utils/async.js';
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
- pendingCrudUpload;
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.pendingCrudUpload != null) {
111
+ if (!this.syncStatus.connected || this.isUploadingCrud) {
107
112
  return;
108
113
  }
109
- this.pendingCrudUpload = new Promise((resolve) => {
110
- this._uploadAllCrud().finally(() => {
111
- this.notifyCompletedUploads?.();
112
- this.pendingCrudUpload = undefined;
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
- return response['data']['write_checkpoint'];
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
- while (true) {
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
- let validatedCheckpoint = null;
444
- let appliedCheckpoint = null;
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.postStream(syncOptions);
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
- stream = await this.options.remote.socketStream({
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, signal);
558
+ const result = await this.applyCheckpoint(targetCheckpoint);
505
559
  if (result.endIteration) {
506
560
  return;
507
561
  }
508
- else if (result.applied) {
509
- appliedCheckpoint = targetCheckpoint;
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('Sync complete');
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
- await control(PowerSyncControlCommand.START, JSON.stringify({
807
- parameters: resolvedOptions.params
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, abort) {
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('Checksum mismatch in checkpoint, will reconnect');
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 && pending != null) {
867
- // We have pending entries in the local upload queue or are waiting to confirm a write
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 {};