@powersync/common 1.40.0 → 1.41.1

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 (183) hide show
  1. package/dist/bundle.cjs +10809 -22
  2. package/dist/bundle.cjs.map +1 -0
  3. package/dist/bundle.mjs +10730 -22
  4. package/dist/bundle.mjs.map +1 -0
  5. package/dist/bundle.node.cjs +10809 -0
  6. package/dist/bundle.node.cjs.map +1 -0
  7. package/dist/bundle.node.mjs +10730 -0
  8. package/dist/bundle.node.mjs.map +1 -0
  9. package/dist/index.d.cts +5 -1
  10. package/lib/client/AbstractPowerSyncDatabase.js +1 -0
  11. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -0
  12. package/lib/client/AbstractPowerSyncOpenFactory.js +1 -0
  13. package/lib/client/AbstractPowerSyncOpenFactory.js.map +1 -0
  14. package/lib/client/ConnectionManager.js +1 -0
  15. package/lib/client/ConnectionManager.js.map +1 -0
  16. package/lib/client/CustomQuery.js +1 -0
  17. package/lib/client/CustomQuery.js.map +1 -0
  18. package/lib/client/Query.js +1 -0
  19. package/lib/client/Query.js.map +1 -0
  20. package/lib/client/SQLOpenFactory.js +1 -0
  21. package/lib/client/SQLOpenFactory.js.map +1 -0
  22. package/lib/client/compilableQueryWatch.js +1 -0
  23. package/lib/client/compilableQueryWatch.js.map +1 -0
  24. package/lib/client/connection/PowerSyncBackendConnector.js +1 -0
  25. package/lib/client/connection/PowerSyncBackendConnector.js.map +1 -0
  26. package/lib/client/connection/PowerSyncCredentials.js +1 -0
  27. package/lib/client/connection/PowerSyncCredentials.js.map +1 -0
  28. package/lib/client/constants.js +1 -0
  29. package/lib/client/constants.js.map +1 -0
  30. package/lib/client/runOnSchemaChange.js +1 -0
  31. package/lib/client/runOnSchemaChange.js.map +1 -0
  32. package/lib/client/sync/bucket/BucketStorageAdapter.js +1 -0
  33. package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -0
  34. package/lib/client/sync/bucket/CrudBatch.js +1 -0
  35. package/lib/client/sync/bucket/CrudBatch.js.map +1 -0
  36. package/lib/client/sync/bucket/CrudEntry.js +1 -0
  37. package/lib/client/sync/bucket/CrudEntry.js.map +1 -0
  38. package/lib/client/sync/bucket/CrudTransaction.js +1 -0
  39. package/lib/client/sync/bucket/CrudTransaction.js.map +1 -0
  40. package/lib/client/sync/bucket/OpType.js +1 -0
  41. package/lib/client/sync/bucket/OpType.js.map +1 -0
  42. package/lib/client/sync/bucket/OplogEntry.js +1 -0
  43. package/lib/client/sync/bucket/OplogEntry.js.map +1 -0
  44. package/lib/client/sync/bucket/SqliteBucketStorage.js +1 -0
  45. package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -0
  46. package/lib/client/sync/bucket/SyncDataBatch.js +1 -0
  47. package/lib/client/sync/bucket/SyncDataBatch.js.map +1 -0
  48. package/lib/client/sync/bucket/SyncDataBucket.js +1 -0
  49. package/lib/client/sync/bucket/SyncDataBucket.js.map +1 -0
  50. package/lib/client/sync/stream/AbstractRemote.d.ts +5 -0
  51. package/lib/client/sync/stream/AbstractRemote.js +9 -2
  52. package/lib/client/sync/stream/AbstractRemote.js.map +1 -0
  53. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +1 -0
  54. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -0
  55. package/lib/client/sync/stream/WebsocketClientTransport.js +1 -0
  56. package/lib/client/sync/stream/WebsocketClientTransport.js.map +1 -0
  57. package/lib/client/sync/stream/core-instruction.js +1 -0
  58. package/lib/client/sync/stream/core-instruction.js.map +1 -0
  59. package/lib/client/sync/stream/streaming-sync-types.js +1 -0
  60. package/lib/client/sync/stream/streaming-sync-types.js.map +1 -0
  61. package/lib/client/sync/sync-streams.js +1 -0
  62. package/lib/client/sync/sync-streams.js.map +1 -0
  63. package/lib/client/triggers/TriggerManager.js +1 -0
  64. package/lib/client/triggers/TriggerManager.js.map +1 -0
  65. package/lib/client/triggers/TriggerManagerImpl.js +1 -0
  66. package/lib/client/triggers/TriggerManagerImpl.js.map +1 -0
  67. package/lib/client/triggers/sanitizeSQL.js +1 -0
  68. package/lib/client/triggers/sanitizeSQL.js.map +1 -0
  69. package/lib/client/watched/GetAllQuery.js +1 -0
  70. package/lib/client/watched/GetAllQuery.js.map +1 -0
  71. package/lib/client/watched/WatchedQuery.js +1 -0
  72. package/lib/client/watched/WatchedQuery.js.map +1 -0
  73. package/lib/client/watched/processors/AbstractQueryProcessor.js +1 -0
  74. package/lib/client/watched/processors/AbstractQueryProcessor.js.map +1 -0
  75. package/lib/client/watched/processors/DifferentialQueryProcessor.js +1 -0
  76. package/lib/client/watched/processors/DifferentialQueryProcessor.js.map +1 -0
  77. package/lib/client/watched/processors/OnChangeQueryProcessor.js +1 -0
  78. package/lib/client/watched/processors/OnChangeQueryProcessor.js.map +1 -0
  79. package/lib/client/watched/processors/comparators.js +1 -0
  80. package/lib/client/watched/processors/comparators.js.map +1 -0
  81. package/lib/db/DBAdapter.js +1 -0
  82. package/lib/db/DBAdapter.js.map +1 -0
  83. package/lib/db/crud/SyncProgress.js +1 -0
  84. package/lib/db/crud/SyncProgress.js.map +1 -0
  85. package/lib/db/crud/SyncStatus.js +1 -0
  86. package/lib/db/crud/SyncStatus.js.map +1 -0
  87. package/lib/db/crud/UploadQueueStatus.js +1 -0
  88. package/lib/db/crud/UploadQueueStatus.js.map +1 -0
  89. package/lib/db/schema/Column.js +1 -0
  90. package/lib/db/schema/Column.js.map +1 -0
  91. package/lib/db/schema/Index.js +1 -0
  92. package/lib/db/schema/Index.js.map +1 -0
  93. package/lib/db/schema/IndexedColumn.js +1 -0
  94. package/lib/db/schema/IndexedColumn.js.map +1 -0
  95. package/lib/db/schema/RawTable.js +1 -0
  96. package/lib/db/schema/RawTable.js.map +1 -0
  97. package/lib/db/schema/Schema.d.ts +0 -1
  98. package/lib/db/schema/Schema.js +4 -8
  99. package/lib/db/schema/Schema.js.map +1 -0
  100. package/lib/db/schema/Table.js +1 -0
  101. package/lib/db/schema/Table.js.map +1 -0
  102. package/lib/db/schema/TableV2.js +1 -0
  103. package/lib/db/schema/TableV2.js.map +1 -0
  104. package/lib/index.js +1 -0
  105. package/lib/index.js.map +1 -0
  106. package/lib/types/types.js +1 -0
  107. package/lib/types/types.js.map +1 -0
  108. package/lib/utils/AbortOperation.js +1 -0
  109. package/lib/utils/AbortOperation.js.map +1 -0
  110. package/lib/utils/BaseObserver.js +1 -0
  111. package/lib/utils/BaseObserver.js.map +1 -0
  112. package/lib/utils/ControlledExecutor.js +1 -0
  113. package/lib/utils/ControlledExecutor.js.map +1 -0
  114. package/lib/utils/DataStream.js +1 -0
  115. package/lib/utils/DataStream.js.map +1 -0
  116. package/lib/utils/Logger.js +1 -0
  117. package/lib/utils/Logger.js.map +1 -0
  118. package/lib/utils/MetaBaseObserver.js +1 -0
  119. package/lib/utils/MetaBaseObserver.js.map +1 -0
  120. package/lib/utils/async.js +1 -0
  121. package/lib/utils/async.js.map +1 -0
  122. package/lib/utils/mutex.js +1 -0
  123. package/lib/utils/mutex.js.map +1 -0
  124. package/lib/utils/parseQuery.js +1 -0
  125. package/lib/utils/parseQuery.js.map +1 -0
  126. package/package.json +23 -15
  127. package/src/client/AbstractPowerSyncDatabase.ts +1343 -0
  128. package/src/client/AbstractPowerSyncOpenFactory.ts +39 -0
  129. package/src/client/ConnectionManager.ts +402 -0
  130. package/src/client/CustomQuery.ts +56 -0
  131. package/src/client/Query.ts +106 -0
  132. package/src/client/SQLOpenFactory.ts +55 -0
  133. package/src/client/compilableQueryWatch.ts +55 -0
  134. package/src/client/connection/PowerSyncBackendConnector.ts +25 -0
  135. package/src/client/connection/PowerSyncCredentials.ts +5 -0
  136. package/src/client/constants.ts +1 -0
  137. package/src/client/runOnSchemaChange.ts +31 -0
  138. package/src/client/sync/bucket/BucketStorageAdapter.ts +118 -0
  139. package/src/client/sync/bucket/CrudBatch.ts +21 -0
  140. package/src/client/sync/bucket/CrudEntry.ts +172 -0
  141. package/src/client/sync/bucket/CrudTransaction.ts +21 -0
  142. package/src/client/sync/bucket/OpType.ts +23 -0
  143. package/src/client/sync/bucket/OplogEntry.ts +50 -0
  144. package/src/client/sync/bucket/SqliteBucketStorage.ts +395 -0
  145. package/src/client/sync/bucket/SyncDataBatch.ts +11 -0
  146. package/src/client/sync/bucket/SyncDataBucket.ts +49 -0
  147. package/src/client/sync/stream/AbstractRemote.ts +626 -0
  148. package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +1258 -0
  149. package/src/client/sync/stream/WebsocketClientTransport.ts +80 -0
  150. package/src/client/sync/stream/core-instruction.ts +99 -0
  151. package/src/client/sync/stream/streaming-sync-types.ts +205 -0
  152. package/src/client/sync/sync-streams.ts +107 -0
  153. package/src/client/triggers/TriggerManager.ts +384 -0
  154. package/src/client/triggers/TriggerManagerImpl.ts +314 -0
  155. package/src/client/triggers/sanitizeSQL.ts +66 -0
  156. package/src/client/watched/GetAllQuery.ts +46 -0
  157. package/src/client/watched/WatchedQuery.ts +121 -0
  158. package/src/client/watched/processors/AbstractQueryProcessor.ts +226 -0
  159. package/src/client/watched/processors/DifferentialQueryProcessor.ts +305 -0
  160. package/src/client/watched/processors/OnChangeQueryProcessor.ts +122 -0
  161. package/src/client/watched/processors/comparators.ts +57 -0
  162. package/src/db/DBAdapter.ts +134 -0
  163. package/src/db/crud/SyncProgress.ts +100 -0
  164. package/src/db/crud/SyncStatus.ts +308 -0
  165. package/src/db/crud/UploadQueueStatus.ts +20 -0
  166. package/src/db/schema/Column.ts +60 -0
  167. package/src/db/schema/Index.ts +39 -0
  168. package/src/db/schema/IndexedColumn.ts +42 -0
  169. package/src/db/schema/RawTable.ts +67 -0
  170. package/src/db/schema/Schema.ts +76 -0
  171. package/src/db/schema/Table.ts +359 -0
  172. package/src/db/schema/TableV2.ts +9 -0
  173. package/src/index.ts +52 -0
  174. package/src/types/types.ts +9 -0
  175. package/src/utils/AbortOperation.ts +17 -0
  176. package/src/utils/BaseObserver.ts +41 -0
  177. package/src/utils/ControlledExecutor.ts +72 -0
  178. package/src/utils/DataStream.ts +211 -0
  179. package/src/utils/Logger.ts +47 -0
  180. package/src/utils/MetaBaseObserver.ts +81 -0
  181. package/src/utils/async.ts +61 -0
  182. package/src/utils/mutex.ts +34 -0
  183. package/src/utils/parseQuery.ts +25 -0
@@ -0,0 +1,66 @@
1
+ function sanitizeString(input: string): string {
2
+ return `'${input.replace(/'/g, "''")}'`;
3
+ }
4
+ /**
5
+ * Helper function for sanitizing UUID input strings.
6
+ * Typically used with {@link sanitizeSQL}.
7
+ */
8
+ export function sanitizeUUID(uuid: string): string {
9
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
+ const isValid = uuidRegex.test(uuid);
11
+ if (!isValid) {
12
+ throw new Error(`${uuid} is not a valid UUID`);
13
+ }
14
+ return uuid;
15
+ }
16
+
17
+ /**
18
+ * SQL string template function for {@link TrackDiffOptions#when} and {@link CreateDiffTriggerOptions#when}.
19
+ *
20
+ * This function performs basic string interpolation for SQLite WHEN clauses.
21
+ *
22
+ * **String placeholders:**
23
+ * - All string values passed as placeholders are automatically wrapped in single quotes (`'`).
24
+ * - Do not manually wrap placeholders in single quotes in your template string; the function will handle quoting and escaping for you.
25
+ * - Any single quotes within the string value are escaped by doubling them (`''`), as required by SQL syntax.
26
+ *
27
+ * **Other types:**
28
+ * - `null` and `undefined` are converted to SQL `NULL`.
29
+ * - Objects are stringified using `JSON.stringify()` and wrapped in single quotes, with any single quotes inside the stringified value escaped.
30
+ * - Numbers and other primitive types are inserted directly.
31
+ *
32
+ * **Usage example:**
33
+ * ```typescript
34
+ * const myID = "O'Reilly";
35
+ * const clause = sanitizeSQL`New.id = ${myID}`;
36
+ * // Result: "New.id = 'O''Reilly'"
37
+ * ```
38
+ *
39
+ * Avoid manually quoting placeholders:
40
+ * ```typescript
41
+ * // Incorrect:
42
+ * sanitizeSQL`New.id = '${myID}'` // Produces double quotes: New.id = ''O''Reilly''
43
+ * ```
44
+ */
45
+ export function sanitizeSQL(strings: TemplateStringsArray, ...values: any[]): string {
46
+ let result = '';
47
+ strings.forEach((str, i) => {
48
+ result += str;
49
+ if (i < values.length) {
50
+ // For SQL, escape single quotes in string values
51
+ const value = values[i];
52
+ if (typeof value == 'string') {
53
+ result += sanitizeString(value);
54
+ } else if (value == null) {
55
+ result += 'NULL';
56
+ } else if (typeof value == 'object') {
57
+ // Stringify the object and escape single quotes in the result
58
+ const stringified = JSON.stringify(value);
59
+ result += sanitizeString(stringified);
60
+ } else {
61
+ result += value;
62
+ }
63
+ }
64
+ });
65
+ return result;
66
+ }
@@ -0,0 +1,46 @@
1
+ import { CompiledQuery } from '../../types/types.js';
2
+ import { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
3
+ import { WatchCompatibleQuery } from './WatchedQuery.js';
4
+
5
+ /**
6
+ * Options for {@link GetAllQuery}.
7
+ */
8
+ export type GetAllQueryOptions<RowType = unknown> = {
9
+ sql: string;
10
+ parameters?: ReadonlyArray<unknown>;
11
+ /**
12
+ * Optional mapper function to convert raw rows into the desired RowType.
13
+ * @example
14
+ * ```javascript
15
+ * (rawRow) => ({
16
+ * id: rawRow.id,
17
+ * created_at: new Date(rawRow.created_at),
18
+ * })
19
+ * ```
20
+ */
21
+ mapper?: (rawRow: Record<string, unknown>) => RowType;
22
+ };
23
+
24
+ /**
25
+ * Performs a {@link AbstractPowerSyncDatabase.getAll} operation for a watched query.
26
+ */
27
+ export class GetAllQuery<RowType = unknown> implements WatchCompatibleQuery<RowType[]> {
28
+ constructor(protected options: GetAllQueryOptions<RowType>) {}
29
+
30
+ compile(): CompiledQuery {
31
+ return {
32
+ sql: this.options.sql,
33
+ parameters: this.options.parameters ?? []
34
+ };
35
+ }
36
+
37
+ async execute(options: { db: AbstractPowerSyncDatabase }): Promise<RowType[]> {
38
+ const { db } = options;
39
+ const { sql, parameters = [] } = this.compile();
40
+ const rawResult = await db.getAll<unknown>(sql, [...parameters]);
41
+ if (this.options.mapper) {
42
+ return rawResult.map(this.options.mapper);
43
+ }
44
+ return rawResult as RowType[];
45
+ }
46
+ }
@@ -0,0 +1,121 @@
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
+ /**
7
+ * State for {@link WatchedQuery} instances.
8
+ */
9
+ export interface WatchedQueryState<Data> {
10
+ /**
11
+ * Indicates the initial loading state (hard loading).
12
+ * Loading becomes false once the first set of results from the watched query is available or an error occurs.
13
+ */
14
+ readonly isLoading: boolean;
15
+ /**
16
+ * Indicates whether the query is currently fetching data, is true during the initial load
17
+ * and any time when the query is re-evaluating (useful for large queries).
18
+ */
19
+ readonly isFetching: boolean;
20
+ /**
21
+ * The last error that occurred while executing the query.
22
+ */
23
+ readonly error: Error | null;
24
+ /**
25
+ * The last time the query was updated.
26
+ */
27
+ readonly lastUpdated: Date | null;
28
+ /**
29
+ * The last data returned by the query.
30
+ */
31
+ readonly data: Data;
32
+ }
33
+
34
+ /**
35
+ * Options provided to the `execute` method of a {@link WatchCompatibleQuery}.
36
+ */
37
+ export interface WatchExecuteOptions {
38
+ sql: string;
39
+ parameters: any[];
40
+ db: AbstractPowerSyncDatabase;
41
+ }
42
+
43
+ /**
44
+ * Similar to {@link CompatibleQuery}, except the `execute` method
45
+ * does not enforce an Array result type.
46
+ */
47
+ export interface WatchCompatibleQuery<ResultType> {
48
+ execute(options: WatchExecuteOptions): Promise<ResultType>;
49
+ compile(): CompiledQuery;
50
+ }
51
+
52
+ export interface WatchedQueryOptions {
53
+ /** The minimum interval between queries. */
54
+ throttleMs?: number;
55
+ /**
56
+ * If true (default) the watched query will update its state to report
57
+ * on the fetching state of the query.
58
+ * Setting to false reduces the number of state changes if the fetch status
59
+ * is not relevant to the consumer.
60
+ */
61
+ reportFetching?: boolean;
62
+
63
+ /**
64
+ * By default, watched queries requery the database on any change to any dependent table of the query.
65
+ * Supplying an override here can be used to limit the tables which trigger querying the database.
66
+ */
67
+ triggerOnTables?: string[];
68
+ }
69
+
70
+ export enum WatchedQueryListenerEvent {
71
+ ON_DATA = 'onData',
72
+ ON_ERROR = 'onError',
73
+ ON_STATE_CHANGE = 'onStateChange',
74
+ SETTINGS_WILL_UPDATE = 'settingsWillUpdate',
75
+ CLOSED = 'closed'
76
+ }
77
+
78
+ export interface WatchedQueryListener<Data> extends BaseListener {
79
+ [WatchedQueryListenerEvent.ON_DATA]?: (data: Data) => void | Promise<void>;
80
+ [WatchedQueryListenerEvent.ON_ERROR]?: (error: Error) => void | Promise<void>;
81
+ [WatchedQueryListenerEvent.ON_STATE_CHANGE]?: (state: WatchedQueryState<Data>) => void | Promise<void>;
82
+ [WatchedQueryListenerEvent.SETTINGS_WILL_UPDATE]?: () => void;
83
+ [WatchedQueryListenerEvent.CLOSED]?: () => void | Promise<void>;
84
+ }
85
+
86
+ export const DEFAULT_WATCH_THROTTLE_MS = 30;
87
+
88
+ export const DEFAULT_WATCH_QUERY_OPTIONS: WatchedQueryOptions = {
89
+ throttleMs: DEFAULT_WATCH_THROTTLE_MS,
90
+ reportFetching: true
91
+ };
92
+
93
+ export interface WatchedQuery<
94
+ Data = unknown,
95
+ Settings extends WatchedQueryOptions = WatchedQueryOptions,
96
+ Listener extends WatchedQueryListener<Data> = WatchedQueryListener<Data>
97
+ > extends MetaBaseObserverInterface<Listener> {
98
+ /**
99
+ * Current state of the watched query.
100
+ */
101
+ readonly state: WatchedQueryState<Data>;
102
+
103
+ readonly closed: boolean;
104
+
105
+ /**
106
+ * Subscribe to watched query events.
107
+ * @returns A function to unsubscribe from the events.
108
+ */
109
+ registerListener(listener: Listener): () => void;
110
+
111
+ /**
112
+ * Updates the underlying query options.
113
+ * This will trigger a re-evaluation of the query and update the state.
114
+ */
115
+ updateSettings(options: Settings): Promise<void>;
116
+
117
+ /**
118
+ * Close the watched query and end all subscriptions.
119
+ */
120
+ close(): Promise<void>;
121
+ }
@@ -0,0 +1,226 @@
1
+ import { AbstractPowerSyncDatabase } from '../../../client/AbstractPowerSyncDatabase.js';
2
+ import { MetaBaseObserver } from '../../../utils/MetaBaseObserver.js';
3
+ import {
4
+ WatchedQuery,
5
+ WatchedQueryListener,
6
+ WatchedQueryListenerEvent,
7
+ WatchedQueryOptions,
8
+ WatchedQueryState
9
+ } from '../WatchedQuery.js';
10
+
11
+ /**
12
+ * @internal
13
+ */
14
+ export interface AbstractQueryProcessorOptions<Data, Settings extends WatchedQueryOptions = WatchedQueryOptions> {
15
+ db: AbstractPowerSyncDatabase;
16
+ watchOptions: Settings;
17
+ placeholderData: Data;
18
+ }
19
+
20
+ /**
21
+ * @internal
22
+ */
23
+ export interface LinkQueryOptions<Data, Settings extends WatchedQueryOptions = WatchedQueryOptions> {
24
+ abortSignal: AbortSignal;
25
+ settings: Settings;
26
+ }
27
+
28
+ type MutableDeep<T> =
29
+ T extends ReadonlyArray<infer U>
30
+ ? U[] // convert readonly arrays to mutable arrays
31
+ : T;
32
+
33
+ /**
34
+ * @internal Mutable version of {@link WatchedQueryState}.
35
+ * This is used internally to allow updates to the state.
36
+ */
37
+ export type MutableWatchedQueryState<Data> = {
38
+ -readonly [P in keyof WatchedQueryState<Data>]: MutableDeep<WatchedQueryState<Data>[P]>;
39
+ };
40
+
41
+ type WatchedQueryProcessorListener<Data> = WatchedQueryListener<Data>;
42
+
43
+ /**
44
+ * Performs underlying watching and yields a stream of results.
45
+ * @internal
46
+ */
47
+ export abstract class AbstractQueryProcessor<
48
+ Data = unknown[],
49
+ Settings extends WatchedQueryOptions = WatchedQueryOptions
50
+ >
51
+ extends MetaBaseObserver<WatchedQueryProcessorListener<Data>>
52
+ implements WatchedQuery<Data, Settings>
53
+ {
54
+ readonly state: WatchedQueryState<Data>;
55
+
56
+ protected abortController: AbortController;
57
+ protected initialized: Promise<void>;
58
+ protected _closed: boolean;
59
+ protected disposeListeners: (() => void) | null;
60
+
61
+ get closed() {
62
+ return this._closed;
63
+ }
64
+
65
+ constructor(protected options: AbstractQueryProcessorOptions<Data, Settings>) {
66
+ super();
67
+ this.abortController = new AbortController();
68
+ this._closed = false;
69
+ this.state = this.constructInitialState();
70
+ this.disposeListeners = null;
71
+ this.initialized = this.init(this.abortController.signal);
72
+ }
73
+
74
+ protected constructInitialState(): WatchedQueryState<Data> {
75
+ return {
76
+ isLoading: true,
77
+ isFetching: this.reportFetching, // Only set to true if we will report updates in future
78
+ error: null,
79
+ lastUpdated: null,
80
+ data: this.options.placeholderData
81
+ };
82
+ }
83
+
84
+ protected get reportFetching() {
85
+ return this.options.watchOptions.reportFetching ?? true;
86
+ }
87
+
88
+ protected async updateSettingsInternal(settings: Settings, signal: AbortSignal) {
89
+ // This may have been aborted while awaiting or if multiple calls to `updateSettings` were made
90
+ if (this._closed || signal.aborted) {
91
+ return;
92
+ }
93
+
94
+ this.options.watchOptions = settings;
95
+
96
+ this.iterateListeners((l) => l[WatchedQueryListenerEvent.SETTINGS_WILL_UPDATE]?.());
97
+
98
+ if (!this.state.isFetching && this.reportFetching) {
99
+ await this.updateState({
100
+ isFetching: true
101
+ });
102
+ }
103
+
104
+ await this.runWithReporting(() =>
105
+ this.linkQuery({
106
+ abortSignal: signal,
107
+ settings
108
+ })
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Updates the underlying query.
114
+ */
115
+ async updateSettings(settings: Settings) {
116
+ // Abort the previous request
117
+ this.abortController.abort();
118
+
119
+ // Keep track of this controller's abort status
120
+ const abortController = new AbortController();
121
+ // Allow this to be aborted externally
122
+ this.abortController = abortController;
123
+
124
+ await this.initialized;
125
+ return this.updateSettingsInternal(settings, abortController.signal);
126
+ }
127
+
128
+ /**
129
+ * This method is used to link a query to the subscribers of this listener class.
130
+ * This method should perform actual query watching and report results via {@link updateState} method.
131
+ */
132
+ protected abstract linkQuery(options: LinkQueryOptions<Data>): Promise<void>;
133
+
134
+ protected async updateState(update: Partial<MutableWatchedQueryState<Data>>) {
135
+ if (this._closed) {
136
+ return;
137
+ }
138
+
139
+ if (typeof update.error !== 'undefined') {
140
+ await this.iterateAsyncListenersWithError(async (l) => l.onError?.(update.error!));
141
+ // An error always stops for the current fetching state
142
+ update.isFetching = false;
143
+ update.isLoading = false;
144
+ }
145
+
146
+ Object.assign(this.state, { lastUpdated: new Date() } satisfies Partial<WatchedQueryState<Data>>, update);
147
+
148
+ if (typeof update.data !== 'undefined') {
149
+ await this.iterateAsyncListenersWithError(async (l) => l.onData?.(this.state.data));
150
+ }
151
+ await this.iterateAsyncListenersWithError(async (l) => l.onStateChange?.(this.state));
152
+ }
153
+
154
+ /**
155
+ * Configures base DB listeners and links the query to listeners.
156
+ */
157
+ protected async init(signal: AbortSignal) {
158
+ const { db } = this.options;
159
+
160
+ const disposeCloseListener = db.registerListener({
161
+ closing: async () => {
162
+ await this.close();
163
+ }
164
+ });
165
+
166
+ // Wait for the schema to be set before listening to changes
167
+ await db.waitForReady();
168
+ const disposeSchemaListener = db.registerListener({
169
+ schemaChanged: async () => {
170
+ await this.runWithReporting(async () => {
171
+ await this.updateSettings(this.options.watchOptions);
172
+ });
173
+ }
174
+ });
175
+
176
+ this.disposeListeners = () => {
177
+ disposeCloseListener();
178
+ disposeSchemaListener();
179
+ };
180
+
181
+ // Initial setup
182
+ await this.runWithReporting(async () => {
183
+ await this.updateSettingsInternal(this.options.watchOptions, signal);
184
+ });
185
+ }
186
+
187
+ async close() {
188
+ this._closed = true;
189
+ this.abortController.abort();
190
+ this.disposeListeners?.();
191
+ this.disposeListeners = null;
192
+ this.iterateListeners((l) => l.closed?.());
193
+ this.listeners.clear();
194
+ }
195
+
196
+ /**
197
+ * Runs a callback and reports errors to the error listeners.
198
+ */
199
+ protected async runWithReporting<T>(callback: () => Promise<T>): Promise<void> {
200
+ try {
201
+ await callback();
202
+ } catch (error) {
203
+ // This will update the error on the state and iterate error listeners
204
+ await this.updateState({ error });
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Iterate listeners and reports errors to onError handlers.
210
+ */
211
+ protected async iterateAsyncListenersWithError(
212
+ callback: (listener: Partial<WatchedQueryProcessorListener<Data>>) => Promise<void> | void
213
+ ) {
214
+ try {
215
+ await this.iterateAsyncListeners(async (l) => callback(l));
216
+ } catch (error) {
217
+ try {
218
+ await this.iterateAsyncListeners(async (l) => l.onError?.(error));
219
+ } catch (error) {
220
+ // Errors here are ignored
221
+ // since we are already in an error state
222
+ this.options.db.logger.error('Watched query error handler threw an Error', error);
223
+ }
224
+ }
225
+ }
226
+ }