@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,39 @@
1
+ import Logger from 'js-logger';
2
+ import { DBAdapter } from '../db/DBAdapter.js';
3
+ import { Schema } from '../db/schema/Schema.js';
4
+ import { AbstractPowerSyncDatabase, PowerSyncDatabaseOptions } from './AbstractPowerSyncDatabase.js';
5
+ import { SQLOpenOptions } from './SQLOpenFactory.js';
6
+
7
+ export interface PowerSyncOpenFactoryOptions extends Partial<PowerSyncDatabaseOptions>, SQLOpenOptions {
8
+ /** Schema used for the local database. */
9
+ schema: Schema;
10
+ }
11
+
12
+ export abstract class AbstractPowerSyncDatabaseOpenFactory {
13
+ constructor(protected options: PowerSyncOpenFactoryOptions) {
14
+ options.logger = options.logger ?? Logger.get(`PowerSync ${this.options.dbFilename}`);
15
+ }
16
+
17
+ /**
18
+ * Schema used for the local database.
19
+ */
20
+ get schema() {
21
+ return this.options.schema;
22
+ }
23
+
24
+ protected abstract openDB(): DBAdapter;
25
+
26
+ generateOptions(): PowerSyncDatabaseOptions {
27
+ return {
28
+ database: this.openDB(),
29
+ ...this.options
30
+ };
31
+ }
32
+
33
+ abstract generateInstance(options: PowerSyncDatabaseOptions): AbstractPowerSyncDatabase;
34
+
35
+ getInstance(): AbstractPowerSyncDatabase {
36
+ const options = this.generateOptions();
37
+ return this.generateInstance(options);
38
+ }
39
+ }
@@ -0,0 +1,402 @@
1
+ import { ILogger } from 'js-logger';
2
+ import { BaseListener, BaseObserver } from '../utils/BaseObserver.js';
3
+ import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
4
+ import {
5
+ AdditionalConnectionOptions,
6
+ InternalConnectionOptions,
7
+ StreamingSyncImplementation,
8
+ SubscribedStream
9
+ } from './sync/stream/AbstractStreamingSyncImplementation.js';
10
+ import {
11
+ SyncStream,
12
+ SyncStreamDescription,
13
+ SyncStreamSubscribeOptions,
14
+ SyncStreamSubscription
15
+ } from './sync/sync-streams.js';
16
+ import { SyncStatus } from '../db/crud/SyncStatus.js';
17
+
18
+ /**
19
+ * @internal
20
+ */
21
+ export interface ConnectionManagerSyncImplementationResult {
22
+ sync: StreamingSyncImplementation;
23
+ /**
24
+ * Additional cleanup function which is called after the sync stream implementation
25
+ * is disposed.
26
+ */
27
+ onDispose: () => Promise<void> | void;
28
+ }
29
+
30
+ /**
31
+ * The subset of {@link AbstractStreamingSyncImplementationOptions} managed by the connection manager.
32
+ *
33
+ * @internal
34
+ */
35
+ export interface CreateSyncImplementationOptions extends AdditionalConnectionOptions {
36
+ subscriptions: SubscribedStream[];
37
+ }
38
+
39
+ export interface InternalSubscriptionAdapter {
40
+ firstStatusMatching(predicate: (status: SyncStatus) => any, abort?: AbortSignal): Promise<void>;
41
+ resolveOfflineSyncStatus(): Promise<void>;
42
+ rustSubscriptionsCommand(payload: any): Promise<void>;
43
+ }
44
+
45
+ /**
46
+ * @internal
47
+ */
48
+ export interface ConnectionManagerOptions {
49
+ createSyncImplementation(
50
+ connector: PowerSyncBackendConnector,
51
+ options: CreateSyncImplementationOptions
52
+ ): Promise<ConnectionManagerSyncImplementationResult>;
53
+
54
+ logger: ILogger;
55
+ }
56
+
57
+ type StoredConnectionOptions = {
58
+ connector: PowerSyncBackendConnector;
59
+ options: InternalConnectionOptions;
60
+ };
61
+
62
+ /**
63
+ * @internal
64
+ */
65
+ export interface ConnectionManagerListener extends BaseListener {
66
+ syncStreamCreated: (sync: StreamingSyncImplementation) => void;
67
+ }
68
+
69
+ /**
70
+ * @internal
71
+ */
72
+ export class ConnectionManager extends BaseObserver<ConnectionManagerListener> {
73
+ /**
74
+ * Tracks active connection attempts
75
+ */
76
+ protected connectingPromise: Promise<void> | null;
77
+ /**
78
+ * Tracks actively instantiating a streaming sync implementation.
79
+ */
80
+ protected syncStreamInitPromise: Promise<void> | null;
81
+ /**
82
+ * Active disconnect operation. Calling disconnect multiple times
83
+ * will resolve to the same operation.
84
+ */
85
+ protected disconnectingPromise: Promise<void> | null;
86
+ /**
87
+ * Tracks the last parameters supplied to `connect` calls.
88
+ * Calling `connect` multiple times in succession will result in:
89
+ * - 1 pending connection operation which will be aborted.
90
+ * - updating the last set of parameters while waiting for the pending
91
+ * attempt to be aborted
92
+ * - internally connecting with the last set of parameters
93
+ */
94
+ protected pendingConnectionOptions: StoredConnectionOptions | null;
95
+
96
+ syncStreamImplementation: StreamingSyncImplementation | null;
97
+
98
+ /**
99
+ * Additional cleanup function which is called after the sync stream implementation
100
+ * is disposed.
101
+ */
102
+ protected syncDisposer: (() => Promise<void> | void) | null;
103
+
104
+ /**
105
+ * Subscriptions managed in this connection manager.
106
+ *
107
+ * On the web, these local subscriptions are merged across tabs by a shared worker.
108
+ */
109
+ private locallyActiveSubscriptions = new Map<string, ActiveSubscription>();
110
+
111
+ constructor(protected options: ConnectionManagerOptions) {
112
+ super();
113
+ this.connectingPromise = null;
114
+ this.syncStreamInitPromise = null;
115
+ this.disconnectingPromise = null;
116
+ this.pendingConnectionOptions = null;
117
+ this.syncStreamImplementation = null;
118
+ this.syncDisposer = null;
119
+ }
120
+
121
+ get connector() {
122
+ return this.pendingConnectionOptions?.connector ?? null;
123
+ }
124
+
125
+ get connectionOptions() {
126
+ return this.pendingConnectionOptions?.options ?? null;
127
+ }
128
+
129
+ get logger() {
130
+ return this.options.logger;
131
+ }
132
+
133
+ async close() {
134
+ await this.syncStreamImplementation?.dispose();
135
+ await this.syncDisposer?.();
136
+ }
137
+
138
+ async connect(connector: PowerSyncBackendConnector, options: InternalConnectionOptions) {
139
+ // Keep track if there were pending operations before this call
140
+ const hadPendingOptions = !!this.pendingConnectionOptions;
141
+
142
+ // Update pending options to the latest values
143
+ this.pendingConnectionOptions = {
144
+ connector,
145
+ options
146
+ };
147
+
148
+ // Disconnecting here provides aborting in progress connection attempts.
149
+ // The connectInternal method will clear pending options once it starts connecting (with the options).
150
+ // We only need to trigger a disconnect here if we have already reached the point of connecting.
151
+ // If we do already have pending options, a disconnect has already been performed.
152
+ // The connectInternal method also does a sanity disconnect to prevent straggler connections.
153
+ // We should also disconnect if we have already completed a connection attempt.
154
+ if (!hadPendingOptions || this.syncStreamImplementation) {
155
+ await this.disconnectInternal();
156
+ }
157
+
158
+ // Triggers a connect which checks if pending options are available after the connect completes.
159
+ // The completion can be for a successful, unsuccessful or aborted connection attempt.
160
+ // If pending options are available another connection will be triggered.
161
+ const checkConnection = async (): Promise<void> => {
162
+ if (this.pendingConnectionOptions) {
163
+ // Pending options have been placed while connecting.
164
+ // Need to reconnect.
165
+ this.connectingPromise = this.connectInternal()
166
+ .catch(() => {})
167
+ .finally(checkConnection);
168
+ return this.connectingPromise;
169
+ } else {
170
+ // Clear the connecting promise, done.
171
+ this.connectingPromise = null;
172
+ return;
173
+ }
174
+ };
175
+
176
+ this.connectingPromise ??= this.connectInternal()
177
+ .catch(() => {})
178
+ .finally(checkConnection);
179
+ return this.connectingPromise;
180
+ }
181
+
182
+ protected async connectInternal() {
183
+ let appliedOptions: InternalConnectionOptions | null = null;
184
+
185
+ // This method ensures a disconnect before any connection attempt
186
+ await this.disconnectInternal();
187
+
188
+ /**
189
+ * This portion creates a sync implementation which can be racy when disconnecting or
190
+ * if multiple tabs on web are in use.
191
+ * This is protected in an exclusive lock.
192
+ * The promise tracks the creation which is used to synchronize disconnect attempts.
193
+ */
194
+ this.syncStreamInitPromise = new Promise(async (resolve, reject) => {
195
+ try {
196
+ if (!this.pendingConnectionOptions) {
197
+ this.logger.debug('No pending connection options found, not creating sync stream implementation');
198
+ // A disconnect could have cleared this.
199
+ resolve();
200
+ return;
201
+ }
202
+
203
+ if (this.disconnectingPromise) {
204
+ resolve();
205
+ return;
206
+ }
207
+
208
+ const { connector, options } = this.pendingConnectionOptions;
209
+ appliedOptions = options;
210
+
211
+ this.pendingConnectionOptions = null;
212
+
213
+ const { sync, onDispose } = await this.options.createSyncImplementation(connector, {
214
+ subscriptions: this.activeStreams,
215
+ ...options
216
+ });
217
+ this.iterateListeners((l) => l.syncStreamCreated?.(sync));
218
+ this.syncStreamImplementation = sync;
219
+ this.syncDisposer = onDispose;
220
+ await this.syncStreamImplementation.waitForReady();
221
+ resolve();
222
+ } catch (error) {
223
+ reject(error);
224
+ }
225
+ });
226
+
227
+ await this.syncStreamInitPromise;
228
+ this.syncStreamInitPromise = null;
229
+
230
+ if (!appliedOptions) {
231
+ // A disconnect could have cleared the options which did not create a syncStreamImplementation
232
+ return;
233
+ }
234
+
235
+ // It might be possible that a disconnect triggered between the last check
236
+ // and this point. Awaiting here allows the sync stream to be cleared if disconnected.
237
+ await this.disconnectingPromise;
238
+
239
+ this.logger.debug('Attempting to connect to PowerSync instance');
240
+ await this.syncStreamImplementation?.connect(appliedOptions!);
241
+ }
242
+
243
+ /**
244
+ * Close the sync connection.
245
+ *
246
+ * Use {@link connect} to connect again.
247
+ */
248
+ async disconnect() {
249
+ // This will help abort pending connects
250
+ this.pendingConnectionOptions = null;
251
+ await this.disconnectInternal();
252
+ }
253
+
254
+ protected async disconnectInternal(): Promise<void> {
255
+ if (this.disconnectingPromise) {
256
+ // A disconnect is already in progress
257
+ return this.disconnectingPromise;
258
+ }
259
+
260
+ this.disconnectingPromise = this.performDisconnect();
261
+
262
+ await this.disconnectingPromise;
263
+ this.disconnectingPromise = null;
264
+ }
265
+
266
+ protected async performDisconnect() {
267
+ // Wait if a sync stream implementation is being created before closing it
268
+ // (syncStreamImplementation must be assigned before we can properly dispose it)
269
+ await this.syncStreamInitPromise;
270
+
271
+ // Keep reference to the sync stream implementation and disposer
272
+ // The class members will be cleared before we trigger the disconnect
273
+ // to prevent any further calls to the sync stream implementation.
274
+ const sync = this.syncStreamImplementation;
275
+ this.syncStreamImplementation = null;
276
+ const disposer = this.syncDisposer;
277
+ this.syncDisposer = null;
278
+
279
+ await sync?.disconnect();
280
+ await sync?.dispose();
281
+ await disposer?.();
282
+ }
283
+
284
+ stream(adapter: InternalSubscriptionAdapter, name: string, parameters: Record<string, any> | null): SyncStream {
285
+ const desc = { name, parameters } satisfies SyncStreamDescription;
286
+
287
+ const waitForFirstSync = (abort?: AbortSignal) => {
288
+ return adapter.firstStatusMatching((s) => s.forStream(desc)?.subscription.hasSynced, abort);
289
+ };
290
+
291
+ return {
292
+ ...desc,
293
+ subscribe: async (options?: SyncStreamSubscribeOptions) => {
294
+ // NOTE: We also run this command if a subscription already exists, because this increases the expiry date
295
+ // (relevant if the app is closed before connecting again, where the last subscribe call determines the ttl).
296
+ await adapter.rustSubscriptionsCommand({
297
+ subscribe: {
298
+ stream: {
299
+ name,
300
+ params: parameters
301
+ },
302
+ ttl: options?.ttl,
303
+ priority: options?.priority
304
+ }
305
+ });
306
+
307
+ if (!this.syncStreamImplementation) {
308
+ // We're not connected. So, update the offline sync status to reflect the new subscription.
309
+ // (With an active iteration, the sync client would include it in its state).
310
+ await adapter.resolveOfflineSyncStatus();
311
+ }
312
+
313
+ const key = `${name}|${JSON.stringify(parameters)}`;
314
+ let subscription = this.locallyActiveSubscriptions.get(key);
315
+ if (subscription == null) {
316
+ const clearSubscription = () => {
317
+ this.locallyActiveSubscriptions.delete(key);
318
+ this.subscriptionsMayHaveChanged();
319
+ };
320
+
321
+ subscription = new ActiveSubscription(name, parameters, this.logger, waitForFirstSync, clearSubscription);
322
+ this.locallyActiveSubscriptions.set(key, subscription);
323
+ this.subscriptionsMayHaveChanged();
324
+ }
325
+
326
+ return new SyncStreamSubscriptionHandle(subscription);
327
+ },
328
+ unsubscribeAll: async () => {
329
+ await adapter.rustSubscriptionsCommand({ unsubscribe: { name, params: parameters } });
330
+ this.subscriptionsMayHaveChanged();
331
+ }
332
+ };
333
+ }
334
+
335
+ /**
336
+ * @internal exposed for testing
337
+ */
338
+ get activeStreams() {
339
+ return [...this.locallyActiveSubscriptions.values()].map((a) => ({ name: a.name, params: a.parameters }));
340
+ }
341
+
342
+ private subscriptionsMayHaveChanged() {
343
+ this.syncStreamImplementation?.updateSubscriptions(this.activeStreams);
344
+ }
345
+ }
346
+
347
+ class ActiveSubscription {
348
+ refcount: number = 0;
349
+
350
+ constructor(
351
+ readonly name: string,
352
+ readonly parameters: Record<string, any> | null,
353
+ readonly logger: ILogger,
354
+ readonly waitForFirstSync: (abort?: AbortSignal) => Promise<void>,
355
+ private clearSubscription: () => void
356
+ ) {}
357
+
358
+ decrementRefCount() {
359
+ this.refcount--;
360
+ if (this.refcount == 0) {
361
+ this.clearSubscription();
362
+ }
363
+ }
364
+ }
365
+
366
+ class SyncStreamSubscriptionHandle implements SyncStreamSubscription {
367
+ private active: boolean = true;
368
+
369
+ constructor(readonly subscription: ActiveSubscription) {
370
+ subscription.refcount++;
371
+ _finalizer?.register(this, subscription);
372
+ }
373
+
374
+ get name() {
375
+ return this.subscription.name;
376
+ }
377
+
378
+ get parameters() {
379
+ return this.subscription.parameters;
380
+ }
381
+
382
+ waitForFirstSync(abort?: AbortSignal): Promise<void> {
383
+ return this.subscription.waitForFirstSync(abort);
384
+ }
385
+
386
+ unsubscribe(): void {
387
+ if (this.active) {
388
+ this.active = false;
389
+ _finalizer?.unregister(this);
390
+ this.subscription.decrementRefCount();
391
+ }
392
+ }
393
+ }
394
+
395
+ const _finalizer =
396
+ 'FinalizationRegistry' in globalThis
397
+ ? new FinalizationRegistry<ActiveSubscription>((sub) => {
398
+ sub.logger.warn(
399
+ `A subscription to ${sub.name} with params ${JSON.stringify(sub.parameters)} leaked! Please ensure calling unsubscribe() when you don't need a subscription anymore. For global subscriptions, consider storing them in global fields to avoid this warning.`
400
+ );
401
+ })
402
+ : null;
@@ -0,0 +1,56 @@
1
+ import { AbstractPowerSyncDatabase } from './AbstractPowerSyncDatabase.js';
2
+ import { Query, StandardWatchedQueryOptions } from './Query.js';
3
+ import { FalsyComparator } from './watched/processors/comparators.js';
4
+ import {
5
+ DifferentialQueryProcessor,
6
+ DifferentialWatchedQueryOptions
7
+ } from './watched/processors/DifferentialQueryProcessor.js';
8
+ import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
9
+ import { DEFAULT_WATCH_QUERY_OPTIONS, WatchCompatibleQuery, WatchedQueryOptions } from './watched/WatchedQuery.js';
10
+
11
+ /**
12
+ * @internal
13
+ */
14
+ export interface CustomQueryOptions<RowType> {
15
+ db: AbstractPowerSyncDatabase;
16
+ query: WatchCompatibleQuery<RowType[]>;
17
+ }
18
+
19
+ /**
20
+ * @internal
21
+ */
22
+ export class CustomQuery<RowType> implements Query<RowType> {
23
+ constructor(protected options: CustomQueryOptions<RowType>) {}
24
+
25
+ protected resolveOptions(options: WatchedQueryOptions): WatchedQueryOptions {
26
+ return {
27
+ reportFetching: options?.reportFetching ?? DEFAULT_WATCH_QUERY_OPTIONS.reportFetching,
28
+ throttleMs: options?.throttleMs ?? DEFAULT_WATCH_QUERY_OPTIONS.throttleMs,
29
+ triggerOnTables: options?.triggerOnTables
30
+ };
31
+ }
32
+
33
+ watch(watchOptions: StandardWatchedQueryOptions<RowType>) {
34
+ return new OnChangeQueryProcessor<RowType[]>({
35
+ db: this.options.db,
36
+ comparator: watchOptions?.comparator ?? FalsyComparator,
37
+ placeholderData: watchOptions?.placeholderData ?? [],
38
+ watchOptions: {
39
+ ...this.resolveOptions(watchOptions),
40
+ query: this.options.query
41
+ }
42
+ });
43
+ }
44
+
45
+ differentialWatch(differentialWatchOptions: DifferentialWatchedQueryOptions<RowType>) {
46
+ return new DifferentialQueryProcessor<RowType>({
47
+ db: this.options.db,
48
+ rowComparator: differentialWatchOptions?.rowComparator,
49
+ placeholderData: differentialWatchOptions?.placeholderData ?? [],
50
+ watchOptions: {
51
+ ...this.resolveOptions(differentialWatchOptions),
52
+ query: this.options.query
53
+ }
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,106 @@
1
+ import { WatchedQueryComparator } from './watched/processors/comparators.js';
2
+ import {
3
+ DifferentialWatchedQuery,
4
+ DifferentialWatchedQueryOptions
5
+ } from './watched/processors/DifferentialQueryProcessor.js';
6
+ import { StandardWatchedQuery } from './watched/processors/OnChangeQueryProcessor.js';
7
+ import { WatchedQueryOptions } from './watched/WatchedQuery.js';
8
+
9
+ /**
10
+ * Query parameters for {@link ArrayQueryDefinition#parameters}
11
+ */
12
+ export type QueryParam = string | number | boolean | null | undefined | bigint | Uint8Array;
13
+
14
+ /**
15
+ * Options for building a query with {@link AbstractPowerSyncDatabase#query}.
16
+ * This query will be executed with {@link AbstractPowerSyncDatabase#getAll}.
17
+ */
18
+ export interface ArrayQueryDefinition<RowType = unknown> {
19
+ sql: string;
20
+ parameters?: ReadonlyArray<Readonly<QueryParam>>;
21
+ /**
22
+ * Maps the raw SQLite row to a custom typed object.
23
+ * @example
24
+ * ```javascript
25
+ * mapper: (row) => ({
26
+ * ...row,
27
+ * created_at: new Date(row.created_at as string),
28
+ * })
29
+ * ```
30
+ */
31
+ mapper?: (row: Record<string, unknown>) => RowType;
32
+ }
33
+
34
+ /**
35
+ * Options for {@link Query#watch}.
36
+ */
37
+ export interface StandardWatchedQueryOptions<RowType> extends WatchedQueryOptions {
38
+ /**
39
+ * The underlying watched query implementation (re)evaluates the query on any SQLite table change.
40
+ *
41
+ * Providing this optional comparator can be used to filter duplicate result set emissions when the result set is unchanged.
42
+ * The comparator compares the previous and current result set.
43
+ *
44
+ * For an efficient comparator see {@link ArrayComparator}.
45
+ *
46
+ * @example
47
+ * ```javascript
48
+ * comparator: new ArrayComparator({
49
+ * compareBy: (item) => JSON.stringify(item)
50
+ * })
51
+ * ```
52
+ */
53
+ comparator?: WatchedQueryComparator<RowType[]>;
54
+
55
+ /**
56
+ * The initial data state reported while the query is loading for the first time.
57
+ * @default []
58
+ */
59
+ placeholderData?: RowType[];
60
+ }
61
+
62
+ export interface Query<RowType> {
63
+ /**
64
+ * Creates a {@link WatchedQuery} which watches and emits results of the linked query.
65
+ *
66
+ * By default the returned watched query will emit changes whenever a change to the underlying SQLite tables is made.
67
+ * These changes might not be relevant to the query, but the query will emit a new result set.
68
+ *
69
+ * A {@link StandardWatchedQueryOptions#comparator} can be provided to limit the data emissions. The watched query will still
70
+ * query the underlying DB on underlying table changes, but the result will only be emitted if the comparator detects a change in the results.
71
+ *
72
+ * The comparator in this method is optimized and returns early as soon as it detects a change. Each data emission will correlate to a change in the result set,
73
+ * but note that the result set will not maintain internal object references to the previous result set. If internal object references are needed,
74
+ * consider using {@link Query#differentialWatch} instead.
75
+ */
76
+ watch(options?: StandardWatchedQueryOptions<RowType>): StandardWatchedQuery<ReadonlyArray<Readonly<RowType>>>;
77
+
78
+ /**
79
+ * Creates a {@link WatchedQuery} which watches and emits results of the linked query.
80
+ *
81
+ * This query method watches for changes in the underlying SQLite tables and runs the query on each table change.
82
+ * The difference between the current and previous result set is computed.
83
+ * The watched query will not emit changes if the result set is identical to the previous result set.
84
+ *
85
+ * If the result set is different, the watched query will emit the new result set and emit a detailed diff of the changes via the `onData` and `onDiff` listeners.
86
+ *
87
+ * The deep differentiation allows maintaining result set object references between result emissions.
88
+ * The {@link DifferentialWatchedQuery#state} `data` array will contain the previous row references for unchanged rows.
89
+ *
90
+ * @example
91
+ * ```javascript
92
+ * const watchedLists = powerSync.query({sql: 'SELECT * FROM lists'})
93
+ * .differentialWatch();
94
+ *
95
+ * const disposeListener = watchedLists.registerListener({
96
+ * onData: (lists) => {
97
+ * console.log('The latest result set for the query is', lists);
98
+ * },
99
+ * onDiff: (diff) => {
100
+ * console.log('The lists result set has changed since the last emission', diff.added, diff.removed, diff.updated, diff.all)
101
+ * }
102
+ * })
103
+ * ```
104
+ */
105
+ differentialWatch(options?: DifferentialWatchedQueryOptions<RowType>): DifferentialWatchedQuery<RowType>;
106
+ }
@@ -0,0 +1,55 @@
1
+ import { DBAdapter } from '../db/DBAdapter.js';
2
+
3
+ export interface SQLOpenOptions {
4
+ /**
5
+ * Filename for the database.
6
+ */
7
+ dbFilename: string;
8
+ /**
9
+ * Directory where the database file is located.
10
+ *
11
+ * When set, the directory must exist when the database is opened, it will
12
+ * not be created automatically.
13
+ */
14
+ dbLocation?: string;
15
+
16
+ /**
17
+ * Enable debugMode to log queries to the performance timeline.
18
+ *
19
+ * Defaults to false.
20
+ *
21
+ * To enable in development builds, use:
22
+ *
23
+ * debugMode: process.env.NODE_ENV !== 'production'
24
+ */
25
+ debugMode?: boolean;
26
+ }
27
+
28
+ export interface SQLOpenFactory {
29
+ /**
30
+ * Opens a connection adapter to a SQLite DB
31
+ */
32
+ openDB(): DBAdapter;
33
+ }
34
+
35
+ /**
36
+ * Tests if the input is a {@link SQLOpenOptions}
37
+ */
38
+ export const isSQLOpenOptions = (test: any): test is SQLOpenOptions => {
39
+ // typeof null is `object`, but you cannot use the `in` operator on `null.
40
+ return test && typeof test == 'object' && 'dbFilename' in test;
41
+ };
42
+
43
+ /**
44
+ * Tests if input is a {@link SQLOpenFactory}
45
+ */
46
+ export const isSQLOpenFactory = (test: any): test is SQLOpenFactory => {
47
+ return typeof test?.openDB == 'function';
48
+ };
49
+
50
+ /**
51
+ * Tests if input is a {@link DBAdapter}
52
+ */
53
+ export const isDBAdapter = (test: any): test is DBAdapter => {
54
+ return typeof test?.writeTransaction == 'function';
55
+ };