@powersync/common 1.33.2 → 1.35.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 (42) hide show
  1. package/dist/bundle.cjs +5 -5
  2. package/dist/bundle.mjs +3 -3
  3. package/lib/client/AbstractPowerSyncDatabase.d.ts +58 -13
  4. package/lib/client/AbstractPowerSyncDatabase.js +107 -50
  5. package/lib/client/ConnectionManager.d.ts +4 -4
  6. package/lib/client/CustomQuery.d.ts +22 -0
  7. package/lib/client/CustomQuery.js +42 -0
  8. package/lib/client/Query.d.ts +97 -0
  9. package/lib/client/Query.js +1 -0
  10. package/lib/client/sync/bucket/BucketStorageAdapter.d.ts +2 -2
  11. package/lib/client/sync/bucket/SqliteBucketStorage.d.ts +1 -3
  12. package/lib/client/sync/bucket/SqliteBucketStorage.js +15 -17
  13. package/lib/client/sync/stream/AbstractRemote.d.ts +1 -10
  14. package/lib/client/sync/stream/AbstractRemote.js +31 -35
  15. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +13 -8
  16. package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +112 -82
  17. package/lib/client/sync/stream/streaming-sync-types.d.ts +4 -0
  18. package/lib/client/watched/GetAllQuery.d.ts +32 -0
  19. package/lib/client/watched/GetAllQuery.js +24 -0
  20. package/lib/client/watched/WatchedQuery.d.ts +98 -0
  21. package/lib/client/watched/WatchedQuery.js +12 -0
  22. package/lib/client/watched/processors/AbstractQueryProcessor.d.ts +67 -0
  23. package/lib/client/watched/processors/AbstractQueryProcessor.js +135 -0
  24. package/lib/client/watched/processors/DifferentialQueryProcessor.d.ts +121 -0
  25. package/lib/client/watched/processors/DifferentialQueryProcessor.js +166 -0
  26. package/lib/client/watched/processors/OnChangeQueryProcessor.d.ts +33 -0
  27. package/lib/client/watched/processors/OnChangeQueryProcessor.js +76 -0
  28. package/lib/client/watched/processors/comparators.d.ts +30 -0
  29. package/lib/client/watched/processors/comparators.js +34 -0
  30. package/lib/db/schema/RawTable.d.ts +61 -0
  31. package/lib/db/schema/RawTable.js +32 -0
  32. package/lib/db/schema/Schema.d.ts +14 -0
  33. package/lib/db/schema/Schema.js +20 -1
  34. package/lib/index.d.ts +8 -0
  35. package/lib/index.js +8 -0
  36. package/lib/utils/BaseObserver.d.ts +3 -4
  37. package/lib/utils/BaseObserver.js +3 -0
  38. package/lib/utils/MetaBaseObserver.d.ts +29 -0
  39. package/lib/utils/MetaBaseObserver.js +50 -0
  40. package/lib/utils/async.d.ts +0 -1
  41. package/lib/utils/async.js +0 -10
  42. package/package.json +1 -1
@@ -1,4 +1,3 @@
1
- import { Mutex } from 'async-mutex';
2
1
  import { ILogger } from 'js-logger';
3
2
  import { DBAdapter, Transaction } from '../../../db/DBAdapter.js';
4
3
  import { BaseObserver } from '../../../utils/BaseObserver.js';
@@ -8,13 +7,12 @@ import { CrudEntry } from './CrudEntry.js';
8
7
  import { SyncDataBatch } from './SyncDataBatch.js';
9
8
  export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListener> implements BucketStorageAdapter {
10
9
  private db;
11
- private mutex;
12
10
  private logger;
13
11
  tableNames: Set<string>;
14
12
  private _hasCompletedSync;
15
13
  private updateListener;
16
14
  private _clientId?;
17
- constructor(db: DBAdapter, mutex: Mutex, logger?: ILogger);
15
+ constructor(db: DBAdapter, logger?: ILogger);
18
16
  init(): Promise<void>;
19
17
  dispose(): Promise<void>;
20
18
  _getClientId(): Promise<string>;
@@ -6,16 +6,14 @@ import { PSInternalTable } from './BucketStorageAdapter.js';
6
6
  import { CrudEntry } from './CrudEntry.js';
7
7
  export class SqliteBucketStorage extends BaseObserver {
8
8
  db;
9
- mutex;
10
9
  logger;
11
10
  tableNames;
12
11
  _hasCompletedSync;
13
12
  updateListener;
14
13
  _clientId;
15
- constructor(db, mutex, logger = Logger.get('SqliteBucketStorage')) {
14
+ constructor(db, logger = Logger.get('SqliteBucketStorage')) {
16
15
  super();
17
16
  this.db = db;
18
- this.mutex = mutex;
19
17
  this.logger = logger;
20
18
  this._hasCompletedSync = false;
21
19
  this.tableNames = new Set();
@@ -66,11 +64,11 @@ export class SqliteBucketStorage extends BaseObserver {
66
64
  async saveSyncData(batch, fixedKeyFormat = false) {
67
65
  await this.writeTransaction(async (tx) => {
68
66
  for (const b of batch.buckets) {
69
- const result = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
67
+ await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
70
68
  'save',
71
69
  JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
72
70
  ]);
73
- this.logger.debug('saveSyncData', JSON.stringify(result));
71
+ this.logger.debug(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
74
72
  }
75
73
  });
76
74
  }
@@ -86,7 +84,7 @@ export class SqliteBucketStorage extends BaseObserver {
86
84
  await this.writeTransaction(async (tx) => {
87
85
  await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
88
86
  });
89
- this.logger.debug('done deleting bucket');
87
+ this.logger.debug(`Done deleting bucket ${bucket}`);
90
88
  }
91
89
  async hasCompletedSync() {
92
90
  if (this._hasCompletedSync) {
@@ -108,6 +106,12 @@ export class SqliteBucketStorage extends BaseObserver {
108
106
  }
109
107
  return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
110
108
  }
109
+ if (priority == null) {
110
+ this.logger.debug(`Validated checksums checkpoint ${checkpoint.last_op_id}`);
111
+ }
112
+ else {
113
+ this.logger.debug(`Validated checksums for partial checkpoint ${checkpoint.last_op_id}, priority ${priority}`);
114
+ }
111
115
  let buckets = checkpoint.buckets;
112
116
  if (priority !== undefined) {
113
117
  buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
@@ -124,7 +128,6 @@ export class SqliteBucketStorage extends BaseObserver {
124
128
  });
125
129
  const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
126
130
  if (!valid) {
127
- this.logger.debug('Not at a consistent checkpoint - cannot update local db');
128
131
  return { ready: false, checkpointValid: true };
129
132
  }
130
133
  return {
@@ -177,7 +180,6 @@ export class SqliteBucketStorage extends BaseObserver {
177
180
  JSON.stringify({ ...checkpoint })
178
181
  ]);
179
182
  const resultItem = rs.rows?.item(0);
180
- this.logger.debug('validateChecksums priority, checkpoint, result item', priority, checkpoint, resultItem);
181
183
  if (!resultItem) {
182
184
  return {
183
185
  checkpointValid: false,
@@ -210,30 +212,26 @@ export class SqliteBucketStorage extends BaseObserver {
210
212
  }
211
213
  const seqBefore = rs[0]['seq'];
212
214
  const opId = await cb();
213
- this.logger.debug(`[updateLocalTarget] Updating target to checkpoint ${opId}`);
214
215
  return this.writeTransaction(async (tx) => {
215
216
  const anyData = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
216
217
  if (anyData.rows?.length) {
217
218
  // if isNotEmpty
218
- this.logger.debug('updateLocalTarget', 'ps crud is not empty');
219
+ this.logger.debug(`New data uploaded since write checkpoint ${opId} - need new write checkpoint`);
219
220
  return false;
220
221
  }
221
222
  const rs = await tx.execute("SELECT seq FROM sqlite_sequence WHERE name = 'ps_crud'");
222
223
  if (!rs.rows?.length) {
223
224
  // assert isNotEmpty
224
- throw new Error('SQlite Sequence should not be empty');
225
+ throw new Error('SQLite Sequence should not be empty');
225
226
  }
226
227
  const seqAfter = rs.rows?.item(0)['seq'];
227
- this.logger.debug('seqAfter', JSON.stringify(rs.rows?.item(0)));
228
228
  if (seqAfter != seqBefore) {
229
- this.logger.debug('seqAfter != seqBefore', seqAfter, seqBefore);
229
+ this.logger.debug(`New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)`);
230
230
  // New crud data may have been uploaded since we got the checkpoint. Abort.
231
231
  return false;
232
232
  }
233
- const response = await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [
234
- opId
235
- ]);
236
- this.logger.debug(['[updateLocalTarget] Response from updating target_op ', JSON.stringify(response)]);
233
+ this.logger.debug(`Updating target write checkpoint to ${opId}`);
234
+ await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [opId]);
237
235
  return true;
238
236
  });
239
237
  }
@@ -3,7 +3,7 @@ import { type fetch } from 'cross-fetch';
3
3
  import Logger, { ILogger } from 'js-logger';
4
4
  import { DataStream } from '../../../utils/DataStream.js';
5
5
  import { PowerSyncCredentials } from '../../connection/PowerSyncCredentials.js';
6
- import { StreamingSyncLine, StreamingSyncRequest } from './streaming-sync-types.js';
6
+ import { StreamingSyncRequest } from './streaming-sync-types.js';
7
7
  export type BSONImplementation = typeof BSON;
8
8
  export type RemoteConnector = {
9
9
  fetchCredentials: () => Promise<PowerSyncCredentials | null>;
@@ -120,11 +120,6 @@ export declare abstract class AbstractRemote {
120
120
  */
121
121
  abstract getBSON(): Promise<BSONImplementation>;
122
122
  protected createSocket(url: string): WebSocket;
123
- /**
124
- * Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
125
- * sent by the server.
126
- */
127
- socketStream(options: SocketSyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
128
123
  /**
129
124
  * Returns a data stream of sync line data.
130
125
  *
@@ -133,10 +128,6 @@ export declare abstract class AbstractRemote {
133
128
  * (required for compatibility with older sync services).
134
129
  */
135
130
  socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Uint8Array) => T, bson?: typeof BSON): Promise<DataStream<T>>;
136
- /**
137
- * Connects to the sync/stream http endpoint, parsing lines as JSON.
138
- */
139
- postStream(options: SyncStreamOptions): Promise<DataStream<StreamingSyncLine>>;
140
131
  /**
141
132
  * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
142
133
  */
@@ -178,14 +178,6 @@ export class AbstractRemote {
178
178
  createSocket(url) {
179
179
  return new WebSocket(url);
180
180
  }
181
- /**
182
- * Connects to the sync/stream websocket endpoint and delivers sync lines by decoding the BSON events
183
- * sent by the server.
184
- */
185
- async socketStream(options) {
186
- const bson = await this.getBSON();
187
- return await this.socketStreamRaw(options, (data) => bson.deserialize(data), bson);
188
- }
189
181
  /**
190
182
  * Returns a data stream of sync line data.
191
183
  *
@@ -212,6 +204,22 @@ export class AbstractRemote {
212
204
  // headers with websockets on web. The browser userAgent is however added
213
205
  // automatically as a header.
214
206
  const userAgent = this.getUserAgent();
207
+ const stream = new DataStream({
208
+ logger: this.logger,
209
+ pressure: {
210
+ lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
211
+ },
212
+ mapLine: map
213
+ });
214
+ // Handle upstream abort
215
+ if (options.abortSignal?.aborted) {
216
+ throw new AbortOperation('Connection request aborted');
217
+ }
218
+ else {
219
+ options.abortSignal?.addEventListener('abort', () => {
220
+ stream.close();
221
+ }, { once: true });
222
+ }
215
223
  let keepAliveTimeout;
216
224
  const resetTimeout = () => {
217
225
  clearTimeout(keepAliveTimeout);
@@ -221,12 +229,22 @@ export class AbstractRemote {
221
229
  }, SOCKET_TIMEOUT_MS);
222
230
  };
223
231
  resetTimeout();
232
+ // Typescript complains about this being `never` if it's not assigned here.
233
+ // This is assigned in `wsCreator`.
234
+ let disposeSocketConnectionTimeout = () => { };
224
235
  const url = this.options.socketUrlTransformer(request.url);
225
236
  const connector = new RSocketConnector({
226
237
  transport: new WebsocketClientTransport({
227
238
  url,
228
239
  wsCreator: (url) => {
229
240
  const socket = this.createSocket(url);
241
+ disposeSocketConnectionTimeout = stream.registerListener({
242
+ closed: () => {
243
+ // Allow closing the underlying WebSocket if the stream was closed before the
244
+ // RSocket connect completed. This should effectively abort the request.
245
+ socket.close();
246
+ }
247
+ });
230
248
  socket.addEventListener('message', (event) => {
231
249
  resetTimeout();
232
250
  });
@@ -250,20 +268,18 @@ export class AbstractRemote {
250
268
  let rsocket;
251
269
  try {
252
270
  rsocket = await connector.connect();
271
+ // The connection is established, we no longer need to monitor the initial timeout
272
+ disposeSocketConnectionTimeout();
253
273
  }
254
274
  catch (ex) {
255
275
  this.logger.error(`Failed to connect WebSocket`, ex);
256
276
  clearTimeout(keepAliveTimeout);
277
+ if (!stream.closed) {
278
+ await stream.close();
279
+ }
257
280
  throw ex;
258
281
  }
259
282
  resetTimeout();
260
- const stream = new DataStream({
261
- logger: this.logger,
262
- pressure: {
263
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
264
- },
265
- mapLine: map
266
- });
267
283
  let socketIsClosed = false;
268
284
  const closeSocket = () => {
269
285
  clearTimeout(keepAliveTimeout);
@@ -349,28 +365,8 @@ export class AbstractRemote {
349
365
  l();
350
366
  }
351
367
  });
352
- /**
353
- * Handle abort operations here.
354
- * Unfortunately cannot insert them into the connection.
355
- */
356
- if (options.abortSignal?.aborted) {
357
- stream.close();
358
- }
359
- else {
360
- options.abortSignal?.addEventListener('abort', () => {
361
- stream.close();
362
- });
363
- }
364
368
  return stream;
365
369
  }
366
- /**
367
- * Connects to the sync/stream http endpoint, parsing lines as JSON.
368
- */
369
- async postStream(options) {
370
- return await this.postStreamRaw(options, (line) => {
371
- return JSON.parse(line);
372
- });
373
- }
374
370
  /**
375
371
  * Connects to the sync/stream http endpoint, mapping and emitting each received string line.
376
372
  */
@@ -1,6 +1,6 @@
1
- import Logger, { ILogger } from 'js-logger';
1
+ import { ILogger } from 'js-logger';
2
2
  import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
3
- import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver.js';
3
+ import { BaseListener, BaseObserver, BaseObserverInterface, Disposable } from '../../../utils/BaseObserver.js';
4
4
  import { BucketStorageAdapter } from '../bucket/BucketStorageAdapter.js';
5
5
  import { AbstractRemote, FetchStrategy } from './AbstractRemote.js';
6
6
  import { StreamingSyncRequestParameterType } from './streaming-sync-types.js';
@@ -88,7 +88,8 @@ export interface StreamingSyncImplementationListener extends BaseListener {
88
88
  * Configurable options to be used when connecting to the PowerSync
89
89
  * backend instance.
90
90
  */
91
- export interface PowerSyncConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {
91
+ export type PowerSyncConnectionOptions = Omit<InternalConnectionOptions, 'serializedSchema'>;
92
+ export interface InternalConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {
92
93
  }
93
94
  /** @internal */
94
95
  export interface BaseConnectionOptions {
@@ -114,6 +115,10 @@ export interface BaseConnectionOptions {
114
115
  * These parameters are passed to the sync rules, and will be available under the`user_parameters` object.
115
116
  */
116
117
  params?: Record<string, StreamingSyncRequestParameterType>;
118
+ /**
119
+ * The serialized schema - mainly used to forward information about raw tables to the sync client.
120
+ */
121
+ serializedSchema?: any;
117
122
  }
118
123
  /** @internal */
119
124
  export interface AdditionalConnectionOptions {
@@ -131,11 +136,11 @@ export interface AdditionalConnectionOptions {
131
136
  }
132
137
  /** @internal */
133
138
  export type RequiredAdditionalConnectionOptions = Required<AdditionalConnectionOptions>;
134
- export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncImplementationListener>, Disposable {
139
+ export interface StreamingSyncImplementation extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
135
140
  /**
136
141
  * Connects to the sync service
137
142
  */
138
- connect(options?: PowerSyncConnectionOptions): Promise<void>;
143
+ connect(options?: InternalConnectionOptions): Promise<void>;
139
144
  /**
140
145
  * Disconnects from the sync services.
141
146
  * @throws if not connected or if abort is not controlled internally
@@ -155,7 +160,6 @@ export declare const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
155
160
  export declare const DEFAULT_RETRY_DELAY_MS = 5000;
156
161
  export declare const DEFAULT_STREAMING_SYNC_OPTIONS: {
157
162
  retryDelayMs: number;
158
- logger: Logger.ILogger;
159
163
  crudUploadThrottleMs: number;
160
164
  };
161
165
  export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
@@ -164,9 +168,11 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
164
168
  protected _lastSyncedAt: Date | null;
165
169
  protected options: AbstractStreamingSyncImplementationOptions;
166
170
  protected abortController: AbortController | null;
171
+ protected uploadAbortController: AbortController | null;
167
172
  protected crudUpdateListener?: () => void;
168
173
  protected streamingSyncPromise?: Promise<void>;
169
- private pendingCrudUpload?;
174
+ protected logger: ILogger;
175
+ private isUploadingCrud;
170
176
  private notifyCompletedUploads?;
171
177
  syncStatus: SyncStatus;
172
178
  triggerCrudUpload: () => void;
@@ -176,7 +182,6 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
176
182
  waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
177
183
  get lastSyncedAt(): Date | undefined;
178
184
  get isConnected(): boolean;
179
- protected get logger(): Logger.ILogger;
180
185
  dispose(): Promise<void>;
181
186
  abstract obtainLock<T>(lockOptions: LockOptions<T>): Promise<T>;
182
187
  hasCompletedSync(): Promise<boolean>;