@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.
Files changed (36) hide show
  1. package/dist/bundle.cjs +5 -5
  2. package/dist/bundle.mjs +3 -3
  3. package/lib/client/AbstractPowerSyncDatabase.d.ts +59 -7
  4. package/lib/client/AbstractPowerSyncDatabase.js +105 -35
  5. package/lib/client/ConnectionManager.d.ts +4 -4
  6. package/lib/client/CustomQuery.d.ts +25 -0
  7. package/lib/client/CustomQuery.js +41 -0
  8. package/lib/client/Query.d.ts +79 -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.js +14 -14
  12. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +10 -4
  13. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +26 -18
  14. package/lib/client/watched/GetAllQuery.d.ts +32 -0
  15. package/lib/client/watched/GetAllQuery.js +24 -0
  16. package/lib/client/watched/WatchedQuery.d.ts +93 -0
  17. package/lib/client/watched/WatchedQuery.js +12 -0
  18. package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
  19. package/lib/client/watched/processors/AbstractQueryProcessor.js +136 -0
  20. package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +129 -0
  21. package/lib/client/watched/processors/DifferentialQueryProcessor.js +175 -0
  22. package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +27 -0
  23. package/lib/client/watched/processors/OnChangeQueryProcessor.js +74 -0
  24. package/lib/client/watched/processors/comparators.d.ts +24 -0
  25. package/lib/client/watched/processors/comparators.js +33 -0
  26. package/lib/db/schema/RawTable.d.ts +57 -0
  27. package/lib/db/schema/RawTable.js +28 -0
  28. package/lib/db/schema/Schema.d.ts +14 -0
  29. package/lib/db/schema/Schema.js +20 -1
  30. package/lib/index.d.ts +7 -0
  31. package/lib/index.js +7 -0
  32. package/lib/utils/BaseObserver.d.ts +3 -4
  33. package/lib/utils/BaseObserver.js +3 -0
  34. package/lib/utils/MetaBaseObserver.d.ts +29 -0
  35. package/lib/utils/MetaBaseObserver.js +50 -0
  36. 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(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
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(`Done deleting bucket ${bucket}`);
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(`New data uploaded since write checkpoint ${opId} - need new write checkpoint`);
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('SQLite Sequence should not be empty');
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(`New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)`);
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
- this.logger.debug(`Updating target write checkpoint to ${opId}`);
234
- await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [opId]);
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 interface PowerSyncConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {
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 BaseObserver<StreamingSyncImplementationListener>, Disposable {
139
+ export interface StreamingSyncImplementation extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
135
140
  /**
136
141
  * Connects to the sync service
137
142
  */
138
- connect(options?: PowerSyncConnectionOptions): Promise<void>;
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
- const checkpoint = response['data']['write_checkpoint'];
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
- 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,11 +219,7 @@ The next upload iteration will be delayed.`);
210
219
  }
211
220
  else {
212
221
  // Uploading is completed
213
- const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
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
- await control(PowerSyncControlCommand.START, JSON.stringify({
836
- parameters: resolvedOptions.params
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(`Checksum mismatch in checkpoint ${checkpoint.last_op_id}, will reconnect`);
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(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. We will retry applying the checkpoint after that upload is completed.`);
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(`Applied checkpoint ${checkpoint.last_op_id}`, checkpoint);
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
+ }