@powersync/common 1.41.0 → 1.42.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 (183) hide show
  1. package/dist/bundle.cjs +10820 -22
  2. package/dist/bundle.cjs.map +1 -0
  3. package/dist/bundle.mjs +10741 -22
  4. package/dist/bundle.mjs.map +1 -0
  5. package/dist/bundle.node.cjs +10820 -0
  6. package/dist/bundle.node.cjs.map +1 -0
  7. package/dist/bundle.node.mjs +10741 -0
  8. package/dist/bundle.node.mjs.map +1 -0
  9. package/dist/index.d.cts +77 -13
  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 +19 -6
  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.d.ts +71 -12
  64. package/lib/client/triggers/TriggerManager.js +1 -0
  65. package/lib/client/triggers/TriggerManager.js.map +1 -0
  66. package/lib/client/triggers/TriggerManagerImpl.js +11 -5
  67. package/lib/client/triggers/TriggerManagerImpl.js.map +1 -0
  68. package/lib/client/triggers/sanitizeSQL.js +1 -0
  69. package/lib/client/triggers/sanitizeSQL.js.map +1 -0
  70. package/lib/client/watched/GetAllQuery.js +1 -0
  71. package/lib/client/watched/GetAllQuery.js.map +1 -0
  72. package/lib/client/watched/WatchedQuery.js +1 -0
  73. package/lib/client/watched/WatchedQuery.js.map +1 -0
  74. package/lib/client/watched/processors/AbstractQueryProcessor.js +1 -0
  75. package/lib/client/watched/processors/AbstractQueryProcessor.js.map +1 -0
  76. package/lib/client/watched/processors/DifferentialQueryProcessor.js +1 -0
  77. package/lib/client/watched/processors/DifferentialQueryProcessor.js.map +1 -0
  78. package/lib/client/watched/processors/OnChangeQueryProcessor.js +1 -0
  79. package/lib/client/watched/processors/OnChangeQueryProcessor.js.map +1 -0
  80. package/lib/client/watched/processors/comparators.js +1 -0
  81. package/lib/client/watched/processors/comparators.js.map +1 -0
  82. package/lib/db/DBAdapter.js +1 -0
  83. package/lib/db/DBAdapter.js.map +1 -0
  84. package/lib/db/crud/SyncProgress.js +1 -0
  85. package/lib/db/crud/SyncProgress.js.map +1 -0
  86. package/lib/db/crud/SyncStatus.js +1 -0
  87. package/lib/db/crud/SyncStatus.js.map +1 -0
  88. package/lib/db/crud/UploadQueueStatus.js +1 -0
  89. package/lib/db/crud/UploadQueueStatus.js.map +1 -0
  90. package/lib/db/schema/Column.js +1 -0
  91. package/lib/db/schema/Column.js.map +1 -0
  92. package/lib/db/schema/Index.js +1 -0
  93. package/lib/db/schema/Index.js.map +1 -0
  94. package/lib/db/schema/IndexedColumn.js +1 -0
  95. package/lib/db/schema/IndexedColumn.js.map +1 -0
  96. package/lib/db/schema/RawTable.js +1 -0
  97. package/lib/db/schema/RawTable.js.map +1 -0
  98. package/lib/db/schema/Schema.js +1 -0
  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 +636 -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 +451 -0
  154. package/src/client/triggers/TriggerManagerImpl.ts +320 -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,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
+ }
@@ -0,0 +1,305 @@
1
+ import { WatchCompatibleQuery, WatchedQuery, WatchedQueryListener, WatchedQueryOptions } from '../WatchedQuery.js';
2
+ import {
3
+ AbstractQueryProcessor,
4
+ AbstractQueryProcessorOptions,
5
+ LinkQueryOptions,
6
+ MutableWatchedQueryState
7
+ } from './AbstractQueryProcessor.js';
8
+
9
+ /**
10
+ * Represents an updated row in a differential watched query.
11
+ * It contains both the current and previous state of the row.
12
+ */
13
+ export interface WatchedQueryRowDifferential<RowType> {
14
+ readonly current: RowType;
15
+ readonly previous: RowType;
16
+ }
17
+
18
+ /**
19
+ * Represents the result of a watched query that has been diffed.
20
+ * {@link DifferentialWatchedQueryState#diff} is of the {@link WatchedQueryDifferential} form.
21
+ */
22
+ export interface WatchedQueryDifferential<RowType> {
23
+ readonly added: ReadonlyArray<Readonly<RowType>>;
24
+ /**
25
+ * The entire current result set.
26
+ * Array item object references are preserved between updates if the item is unchanged.
27
+ *
28
+ * e.g. In the query
29
+ * ```sql
30
+ * SELECT name, make FROM assets ORDER BY make ASC;
31
+ * ```
32
+ *
33
+ * If a previous result set contains an item (A) `{name: 'pc', make: 'Cool PC'}` and
34
+ * an update has been made which adds another item (B) to the result set (the item A is unchanged) - then
35
+ * the updated result set will be contain the same object reference, to item A, as the previous result set.
36
+ * This is regardless of the item A's position in the updated result set.
37
+ */
38
+ readonly all: ReadonlyArray<Readonly<RowType>>;
39
+ readonly removed: ReadonlyArray<Readonly<RowType>>;
40
+ readonly updated: ReadonlyArray<WatchedQueryRowDifferential<Readonly<RowType>>>;
41
+ readonly unchanged: ReadonlyArray<Readonly<RowType>>;
42
+ }
43
+
44
+ /**
45
+ * Row comparator for differentially watched queries which keys and compares items in the result set.
46
+ */
47
+ export interface DifferentialWatchedQueryComparator<RowType> {
48
+ /**
49
+ * Generates a unique key for the item.
50
+ */
51
+ keyBy: (item: RowType) => string;
52
+ /**
53
+ * Generates a token for comparing items with matching keys.
54
+ */
55
+ compareBy: (item: RowType) => string;
56
+ }
57
+
58
+ /**
59
+ * Options for building a differential watched query with the {@link Query} builder.
60
+ */
61
+ export interface DifferentialWatchedQueryOptions<RowType> extends WatchedQueryOptions {
62
+ /**
63
+ * Initial result data which is presented while the initial loading is executing.
64
+ */
65
+ placeholderData?: RowType[];
66
+
67
+ /**
68
+ * Row comparator used to identify and compare rows in the result set.
69
+ * If not provided, the default comparator will be used which keys items by their `id` property if available,
70
+ * otherwise it uses JSON stringification of the entire item for keying and comparison.
71
+ * @defaultValue {@link DEFAULT_ROW_COMPARATOR}
72
+ */
73
+ rowComparator?: DifferentialWatchedQueryComparator<RowType>;
74
+ }
75
+
76
+ /**
77
+ * Settings for differential incremental watched queries using.
78
+ */
79
+ export interface DifferentialWatchedQuerySettings<RowType> extends DifferentialWatchedQueryOptions<RowType> {
80
+ /**
81
+ * The query here must return an array of items that can be differentiated.
82
+ */
83
+ query: WatchCompatibleQuery<RowType[]>;
84
+ }
85
+
86
+ export interface DifferentialWatchedQueryListener<RowType>
87
+ extends WatchedQueryListener<ReadonlyArray<Readonly<RowType>>> {
88
+ onDiff?: (diff: WatchedQueryDifferential<RowType>) => void | Promise<void>;
89
+ }
90
+
91
+ export type DifferentialWatchedQuery<RowType> = WatchedQuery<
92
+ ReadonlyArray<Readonly<RowType>>,
93
+ DifferentialWatchedQuerySettings<RowType>,
94
+ DifferentialWatchedQueryListener<RowType>
95
+ >;
96
+
97
+ /**
98
+ * @internal
99
+ */
100
+ export interface DifferentialQueryProcessorOptions<RowType>
101
+ extends AbstractQueryProcessorOptions<RowType[], DifferentialWatchedQuerySettings<RowType>> {
102
+ rowComparator?: DifferentialWatchedQueryComparator<RowType>;
103
+ }
104
+
105
+ type DataHashMap<RowType> = Map<string, { hash: string; item: RowType }>;
106
+
107
+ /**
108
+ * An empty differential result set.
109
+ * This is used as the initial state for differential incrementally watched queries.
110
+ */
111
+ export const EMPTY_DIFFERENTIAL = {
112
+ added: [],
113
+ all: [],
114
+ removed: [],
115
+ updated: [],
116
+ unchanged: []
117
+ };
118
+
119
+ /**
120
+ * Default implementation of the {@link DifferentialWatchedQueryComparator} for watched queries.
121
+ * It keys items by their `id` property if available, alternatively it uses JSON stringification
122
+ * of the entire item for the key and comparison.
123
+ */
124
+ export const DEFAULT_ROW_COMPARATOR: DifferentialWatchedQueryComparator<any> = {
125
+ keyBy: (item) => {
126
+ if (item && typeof item == 'object' && typeof item['id'] == 'string') {
127
+ return item['id'];
128
+ }
129
+ return JSON.stringify(item);
130
+ },
131
+ compareBy: (item) => JSON.stringify(item)
132
+ };
133
+
134
+ /**
135
+ * Uses the PowerSync onChange event to trigger watched queries.
136
+ * Results are emitted on every change of the relevant tables.
137
+ * @internal
138
+ */
139
+ export class DifferentialQueryProcessor<RowType>
140
+ extends AbstractQueryProcessor<ReadonlyArray<Readonly<RowType>>, DifferentialWatchedQuerySettings<RowType>>
141
+ implements DifferentialWatchedQuery<RowType>
142
+ {
143
+ protected comparator: DifferentialWatchedQueryComparator<RowType>;
144
+
145
+ constructor(protected options: DifferentialQueryProcessorOptions<RowType>) {
146
+ super(options);
147
+ this.comparator = options.rowComparator ?? DEFAULT_ROW_COMPARATOR;
148
+ }
149
+
150
+ /*
151
+ * @returns If the sets are equal
152
+ */
153
+ protected differentiate(
154
+ current: RowType[],
155
+ previousMap: DataHashMap<RowType>
156
+ ): { diff: WatchedQueryDifferential<RowType>; map: DataHashMap<RowType>; hasChanged: boolean } {
157
+ const { keyBy, compareBy } = this.comparator;
158
+
159
+ let hasChanged = false;
160
+ const currentMap = new Map<string, { hash: string; item: RowType }>();
161
+ const removedTracker = new Set(previousMap.keys());
162
+
163
+ // Allow mutating to populate the data temporarily.
164
+ const diff = {
165
+ all: [] as RowType[],
166
+ added: [] as RowType[],
167
+ removed: [] as RowType[],
168
+ updated: [] as WatchedQueryRowDifferential<RowType>[],
169
+ unchanged: [] as RowType[]
170
+ };
171
+
172
+ /**
173
+ * Looping over the current result set array is important to preserve
174
+ * the ordering of the result set.
175
+ * We can replace items in the current array with previous object references if they are equal.
176
+ */
177
+ for (const item of current) {
178
+ const key = keyBy(item);
179
+ const hash = compareBy(item);
180
+ currentMap.set(key, { hash, item });
181
+
182
+ const previousItem = previousMap.get(key);
183
+ if (!previousItem) {
184
+ // New item
185
+ hasChanged = true;
186
+ diff.added.push(item);
187
+ diff.all.push(item);
188
+ } else {
189
+ // Existing item
190
+ if (hash == previousItem.hash) {
191
+ diff.unchanged.push(previousItem.item);
192
+ // Use the previous object reference
193
+ diff.all.push(previousItem.item);
194
+ // update the map to preserve the reference
195
+ currentMap.set(key, previousItem);
196
+ } else {
197
+ hasChanged = true;
198
+ diff.updated.push({ current: item, previous: previousItem.item });
199
+ // Use the new reference
200
+ diff.all.push(item);
201
+ }
202
+ }
203
+ // The item is present, we don't consider it removed
204
+ removedTracker.delete(key);
205
+ }
206
+
207
+ diff.removed = Array.from(removedTracker).map((key) => previousMap.get(key)!.item);
208
+ hasChanged = hasChanged || diff.removed.length > 0;
209
+
210
+ return {
211
+ diff,
212
+ hasChanged,
213
+ map: currentMap
214
+ };
215
+ }
216
+
217
+ protected async linkQuery(options: LinkQueryOptions<WatchedQueryDifferential<RowType>>): Promise<void> {
218
+ const { db, watchOptions } = this.options;
219
+ const { abortSignal } = options;
220
+
221
+ const compiledQuery = watchOptions.query.compile();
222
+ const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[], {
223
+ tables: options.settings.triggerOnTables
224
+ });
225
+
226
+ let currentMap: DataHashMap<RowType> = new Map();
227
+
228
+ // populate the currentMap from the placeholder data
229
+ this.state.data.forEach((item) => {
230
+ currentMap.set(this.comparator.keyBy(item), {
231
+ hash: this.comparator.compareBy(item),
232
+ item
233
+ });
234
+ });
235
+
236
+ db.onChangeWithCallback(
237
+ {
238
+ onChange: async () => {
239
+ if (this.closed || abortSignal.aborted) {
240
+ return;
241
+ }
242
+ // This fires for each change of the relevant tables
243
+ try {
244
+ if (this.reportFetching && !this.state.isFetching) {
245
+ await this.updateState({ isFetching: true });
246
+ }
247
+
248
+ const partialStateUpdate: Partial<MutableWatchedQueryState<RowType[]>> = {};
249
+
250
+ // Always run the query if an underlying table has changed
251
+ const result = await watchOptions.query.execute({
252
+ sql: compiledQuery.sql,
253
+ // Allows casting from ReadOnlyArray[unknown] to Array<unknown>
254
+ // This allows simpler compatibility with PowerSync queries
255
+ parameters: [...compiledQuery.parameters],
256
+ db: this.options.db
257
+ });
258
+
259
+ if (abortSignal.aborted) {
260
+ return;
261
+ }
262
+
263
+ if (this.reportFetching) {
264
+ partialStateUpdate.isFetching = false;
265
+ }
266
+
267
+ if (this.state.isLoading) {
268
+ partialStateUpdate.isLoading = false;
269
+ }
270
+
271
+ const { diff, hasChanged, map } = this.differentiate(result, currentMap);
272
+ // Update for future comparisons
273
+ currentMap = map;
274
+
275
+ if (hasChanged) {
276
+ await this.iterateAsyncListenersWithError((l) => l.onDiff?.(diff));
277
+ Object.assign(partialStateUpdate, {
278
+ data: diff.all
279
+ });
280
+ }
281
+
282
+ if (this.state.error) {
283
+ partialStateUpdate.error = null;
284
+ }
285
+
286
+ if (Object.keys(partialStateUpdate).length > 0) {
287
+ await this.updateState(partialStateUpdate);
288
+ }
289
+ } catch (error) {
290
+ await this.updateState({ error });
291
+ }
292
+ },
293
+ onError: async (error) => {
294
+ await this.updateState({ error });
295
+ }
296
+ },
297
+ {
298
+ signal: abortSignal,
299
+ tables,
300
+ throttleMs: watchOptions.throttleMs,
301
+ triggerImmediate: true // used to emit the initial state
302
+ }
303
+ );
304
+ }
305
+ }
@@ -0,0 +1,122 @@
1
+ import { WatchCompatibleQuery, WatchedQuery, WatchedQueryOptions } from '../WatchedQuery.js';
2
+ import {
3
+ AbstractQueryProcessor,
4
+ AbstractQueryProcessorOptions,
5
+ LinkQueryOptions,
6
+ MutableWatchedQueryState
7
+ } from './AbstractQueryProcessor.js';
8
+ import { WatchedQueryComparator } from './comparators.js';
9
+
10
+ /**
11
+ * Settings for {@link WatchedQuery} instances created via {@link Query#watch}.
12
+ */
13
+ export interface WatchedQuerySettings<DataType> extends WatchedQueryOptions {
14
+ query: WatchCompatibleQuery<DataType>;
15
+ }
16
+
17
+ /**
18
+ * {@link WatchedQuery} returned from {@link Query#watch}.
19
+ */
20
+ export type StandardWatchedQuery<DataType> = WatchedQuery<DataType, WatchedQuerySettings<DataType>>;
21
+
22
+ /**
23
+ * @internal
24
+ */
25
+ export interface OnChangeQueryProcessorOptions<Data>
26
+ extends AbstractQueryProcessorOptions<Data, WatchedQuerySettings<Data>> {
27
+ comparator?: WatchedQueryComparator<Data>;
28
+ }
29
+
30
+ /**
31
+ * Uses the PowerSync onChange event to trigger watched queries.
32
+ * Results are emitted on every change of the relevant tables.
33
+ * @internal
34
+ */
35
+ export class OnChangeQueryProcessor<Data> extends AbstractQueryProcessor<Data, WatchedQuerySettings<Data>> {
36
+ constructor(protected options: OnChangeQueryProcessorOptions<Data>) {
37
+ super(options);
38
+ }
39
+
40
+ /**
41
+ * @returns If the sets are equal
42
+ */
43
+ protected checkEquality(current: Data, previous: Data): boolean {
44
+ // Use the provided comparator if available. Assume values are unique if not available.
45
+ return this.options.comparator?.checkEquality?.(current, previous) ?? false;
46
+ }
47
+
48
+ protected async linkQuery(options: LinkQueryOptions<Data>): Promise<void> {
49
+ const { db, watchOptions } = this.options;
50
+ const { abortSignal } = options;
51
+
52
+ const compiledQuery = watchOptions.query.compile();
53
+ const tables = await db.resolveTables(compiledQuery.sql, compiledQuery.parameters as any[], {
54
+ tables: options.settings.triggerOnTables
55
+ });
56
+
57
+ db.onChangeWithCallback(
58
+ {
59
+ onChange: async () => {
60
+ if (this.closed || abortSignal.aborted) {
61
+ return;
62
+ }
63
+ // This fires for each change of the relevant tables
64
+ try {
65
+ if (this.reportFetching && !this.state.isFetching) {
66
+ await this.updateState({ isFetching: true });
67
+ }
68
+
69
+ const partialStateUpdate: Partial<MutableWatchedQueryState<Data>> & { data?: Data } = {};
70
+
71
+ // Always run the query if an underlying table has changed
72
+ const result = await watchOptions.query.execute({
73
+ sql: compiledQuery.sql,
74
+ // Allows casting from ReadOnlyArray[unknown] to Array<unknown>
75
+ // This allows simpler compatibility with PowerSync queries
76
+ parameters: [...compiledQuery.parameters],
77
+ db: this.options.db
78
+ });
79
+
80
+ if (abortSignal.aborted) {
81
+ return;
82
+ }
83
+
84
+ if (this.reportFetching) {
85
+ partialStateUpdate.isFetching = false;
86
+ }
87
+
88
+ if (this.state.isLoading) {
89
+ partialStateUpdate.isLoading = false;
90
+ }
91
+
92
+ // Check if the result has changed
93
+ if (!this.checkEquality(result, this.state.data)) {
94
+ Object.assign(partialStateUpdate, {
95
+ data: result
96
+ });
97
+ }
98
+
99
+ if (this.state.error) {
100
+ partialStateUpdate.error = null;
101
+ }
102
+
103
+ if (Object.keys(partialStateUpdate).length > 0) {
104
+ await this.updateState(partialStateUpdate);
105
+ }
106
+ } catch (error) {
107
+ await this.updateState({ error });
108
+ }
109
+ },
110
+ onError: async (error) => {
111
+ await this.updateState({ error });
112
+ }
113
+ },
114
+ {
115
+ signal: abortSignal,
116
+ tables,
117
+ throttleMs: watchOptions.throttleMs,
118
+ triggerImmediate: true // used to emit the initial state
119
+ }
120
+ );
121
+ }
122
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A basic comparator for incrementally watched queries. This performs a single comparison which
3
+ * determines if the result set has changed. The {@link WatchedQuery} will only emit the new result
4
+ * if a change has been detected.
5
+ */
6
+ export interface WatchedQueryComparator<Data> {
7
+ checkEquality: (current: Data, previous: Data) => boolean;
8
+ }
9
+
10
+ /**
11
+ * Options for {@link ArrayComparator}
12
+ */
13
+ export type ArrayComparatorOptions<ItemType> = {
14
+ /**
15
+ * Returns a string to uniquely identify an item in the array.
16
+ */
17
+ compareBy: (item: ItemType) => string;
18
+ };
19
+
20
+ /**
21
+ * An efficient comparator for {@link WatchedQuery} created with {@link Query#watch}. This has the ability to determine if a query
22
+ * result has changes without necessarily processing all items in the result.
23
+ */
24
+ export class ArrayComparator<ItemType> implements WatchedQueryComparator<ItemType[]> {
25
+ constructor(protected options: ArrayComparatorOptions<ItemType>) {}
26
+
27
+ checkEquality(current: ItemType[], previous: ItemType[]) {
28
+ if (current.length === 0 && previous.length === 0) {
29
+ return true;
30
+ }
31
+
32
+ if (current.length !== previous.length) {
33
+ return false;
34
+ }
35
+
36
+ const { compareBy } = this.options;
37
+
38
+ // At this point the lengths are equal
39
+ for (let i = 0; i < current.length; i++) {
40
+ const currentItem = compareBy(current[i]);
41
+ const previousItem = compareBy(previous[i]);
42
+
43
+ if (currentItem !== previousItem) {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ return true;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Watched query comparator that always reports changed result sets.
54
+ */
55
+ export const FalsyComparator: WatchedQueryComparator<unknown> = {
56
+ checkEquality: () => false // Default comparator that always returns false
57
+ };