@powersync/common 0.0.0-dev-20250715104704 → 0.0.0-dev-20250722092404

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/bundle.cjs +5 -5
  2. package/dist/bundle.mjs +3 -3
  3. package/lib/client/AbstractPowerSyncDatabase.d.ts +58 -8
  4. package/lib/client/AbstractPowerSyncDatabase.js +100 -36
  5. package/lib/client/CustomQuery.d.ts +25 -0
  6. package/lib/client/CustomQuery.js +41 -0
  7. package/lib/client/Query.d.ts +97 -0
  8. package/lib/client/Query.js +1 -0
  9. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
  10. package/lib/client/sync/bucket/SqliteBucketStorage.js +14 -14
  11. package/lib/client/sync/stream/AbstractRemote.js +31 -19
  12. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +5 -5
  13. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +30 -12
  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 +121 -0
  21. package/lib/client/watched/processors/DifferentialQueryProcessor.js +164 -0
  22. package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
  23. package/lib/client/watched/processors/OnChangeQueryProcessor.js +74 -0
  24. package/lib/client/watched/processors/comparators.d.ts +30 -0
  25. package/lib/client/watched/processors/comparators.js +34 -0
  26. package/lib/db/schema/RawTable.d.ts +4 -0
  27. package/lib/db/schema/RawTable.js +4 -0
  28. package/lib/index.d.ts +7 -1
  29. package/lib/index.js +7 -1
  30. package/lib/utils/BaseObserver.d.ts +3 -4
  31. package/lib/utils/BaseObserver.js +3 -0
  32. package/lib/utils/MetaBaseObserver.d.ts +29 -0
  33. package/lib/utils/MetaBaseObserver.js +50 -0
  34. package/package.json +1 -1
@@ -204,6 +204,22 @@ export class AbstractRemote {
204
204
  // headers with websockets on web. The browser userAgent is however added
205
205
  // automatically as a header.
206
206
  const userAgent = this.getUserAgent();
207
+ const stream = new DataStream({
208
+ logger: this.logger,
209
+ pressure: {
210
+ lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
211
+ },
212
+ mapLine: map
213
+ });
214
+ // Handle upstream abort
215
+ if (options.abortSignal?.aborted) {
216
+ throw new AbortOperation('Connection request aborted');
217
+ }
218
+ else {
219
+ options.abortSignal?.addEventListener('abort', () => {
220
+ stream.close();
221
+ }, { once: true });
222
+ }
207
223
  let keepAliveTimeout;
208
224
  const resetTimeout = () => {
209
225
  clearTimeout(keepAliveTimeout);
@@ -213,12 +229,22 @@ export class AbstractRemote {
213
229
  }, SOCKET_TIMEOUT_MS);
214
230
  };
215
231
  resetTimeout();
232
+ // Typescript complains about this being `never` if it's not assigned here.
233
+ // This is assigned in `wsCreator`.
234
+ let disposeSocketConnectionTimeout = () => { };
216
235
  const url = this.options.socketUrlTransformer(request.url);
217
236
  const connector = new RSocketConnector({
218
237
  transport: new WebsocketClientTransport({
219
238
  url,
220
239
  wsCreator: (url) => {
221
240
  const socket = this.createSocket(url);
241
+ disposeSocketConnectionTimeout = stream.registerListener({
242
+ closed: () => {
243
+ // Allow closing the underlying WebSocket if the stream was closed before the
244
+ // RSocket connect completed. This should effectively abort the request.
245
+ socket.close();
246
+ }
247
+ });
222
248
  socket.addEventListener('message', (event) => {
223
249
  resetTimeout();
224
250
  });
@@ -242,20 +268,18 @@ export class AbstractRemote {
242
268
  let rsocket;
243
269
  try {
244
270
  rsocket = await connector.connect();
271
+ // The connection is established, we no longer need to monitor the initial timeout
272
+ disposeSocketConnectionTimeout();
245
273
  }
246
274
  catch (ex) {
247
275
  this.logger.error(`Failed to connect WebSocket`, ex);
248
276
  clearTimeout(keepAliveTimeout);
277
+ if (!stream.closed) {
278
+ await stream.close();
279
+ }
249
280
  throw ex;
250
281
  }
251
282
  resetTimeout();
252
- const stream = new DataStream({
253
- logger: this.logger,
254
- pressure: {
255
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
256
- },
257
- mapLine: map
258
- });
259
283
  let socketIsClosed = false;
260
284
  const closeSocket = () => {
261
285
  clearTimeout(keepAliveTimeout);
@@ -341,18 +365,6 @@ export class AbstractRemote {
341
365
  l();
342
366
  }
343
367
  });
344
- /**
345
- * Handle abort operations here.
346
- * Unfortunately cannot insert them into the connection.
347
- */
348
- if (options.abortSignal?.aborted) {
349
- stream.close();
350
- }
351
- else {
352
- options.abortSignal?.addEventListener('abort', () => {
353
- stream.close();
354
- });
355
- }
356
368
  return stream;
357
369
  }
358
370
  /**
@@ -1,6 +1,6 @@
1
- import Logger, { ILogger } from 'js-logger';
1
+ import { ILogger } from 'js-logger';
2
2
  import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
3
- import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver.js';
3
+ import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
4
4
  import { BucketStorageAdapter } from '../bucket/BucketStorageAdapter.js';
5
5
  import { AbstractRemote, FetchStrategy } from './AbstractRemote.js';
6
6
  import { StreamingSyncRequestParameterType } from './streaming-sync-types.js';
@@ -136,7 +136,7 @@ export interface AdditionalConnectionOptions {
136
136
  }
137
137
  /** @internal */
138
138
  export type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>;
139
- export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener>, Disposable {
139
+ export interface StreamingSyncImplementation extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
140
140
  /**
141
141
  * Connects to the sync service
142
142
  */
@@ -160,7 +160,6 @@ export declare const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
160
160
  export declare const DEFAULT_RETRY_DELAY_MS = 5000;
161
161
  export declare const DEFAULT_STREAMING_SYNC_OPTIONS: {
162
162
  retryDelayMs: number;
163
- logger: Logger.ILogger;
164
163
  crudUploadThrottleMs: number;
165
164
  };
166
165
  export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
@@ -169,8 +168,10 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
169
168
  protected _lastSyncedAt: Date | null;
170
169
  protected options: AbstractStreamingSyncImplementationOptions;
171
170
  protected abortController: AbortController | null;
171
+ protected uploadAbortController: AbortController | null;
172
172
  protected crudUpdateListener?: () => void;
173
173
  protected streamingSyncPromise?: Promise<void>;
174
+ protected logger: ILogger;
174
175
  private isUploadingCrud;
175
176
  private notifyCompletedUploads?;
176
177
  syncStatus: SyncStatus;
@@ -181,7 +182,6 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
181
182
  waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
182
183
  get lastSyncedAt(): Date | undefined;
183
184
  get isConnected(): boolean;
184
- protected get logger(): Logger.ILogger;
185
185
  dispose(): Promise<void>;
186
186
  abstract obtainLock<T>(lockOptions: LockOptions<T>): Promise<T>;
187
187
  hasCompletedSync(): Promise<boolean>;
@@ -1,6 +1,6 @@
1
1
  import Logger from 'js-logger';
2
- import { SyncStatus } from '../../../db/crud/SyncStatus.js';
3
2
  import { FULL_SYNC_PRIORITY } from '../../../db/crud/SyncProgress.js';
3
+ import { SyncStatus } from '../../../db/crud/SyncStatus.js';
4
4
  import { AbortOperation } from '../../../utils/AbortOperation.js';
5
5
  import { BaseObserver } from '../../../utils/BaseObserver.js';
6
6
  import { throttleLeadingTrailing } from '../../../utils/async.js';
@@ -65,7 +65,6 @@ export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
65
65
  export const DEFAULT_RETRY_DELAY_MS = 5000;
66
66
  export const DEFAULT_STREAMING_SYNC_OPTIONS = {
67
67
  retryDelayMs: DEFAULT_RETRY_DELAY_MS,
68
- logger: Logger.get('PowerSyncStream'),
69
68
  crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
70
69
  };
71
70
  export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
@@ -84,8 +83,12 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
84
83
  _lastSyncedAt;
85
84
  options;
86
85
  abortController;
86
+ // In rare cases, mostly for tests, uploads can be triggered without being properly connected.
87
+ // This allows ensuring that all upload processes can be aborted.
88
+ uploadAbortController;
87
89
  crudUpdateListener;
88
90
  streamingSyncPromise;
91
+ logger;
89
92
  isUploadingCrud = false;
90
93
  notifyCompletedUploads;
91
94
  syncStatus;
@@ -93,6 +96,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
93
96
  constructor(options) {
94
97
  super();
95
98
  this.options = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...options };
99
+ this.logger = options.logger ?? Logger.get('PowerSyncStream');
96
100
  this.syncStatus = new SyncStatus({
97
101
  connected: false,
98
102
  connecting: false,
@@ -156,12 +160,11 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
156
160
  get isConnected() {
157
161
  return this.syncStatus.connected;
158
162
  }
159
- get logger() {
160
- return this.options.logger;
161
- }
162
163
  async dispose() {
164
+ super.dispose();
163
165
  this.crudUpdateListener?.();
164
166
  this.crudUpdateListener = undefined;
167
+ this.uploadAbortController?.abort();
165
168
  }
166
169
  async hasCompletedSync() {
167
170
  return this.options.adapter.hasCompletedSync();
@@ -170,7 +173,9 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
170
173
  const clientId = await this.options.adapter.getClientId();
171
174
  let path = `/write-checkpoint2.json?client_id=${clientId}`;
172
175
  const response = await this.options.remote.get(path);
173
- return response['data']['write_checkpoint'];
176
+ const checkpoint = response['data']['write_checkpoint'];
177
+ this.logger.debug(`Created write checkpoint: ${checkpoint}`);
178
+ return checkpoint;
174
179
  }
175
180
  async _uploadAllCrud() {
176
181
  return this.obtainLock({
@@ -180,7 +185,12 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
180
185
  * Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
181
186
  */
182
187
  let checkedCrudItem;
183
- 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) {
184
194
  try {
185
195
  /**
186
196
  * This is the first item in the FIFO CRUD queue.
@@ -209,7 +219,11 @@ The next upload iteration will be delayed.`);
209
219
  }
210
220
  else {
211
221
  // Uploading is completed
212
- await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
222
+ const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
223
+ if (neededUpdate == false && checkedCrudItem != null) {
224
+ // Only log this if there was something to upload
225
+ this.logger.debug('Upload complete, no write checkpoint needed.');
226
+ }
213
227
  break;
214
228
  }
215
229
  }
@@ -221,7 +235,7 @@ The next upload iteration will be delayed.`);
221
235
  uploadError: ex
222
236
  }
223
237
  });
224
- await this.delayRetry();
238
+ await this.delayRetry(controller.signal);
225
239
  if (!this.isConnected) {
226
240
  // Exit the upload loop if the sync stream is no longer connected
227
241
  break;
@@ -236,6 +250,7 @@ The next upload iteration will be delayed.`);
236
250
  });
237
251
  }
238
252
  }
253
+ this.uploadAbortController = null;
239
254
  }
240
255
  });
241
256
  }
@@ -435,6 +450,9 @@ The next upload iteration will be delayed.`);
435
450
  });
436
451
  }
437
452
  async legacyStreamingSyncIteration(signal, resolvedOptions) {
453
+ if (resolvedOptions.serializedSchema?.raw_tables != null) {
454
+ this.logger.warn('Raw tables require the Rust-based sync client. The JS client will ignore them.');
455
+ }
438
456
  this.logger.debug('Streaming sync iteration started');
439
457
  this.options.adapter.startSession();
440
458
  let [req, bucketMap] = await this.collectLocalBucketState();
@@ -882,17 +900,17 @@ The next upload iteration will be delayed.`);
882
900
  async applyCheckpoint(checkpoint) {
883
901
  let result = await this.options.adapter.syncLocalDatabase(checkpoint);
884
902
  if (!result.checkpointValid) {
885
- this.logger.debug('Checksum mismatch in checkpoint, will reconnect');
903
+ this.logger.debug(`Checksum mismatch in checkpoint ${checkpoint.last_op_id}, will reconnect`);
886
904
  // This means checksums failed. Start again with a new checkpoint.
887
905
  // TODO: better back-off
888
906
  await new Promise((resolve) => setTimeout(resolve, 50));
889
907
  return { applied: false, endIteration: true };
890
908
  }
891
909
  else if (!result.ready) {
892
- this.logger.debug('Could not apply checkpoint due to local data. We will retry applying the checkpoint after that upload is completed.');
910
+ this.logger.debug(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. We will retry applying the checkpoint after that upload is completed.`);
893
911
  return { applied: false, endIteration: false };
894
912
  }
895
- this.logger.debug('validated checkpoint', checkpoint);
913
+ this.logger.debug(`Applied checkpoint ${checkpoint.last_op_id}`, checkpoint);
896
914
  this.updateSyncStatus({
897
915
  connected: true,
898
916
  lastSyncedAt: new Date(),
@@ -0,0 +1,32 @@
1
+ import { CompiledQuery } from '../../types/types.js';
2
+ import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
3
+ import { WatchCompatibleQuery } from './WatchedQuery.js';
4
+ /**
5
+ * Options for {@link GetAllQuery}.
6
+ */
7
+ export type GetAllQueryOptions<RowType = unknown> = {
8
+ sql: string;
9
+ parameters?: ReadonlyArray<unknown>;
10
+ /**
11
+ * Optional mapper function to convert raw rows into the desired RowType.
12
+ * @example
13
+ * ```javascript
14
+ * (rawRow) => ({
15
+ * id: rawRow.id,
16
+ * created_at: new Date(rawRow.created_at),
17
+ * })
18
+ * ```
19
+ */
20
+ mapper?: (rawRow: Record<string, unknown>) => RowType;
21
+ };
22
+ /**
23
+ * Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query.
24
+ */
25
+ export declare class GetAllQuery<RowType = unknown> implements WatchCompatibleQuery<RowType[]> {
26
+ protected options: GetAllQueryOptions<RowType>;
27
+ constructor(options: GetAllQueryOptions<RowType>);
28
+ compile(): CompiledQuery;
29
+ execute(options: {
30
+ db: AbstractPowerSyncDatabase;
31
+ }): Promise<RowType[]>;
32
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query.
3
+ */
4
+ export class GetAllQuery {
5
+ options;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ compile() {
10
+ return {
11
+ sql: this.options.sql,
12
+ parameters: this.options.parameters ?? []
13
+ };
14
+ }
15
+ async execute(options) {
16
+ const { db } = options;
17
+ const { sql, parameters = [] } = this.compile();
18
+ const rawResult = await db.getAll(sql, [...parameters]);
19
+ if (this.options.mapper) {
20
+ return rawResult.map(this.options.mapper);
21
+ }
22
+ return rawResult;
23
+ }
24
+ }
@@ -0,0 +1,93 @@
1
+ import { CompiledQuery } from '../../types/types.js';
2
+ import { BaseListener } from '../../utils/BaseObserver.js';
3
+ import { MetaBaseObserverInterface } from '../../utils/MetaBaseObserver.js';
4
+ import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
5
+ /**
6
+ * State for {@link WatchedQuery} instances.
7
+ */
8
+ export interface WatchedQueryState<Data> {
9
+ /**
10
+ * Indicates the initial loading state (hard loading).
11
+ * Loading becomes false once the first set of results from the watched query is available or an error occurs.
12
+ */
13
+ readonly isLoading: boolean;
14
+ /**
15
+ * Indicates whether the query is currently fetching data, is true during the initial load
16
+ * and any time when the query is re-evaluating (useful for large queries).
17
+ */
18
+ readonly isFetching: boolean;
19
+ /**
20
+ * The last error that occurred while executing the query.
21
+ */
22
+ readonly error: Error | null;
23
+ /**
24
+ * The last time the query was updated.
25
+ */
26
+ readonly lastUpdated: Date | null;
27
+ /**
28
+ * The last data returned by the query.
29
+ */
30
+ readonly data: Data;
31
+ }
32
+ /**
33
+ * Options provided to the `execute` method of a {@link WatchCompatibleQuery}.
34
+ */
35
+ export interface WatchExecuteOptions {
36
+ sql: string;
37
+ parameters: any[];
38
+ db: AbstractPowerSyncDatabase;
39
+ }
40
+ /**
41
+ * Similar to {@link CompatibleQuery}, except the `execute` method
42
+ * does not enforce an Array result type.
43
+ */
44
+ export interface WatchCompatibleQuery<ResultType> {
45
+ execute(options: WatchExecuteOptions): Promise<ResultType>;
46
+ compile(): CompiledQuery;
47
+ }
48
+ export interface WatchedQueryOptions {
49
+ /** The minimum interval between queries. */
50
+ throttleMs?: number;
51
+ /**
52
+ * If true (default) the watched query will update its state to report
53
+ * on the fetching state of the query.
54
+ * Setting to false reduces the number of state changes if the fetch status
55
+ * is not relevant to the consumer.
56
+ */
57
+ reportFetching?: boolean;
58
+ }
59
+ export declare enum WatchedQueryListenerEvent {
60
+ ON_DATA = "onData",
61
+ ON_ERROR = "onError",
62
+ ON_STATE_CHANGE = "onStateChange",
63
+ CLOSED = "closed"
64
+ }
65
+ export interface WatchedQueryListener<Data> extends BaseListener {
66
+ [WatchedQueryListenerEvent.ON_DATA]?: (data: Data) => void | Promise<void>;
67
+ [WatchedQueryListenerEvent.ON_ERROR]?: (error: Error) => void | Promise<void>;
68
+ [WatchedQueryListenerEvent.ON_STATE_CHANGE]?: (state: WatchedQueryState<Data>) => void | Promise<void>;
69
+ [WatchedQueryListenerEvent.CLOSED]?: () => void | Promise<void>;
70
+ }
71
+ export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
72
+ export declare const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions;
73
+ export interface WatchedQuery<Data = unknown, Settings extends WatchedQueryOptions = WatchedQueryOptions, Listener extends WatchedQueryListener<Data> = WatchedQueryListener<Data>> extends MetaBaseObserverInterface<Listener> {
74
+ /**
75
+ * Current state of the watched query.
76
+ */
77
+ readonly state: WatchedQueryState<Data>;
78
+ readonly closed: boolean;
79
+ /**
80
+ * Subscribe to watched query events.
81
+ * @returns A function to unsubscribe from the events.
82
+ */
83
+ registerListener(listener: Listener): () => void;
84
+ /**
85
+ * Updates the underlying query options.
86
+ * This will trigger a re-evaluation of the query and update the state.
87
+ */
88
+ updateSettings(options: Settings): Promise<void>;
89
+ /**
90
+ * Close the watched query and end all subscriptions.
91
+ */
92
+ close(): Promise<void>;
93
+ }
@@ -0,0 +1,12 @@
1
+ export var WatchedQueryListenerEvent;
2
+ (function (WatchedQueryListenerEvent) {
3
+ WatchedQueryListenerEvent["ON_DATA"] = "onData";
4
+ WatchedQueryListenerEvent["ON_ERROR"] = "onError";
5
+ WatchedQueryListenerEvent["ON_STATE_CHANGE"] = "onStateChange";
6
+ WatchedQueryListenerEvent["CLOSED"] = "closed";
7
+ })(WatchedQueryListenerEvent || (WatchedQueryListenerEvent = {}));
8
+ export const DEFAULT_WATCH_THROTTLE_MS = 30;
9
+ export const DEFAULT_WATCH_QUERY_OPTIONS = {
10
+ throttleMs: DEFAULT_WATCH_THROTTLE_MS,
11
+ reportFetching: true
12
+ };
@@ -0,0 +1,67 @@
1
+ import { AbstractPowerSyncDatabase } from '../../../client/AbstractPowerSyncDatabase.js';
2
+ import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js';
3
+ import { WatchedQuery, WatchedQueryListener, WatchedQueryOptions, WatchedQueryState } from '../WatchedQuery.js';
4
+ /**
5
+ * @internal
6
+ */
7
+ export interface AbstractQueryProcessorOptions<Data, Settings extends WatchedQueryOptions = WatchedQueryOptions> {
8
+ db: AbstractPowerSyncDatabase;
9
+ watchOptions: Settings;
10
+ placeholderData: Data;
11
+ }
12
+ /**
13
+ * @internal
14
+ */
15
+ export interface LinkQueryOptions<Data, Settings extends WatchedQueryOptions = WatchedQueryOptions> {
16
+ abortSignal: AbortSignal;
17
+ settings: Settings;
18
+ }
19
+ type MutableDeep<T> = T extends ReadonlyArray<infer U> ? U[] : T;
20
+ /**
21
+ * @internal Mutable version of {@link WatchedQueryState}.
22
+ * This is used internally to allow updates to the state.
23
+ */
24
+ export type MutableWatchedQueryState<Data> = {
25
+ -readonly [P in keyof WatchedQueryState<Data>]: MutableDeep<WatchedQueryState<Data>[P]>;
26
+ };
27
+ type WatchedQueryProcessorListener<Data> = WatchedQueryListener<Data>;
28
+ /**
29
+ * Performs underlying watching and yields a stream of results.
30
+ * @internal
31
+ */
32
+ export declare abstract class AbstractQueryProcessor<Data = unknown[], Settings extends WatchedQueryOptions = WatchedQueryOptions> extends MetaBaseObserver<WatchedQueryProcessorListener<Data>> implements WatchedQuery<Data, Settings> {
33
+ protected options: AbstractQueryProcessorOptions<Data, Settings>;
34
+ readonly state: WatchedQueryState<Data>;
35
+ protected abortController: AbortController;
36
+ protected initialized: Promise<void>;
37
+ protected _closed: boolean;
38
+ protected disposeListeners: (() => void) | null;
39
+ get closed(): boolean;
40
+ constructor(options: AbstractQueryProcessorOptions<Data, Settings>);
41
+ protected constructInitialState(): WatchedQueryState<Data>;
42
+ protected get reportFetching(): boolean;
43
+ /**
44
+ * Updates the underlying query.
45
+ */
46
+ updateSettings(settings: Settings): Promise<void>;
47
+ /**
48
+ * This method is used to link a query to the subscribers of this listener class.
49
+ * This method should perform actual query watching and report results via {@link updateState} method.
50
+ */
51
+ protected abstract linkQuery(options: LinkQueryOptions<Data>): Promise<void>;
52
+ protected updateState(update: Partial<MutableWatchedQueryState<Data>>): Promise<void>;
53
+ /**
54
+ * Configures base DB listeners and links the query to listeners.
55
+ */
56
+ protected init(): Promise<void>;
57
+ close(): Promise<void>;
58
+ /**
59
+ * Runs a callback and reports errors to the error listeners.
60
+ */
61
+ protected runWithReporting<T>(callback: () => Promise<T>): Promise<void>;
62
+ /**
63
+ * Iterate listeners and reports errors to onError handlers.
64
+ */
65
+ protected iterateAsyncListenersWithError(callback: (listener: Partial<WatchedQueryProcessorListener<Data>>) => Promise<void> | void): Promise<void>;
66
+ }
67
+ export {};
@@ -0,0 +1,136 @@
1
+ import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js';
2
+ /**
3
+ * Performs underlying watching and yields a stream of results.
4
+ * @internal
5
+ */
6
+ export class AbstractQueryProcessor extends MetaBaseObserver {
7
+ options;
8
+ state;
9
+ abortController;
10
+ initialized;
11
+ _closed;
12
+ disposeListeners;
13
+ get closed() {
14
+ return this._closed;
15
+ }
16
+ constructor(options) {
17
+ super();
18
+ this.options = options;
19
+ this.abortController = new AbortController();
20
+ this._closed = false;
21
+ this.state = this.constructInitialState();
22
+ this.disposeListeners = null;
23
+ this.initialized = this.init();
24
+ }
25
+ constructInitialState() {
26
+ return {
27
+ isLoading: true,
28
+ isFetching: this.reportFetching, // Only set to true if we will report updates in future
29
+ error: null,
30
+ lastUpdated: null,
31
+ data: this.options.placeholderData
32
+ };
33
+ }
34
+ get reportFetching() {
35
+ return this.options.watchOptions.reportFetching ?? true;
36
+ }
37
+ /**
38
+ * Updates the underlying query.
39
+ */
40
+ async updateSettings(settings) {
41
+ await this.initialized;
42
+ if (!this.state.isLoading) {
43
+ await this.updateState({
44
+ isLoading: true,
45
+ isFetching: this.reportFetching ? true : false
46
+ });
47
+ }
48
+ this.options.watchOptions = settings;
49
+ this.abortController.abort();
50
+ this.abortController = new AbortController();
51
+ await this.runWithReporting(() => this.linkQuery({
52
+ abortSignal: this.abortController.signal,
53
+ settings
54
+ }));
55
+ }
56
+ async updateState(update) {
57
+ if (typeof update.error !== 'undefined') {
58
+ await this.iterateAsyncListenersWithError(async (l) => l.onError?.(update.error));
59
+ // An error always stops for the current fetching state
60
+ update.isFetching = false;
61
+ update.isLoading = false;
62
+ }
63
+ Object.assign(this.state, { lastUpdated: new Date() }, update);
64
+ if (typeof update.data !== 'undefined') {
65
+ await this.iterateAsyncListenersWithError(async (l) => l.onData?.(this.state.data));
66
+ }
67
+ await this.iterateAsyncListenersWithError(async (l) => l.onStateChange?.(this.state));
68
+ }
69
+ /**
70
+ * Configures base DB listeners and links the query to listeners.
71
+ */
72
+ async init() {
73
+ const { db } = this.options;
74
+ const disposeCloseListener = db.registerListener({
75
+ closing: async () => {
76
+ await this.close();
77
+ }
78
+ });
79
+ // Wait for the schema to be set before listening to changes
80
+ await db.waitForReady();
81
+ const disposeSchemaListener = db.registerListener({
82
+ schemaChanged: async () => {
83
+ await this.runWithReporting(async () => {
84
+ await this.updateSettings(this.options.watchOptions);
85
+ });
86
+ }
87
+ });
88
+ this.disposeListeners = () => {
89
+ disposeCloseListener();
90
+ disposeSchemaListener();
91
+ };
92
+ // Initial setup
93
+ this.runWithReporting(async () => {
94
+ await this.updateSettings(this.options.watchOptions);
95
+ });
96
+ }
97
+ async close() {
98
+ await this.initialized;
99
+ this.abortController.abort();
100
+ this.disposeListeners?.();
101
+ this.disposeListeners = null;
102
+ this._closed = true;
103
+ this.iterateListeners((l) => l.closed?.());
104
+ this.listeners.clear();
105
+ }
106
+ /**
107
+ * Runs a callback and reports errors to the error listeners.
108
+ */
109
+ async runWithReporting(callback) {
110
+ try {
111
+ await callback();
112
+ }
113
+ catch (error) {
114
+ // This will update the error on the state and iterate error listeners
115
+ await this.updateState({ error });
116
+ }
117
+ }
118
+ /**
119
+ * Iterate listeners and reports errors to onError handlers.
120
+ */
121
+ async iterateAsyncListenersWithError(callback) {
122
+ try {
123
+ await this.iterateAsyncListeners(async (l) => callback(l));
124
+ }
125
+ catch (error) {
126
+ try {
127
+ await this.iterateAsyncListeners(async (l) => l.onError?.(error));
128
+ }
129
+ catch (error) {
130
+ // Errors here are ignored
131
+ // since we are already in an error state
132
+ this.options.db.logger.error('Watched query error handler threw an Error', error);
133
+ }
134
+ }
135
+ }
136
+ }