@powersync/common 0.0.0-dev-20250715111940 → 0.0.0-dev-20250728083821

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.
@@ -1,5 +1,5 @@
1
1
  import { Mutex } from 'async-mutex';
2
- import Logger, { ILogger } from 'js-logger';
2
+ import { ILogger } from 'js-logger';
3
3
  import { DBAdapter, QueryResult, Transaction } from '../db/DBAdapter.js';
4
4
  import { SyncStatus } from '../db/crud/SyncStatus.js';
5
5
  import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
@@ -84,7 +84,6 @@ export declare const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions;
84
84
  export declare const DEFAULT_WATCH_THROTTLE_MS = 30;
85
85
  export declare const DEFAULT_POWERSYNC_DB_OPTIONS: {
86
86
  retryDelayMs: number;
87
- logger: Logger.ILogger;
88
87
  crudUploadThrottleMs: number;
89
88
  };
90
89
  export declare const DEFAULT_CRUD_BATCH_LIMIT = 100;
@@ -118,6 +117,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
118
117
  protected _schema: Schema;
119
118
  private _database;
120
119
  protected runExclusiveMutex: Mutex;
120
+ logger: ILogger;
121
121
  constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
122
122
  constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
123
123
  constructor(options: PowerSyncDatabaseOptionsWithSettings);
@@ -180,7 +180,6 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
180
180
  * Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
181
181
  */
182
182
  updateSchema(schema: Schema): Promise<void>;
183
- get logger(): Logger.ILogger;
184
183
  /**
185
184
  * Wait for initialization to complete.
186
185
  * While initializing is automatic, this helps to catch and report initialization errors.
@@ -26,7 +26,6 @@ export const DEFAULT_POWERSYNC_CLOSE_OPTIONS = {
26
26
  export const DEFAULT_WATCH_THROTTLE_MS = 30;
27
27
  export const DEFAULT_POWERSYNC_DB_OPTIONS = {
28
28
  retryDelayMs: 5000,
29
- logger: Logger.get('PowerSyncDatabase'),
30
29
  crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
31
30
  };
32
31
  export const DEFAULT_CRUD_BATCH_LIMIT = 100;
@@ -64,6 +63,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
64
63
  _schema;
65
64
  _database;
66
65
  runExclusiveMutex;
66
+ logger;
67
67
  constructor(options) {
68
68
  super();
69
69
  this.options = options;
@@ -83,6 +83,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
83
83
  else {
84
84
  throw new Error('The provided `database` option is invalid.');
85
85
  }
86
+ this.logger = options.logger ?? Logger.get(`PowerSyncDatabase[${this._database.name}]`);
86
87
  this.bucketStorageAdapter = this.generateBucketStorageAdapter();
87
88
  this.closed = false;
88
89
  this.currentStatus = new SyncStatus({});
@@ -262,16 +263,13 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
262
263
  schema.validate();
263
264
  }
264
265
  catch (ex) {
265
- this.options.logger?.warn('Schema validation failed. Unexpected behaviour could occur', ex);
266
+ this.logger.warn('Schema validation failed. Unexpected behaviour could occur', ex);
266
267
  }
267
268
  this._schema = schema;
268
269
  await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
269
270
  await this.database.refreshSchema();
270
271
  this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
271
272
  }
272
- get logger() {
273
- return this.options.logger;
274
- }
275
273
  /**
276
274
  * Wait for initialization to complete.
277
275
  * While initializing is automatic, this helps to catch and report initialization errors.
@@ -609,7 +607,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
609
607
  * @param options Options for configuring watch behavior
610
608
  */
611
609
  watchWithCallback(sql, parameters, handler, options) {
612
- const { onResult, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
610
+ const { onResult, onError = (e) => this.logger.error(e) } = handler ?? {};
613
611
  if (!onResult) {
614
612
  throw new Error('onResult is required');
615
613
  }
@@ -715,7 +713,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
715
713
  * @returns A dispose function to stop watching for changes
716
714
  */
717
715
  onChangeWithCallback(handler, options) {
718
- const { onChange, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
716
+ const { onChange, onError = (e) => this.logger.error(e) } = handler ?? {};
719
717
  if (!onChange) {
720
718
  throw new Error('onChange is required');
721
719
  }
@@ -64,11 +64,11 @@ export class SqliteBucketStorage extends BaseObserver {
64
64
  async saveSyncData(batch, fixedKeyFormat = false) {
65
65
  await this.writeTransaction(async (tx) => {
66
66
  for (const b of batch.buckets) {
67
- const result = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
67
+ await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
68
68
  'save',
69
69
  JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
70
70
  ]);
71
- this.logger.debug('saveSyncData', JSON.stringify(result));
71
+ this.logger.debug(`Saved batch of data for bucket: ${b.bucket}, operations: ${b.data.length}`);
72
72
  }
73
73
  });
74
74
  }
@@ -84,7 +84,7 @@ export class SqliteBucketStorage extends BaseObserver {
84
84
  await this.writeTransaction(async (tx) => {
85
85
  await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
86
86
  });
87
- this.logger.debug('done deleting bucket');
87
+ this.logger.debug(`Done deleting bucket ${bucket}`);
88
88
  }
89
89
  async hasCompletedSync() {
90
90
  if (this._hasCompletedSync) {
@@ -106,6 +106,12 @@ export class SqliteBucketStorage extends BaseObserver {
106
106
  }
107
107
  return { ready: false, checkpointValid: false, checkpointFailures: r.checkpointFailures };
108
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
+ }
109
115
  let buckets = checkpoint.buckets;
110
116
  if (priority !== undefined) {
111
117
  buckets = buckets.filter((b) => hasMatchingPriority(priority, b));
@@ -122,7 +128,6 @@ export class SqliteBucketStorage extends BaseObserver {
122
128
  });
123
129
  const valid = await this.updateObjectsFromBuckets(checkpoint, priority);
124
130
  if (!valid) {
125
- this.logger.debug('Not at a consistent checkpoint - cannot update local db');
126
131
  return { ready: false, checkpointValid: true };
127
132
  }
128
133
  return {
@@ -175,7 +180,6 @@ export class SqliteBucketStorage extends BaseObserver {
175
180
  JSON.stringify({ ...checkpoint })
176
181
  ]);
177
182
  const resultItem = rs.rows?.item(0);
178
- this.logger.debug('validateChecksums priority, checkpoint, result item', priority, checkpoint, resultItem);
179
183
  if (!resultItem) {
180
184
  return {
181
185
  checkpointValid: false,
@@ -208,30 +212,26 @@ export class SqliteBucketStorage extends BaseObserver {
208
212
  }
209
213
  const seqBefore = rs[0]['seq'];
210
214
  const opId = await cb();
211
- this.logger.debug(`[updateLocalTarget] Updating target to checkpoint ${opId}`);
212
215
  return this.writeTransaction(async (tx) => {
213
216
  const anyData = await tx.execute('SELECT 1 FROM ps_crud LIMIT 1');
214
217
  if (anyData.rows?.length) {
215
218
  // if isNotEmpty
216
- this.logger.debug('updateLocalTarget', 'ps crud is not empty');
219
+ this.logger.debug(`New data uploaded since write checkpoint ${opId} - need new write checkpoint`);
217
220
  return false;
218
221
  }
219
222
  const rs = await tx.execute("SELECT seq FROM sqlite_sequence WHERE name = 'ps_crud'");
220
223
  if (!rs.rows?.length) {
221
224
  // assert isNotEmpty
222
- throw new Error('SQlite Sequence should not be empty');
225
+ throw new Error('SQLite Sequence should not be empty');
223
226
  }
224
227
  const seqAfter = rs.rows?.item(0)['seq'];
225
- this.logger.debug('seqAfter', JSON.stringify(rs.rows?.item(0)));
226
228
  if (seqAfter != seqBefore) {
227
- this.logger.debug('seqAfter != seqBefore', seqAfter, seqBefore);
229
+ this.logger.debug(`New data uploaded since write checpoint ${opId} - need new write checkpoint (sequence updated)`);
228
230
  // New crud data may have been uploaded since we got the checkpoint. Abort.
229
231
  return false;
230
232
  }
231
- const response = await tx.execute("UPDATE ps_buckets SET target_op = CAST(? as INTEGER) WHERE name='$local'", [
232
- opId
233
- ]);
234
- 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]);
235
235
  return true;
236
236
  });
237
237
  }
@@ -204,6 +204,22 @@ export class AbstractRemote {
204
204
  // headers with websockets on web. The browser userAgent is however added
205
205
  // automatically as a header.
206
206
  const userAgent = this.getUserAgent();
207
+ const stream = new DataStream({
208
+ logger: this.logger,
209
+ pressure: {
210
+ lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
211
+ },
212
+ mapLine: map
213
+ });
214
+ // Handle upstream abort
215
+ if (options.abortSignal?.aborted) {
216
+ throw new AbortOperation('Connection request aborted');
217
+ }
218
+ else {
219
+ options.abortSignal?.addEventListener('abort', () => {
220
+ stream.close();
221
+ }, { once: true });
222
+ }
207
223
  let keepAliveTimeout;
208
224
  const resetTimeout = () => {
209
225
  clearTimeout(keepAliveTimeout);
@@ -213,12 +229,22 @@ export class AbstractRemote {
213
229
  }, SOCKET_TIMEOUT_MS);
214
230
  };
215
231
  resetTimeout();
232
+ // Typescript complains about this being `never` if it's not assigned here.
233
+ // This is assigned in `wsCreator`.
234
+ let disposeSocketConnectionTimeout = () => { };
216
235
  const url = this.options.socketUrlTransformer(request.url);
217
236
  const connector = new RSocketConnector({
218
237
  transport: new WebsocketClientTransport({
219
238
  url,
220
239
  wsCreator: (url) => {
221
240
  const socket = this.createSocket(url);
241
+ disposeSocketConnectionTimeout = stream.registerListener({
242
+ closed: () => {
243
+ // Allow closing the underlying WebSocket if the stream was closed before the
244
+ // RSocket connect completed. This should effectively abort the request.
245
+ socket.close();
246
+ }
247
+ });
222
248
  socket.addEventListener('message', (event) => {
223
249
  resetTimeout();
224
250
  });
@@ -242,20 +268,18 @@ export class AbstractRemote {
242
268
  let rsocket;
243
269
  try {
244
270
  rsocket = await connector.connect();
271
+ // The connection is established, we no longer need to monitor the initial timeout
272
+ disposeSocketConnectionTimeout();
245
273
  }
246
274
  catch (ex) {
247
275
  this.logger.error(`Failed to connect WebSocket`, ex);
248
276
  clearTimeout(keepAliveTimeout);
277
+ if (!stream.closed) {
278
+ await stream.close();
279
+ }
249
280
  throw ex;
250
281
  }
251
282
  resetTimeout();
252
- const stream = new DataStream({
253
- logger: this.logger,
254
- pressure: {
255
- lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
256
- },
257
- mapLine: map
258
- });
259
283
  let socketIsClosed = false;
260
284
  const closeSocket = () => {
261
285
  clearTimeout(keepAliveTimeout);
@@ -341,18 +365,6 @@ export class AbstractRemote {
341
365
  l();
342
366
  }
343
367
  });
344
- /**
345
- * Handle abort operations here.
346
- * Unfortunately cannot insert them into the connection.
347
- */
348
- if (options.abortSignal?.aborted) {
349
- stream.close();
350
- }
351
- else {
352
- options.abortSignal?.addEventListener('abort', () => {
353
- stream.close();
354
- });
355
- }
356
368
  return stream;
357
369
  }
358
370
  /**
@@ -1,4 +1,4 @@
1
- import Logger, { ILogger } from 'js-logger';
1
+ import { ILogger } from 'js-logger';
2
2
  import { SyncStatus, SyncStatusOptions } from '../../../db/crud/SyncStatus.js';
3
3
  import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver.js';
4
4
  import { BucketStorageAdapter } from '../bucket/BucketStorageAdapter.js';
@@ -160,7 +160,6 @@ export declare const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
160
160
  export declare const DEFAULT_RETRY_DELAY_MS = 5000;
161
161
  export declare const DEFAULT_STREAMING_SYNC_OPTIONS: {
162
162
  retryDelayMs: number;
163
- logger: Logger.ILogger;
164
163
  crudUploadThrottleMs: number;
165
164
  };
166
165
  export type RequiredPowerSyncConnectionOptions = Required<BaseConnectionOptions>;
@@ -171,6 +170,7 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
171
170
  protected abortController: AbortController | null;
172
171
  protected crudUpdateListener?: () => void;
173
172
  protected streamingSyncPromise?: Promise<void>;
173
+ protected logger: ILogger;
174
174
  private isUploadingCrud;
175
175
  private notifyCompletedUploads?;
176
176
  syncStatus: SyncStatus;
@@ -181,7 +181,6 @@ export declare abstract class AbstractStreamingSyncImplementation extends BaseOb
181
181
  waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
182
182
  get lastSyncedAt(): Date | undefined;
183
183
  get isConnected(): boolean;
184
- protected get logger(): Logger.ILogger;
185
184
  dispose(): Promise<void>;
186
185
  abstract obtainLock<T>(lockOptions: LockOptions<T>): Promise<T>;
187
186
  hasCompletedSync(): Promise<boolean>;
@@ -65,7 +65,6 @@ export const DEFAULT_CRUD_UPLOAD_THROTTLE_MS = 1000;
65
65
  export const DEFAULT_RETRY_DELAY_MS = 5000;
66
66
  export const DEFAULT_STREAMING_SYNC_OPTIONS = {
67
67
  retryDelayMs: DEFAULT_RETRY_DELAY_MS,
68
- logger: Logger.get('PowerSyncStream'),
69
68
  crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
70
69
  };
71
70
  export const DEFAULT_STREAM_CONNECTION_OPTIONS = {
@@ -86,6 +85,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
86
85
  abortController;
87
86
  crudUpdateListener;
88
87
  streamingSyncPromise;
88
+ logger;
89
89
  isUploadingCrud = false;
90
90
  notifyCompletedUploads;
91
91
  syncStatus;
@@ -93,6 +93,7 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
93
93
  constructor(options) {
94
94
  super();
95
95
  this.options = { ...DEFAULT_STREAMING_SYNC_OPTIONS, ...options };
96
+ this.logger = options.logger ?? Logger.get('PowerSyncStream');
96
97
  this.syncStatus = new SyncStatus({
97
98
  connected: false,
98
99
  connecting: false,
@@ -156,9 +157,6 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
156
157
  get isConnected() {
157
158
  return this.syncStatus.connected;
158
159
  }
159
- get logger() {
160
- return this.options.logger;
161
- }
162
160
  async dispose() {
163
161
  this.crudUpdateListener?.();
164
162
  this.crudUpdateListener = undefined;
@@ -170,7 +168,9 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
170
168
  const clientId = await this.options.adapter.getClientId();
171
169
  let path = `/write-checkpoint2.json?client_id=${clientId}`;
172
170
  const response = await this.options.remote.get(path);
173
- return response['data']['write_checkpoint'];
171
+ const checkpoint = response['data']['write_checkpoint'];
172
+ this.logger.debug(`Created write checkpoint: ${checkpoint}`);
173
+ return checkpoint;
174
174
  }
175
175
  async _uploadAllCrud() {
176
176
  return this.obtainLock({
@@ -209,7 +209,11 @@ The next upload iteration will be delayed.`);
209
209
  }
210
210
  else {
211
211
  // Uploading is completed
212
- await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
212
+ const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
213
+ if (neededUpdate == false && checkedCrudItem != null) {
214
+ // Only log this if there was something to upload
215
+ this.logger.debug('Upload complete, no write checkpoint needed.');
216
+ }
213
217
  break;
214
218
  }
215
219
  }
@@ -435,6 +439,10 @@ The next upload iteration will be delayed.`);
435
439
  });
436
440
  }
437
441
  async legacyStreamingSyncIteration(signal, resolvedOptions) {
442
+ const rawTables = resolvedOptions.serializedSchema?.raw_tables;
443
+ if (rawTables != null && rawTables.length) {
444
+ this.logger.warn('Raw tables require the Rust-based sync client. The JS client will ignore them.');
445
+ }
438
446
  this.logger.debug('Streaming sync iteration started');
439
447
  this.options.adapter.startSession();
440
448
  let [req, bucketMap] = await this.collectLocalBucketState();
@@ -882,17 +890,17 @@ The next upload iteration will be delayed.`);
882
890
  async applyCheckpoint(checkpoint) {
883
891
  let result = await this.options.adapter.syncLocalDatabase(checkpoint);
884
892
  if (!result.checkpointValid) {
885
- this.logger.debug('Checksum mismatch in checkpoint, will reconnect');
893
+ this.logger.debug(`Checksum mismatch in checkpoint ${checkpoint.last_op_id}, will reconnect`);
886
894
  // This means checksums failed. Start again with a new checkpoint.
887
895
  // TODO: better back-off
888
896
  await new Promise((resolve) => setTimeout(resolve, 50));
889
897
  return { applied: false, endIteration: true };
890
898
  }
891
899
  else if (!result.ready) {
892
- this.logger.debug('Could not apply checkpoint due to local data. We will retry applying the checkpoint after that upload is completed.');
900
+ this.logger.debug(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. We will retry applying the checkpoint after that upload is completed.`);
893
901
  return { applied: false, endIteration: false };
894
902
  }
895
- this.logger.debug('validated checkpoint', checkpoint);
903
+ this.logger.debug(`Applied checkpoint ${checkpoint.last_op_id}`, checkpoint);
896
904
  this.updateSyncStatus({
897
905
  connected: true,
898
906
  lastSyncedAt: new Date(),
@@ -37,6 +37,10 @@ export type PendingStatement = {
37
37
  * Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
38
38
  * using client-side table and column constraints.
39
39
  *
40
+ * To collect local writes to raw tables with PowerSync, custom triggers are required. See
41
+ * {@link https://docs.powersync.com/usage/use-case-examples/raw-tables the documentation} for details and an example on
42
+ * using raw tables.
43
+ *
40
44
  * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
41
45
  *
42
46
  * @experimental Please note that this feature is experimental at the moment, and not covered by PowerSync semver or
@@ -4,6 +4,10 @@
4
4
  * Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
5
5
  * using client-side table and column constraints.
6
6
  *
7
+ * To collect local writes to raw tables with PowerSync, custom triggers are required. See
8
+ * {@link https://docs.powersync.com/usage/use-case-examples/raw-tables the documentation} for details and an example on
9
+ * using raw tables.
10
+ *
7
11
  * Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
8
12
  *
9
13
  * @experimental Please note that this feature is experimental at the moment, and not covered by PowerSync semver or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "0.0.0-dev-20250715111940",
3
+ "version": "0.0.0-dev-20250728083821",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"