@powersync/common 0.0.0-dev-20250701144132 → 0.0.0-dev-20250710153817

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;
@@ -123,6 +122,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
123
122
  protected _schema: Schema;
124
123
  private _database;
125
124
  protected runExclusiveMutex: Mutex;
125
+ logger: ILogger;
126
126
  constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
127
127
  constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
128
128
  constructor(options: PowerSyncDatabaseOptionsWithSettings);
@@ -185,7 +185,6 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
185
185
  * Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
186
186
  */
187
187
  updateSchema(schema: Schema): Promise<void>;
188
- get logger(): Logger.ILogger;
189
188
  /**
190
189
  * Wait for initialization to complete.
191
190
  * While initializing is automatic, this helps to catch and report initialization errors.
@@ -27,7 +27,6 @@ export const DEFAULT_POWERSYNC_CLOSE_OPTIONS = {
27
27
  export const DEFAULT_WATCH_THROTTLE_MS = 30;
28
28
  export const DEFAULT_POWERSYNC_DB_OPTIONS = {
29
29
  retryDelayMs: 5000,
30
- logger: Logger.get('PowerSyncDatabase'),
31
30
  crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
32
31
  };
33
32
  export const DEFAULT_CRUD_BATCH_LIMIT = 100;
@@ -70,6 +69,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
70
69
  _schema;
71
70
  _database;
72
71
  runExclusiveMutex;
72
+ logger;
73
73
  constructor(options) {
74
74
  super();
75
75
  this.options = options;
@@ -89,6 +89,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
89
89
  else {
90
90
  throw new Error('The provided `database` option is invalid.');
91
91
  }
92
+ this.logger = options.logger ?? Logger.get(`PowerSyncDatabase[${this._database.name}]`);
92
93
  this.bucketStorageAdapter = this.generateBucketStorageAdapter();
93
94
  this.closed = false;
94
95
  this.currentStatus = new SyncStatus({});
@@ -268,16 +269,13 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
268
269
  schema.validate();
269
270
  }
270
271
  catch (ex) {
271
- this.options.logger?.warn('Schema validation failed. Unexpected behaviour could occur', ex);
272
+ this.logger.warn('Schema validation failed. Unexpected behaviour could occur', ex);
272
273
  }
273
274
  this._schema = schema;
274
275
  await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
275
276
  await this.database.refreshSchema();
276
277
  this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
277
278
  }
278
- get logger() {
279
- return this.options.logger;
280
- }
281
279
  /**
282
280
  * Wait for initialization to complete.
283
281
  * While initializing is automatic, this helps to catch and report initialization errors.
@@ -617,7 +615,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
617
615
  * @param options Options for configuring watch behavior
618
616
  */
619
617
  watchWithCallback(sql, parameters, handler, options) {
620
- const { onResult, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
618
+ const { onResult, onError = (e) => this.logger.error(e) } = handler ?? {};
621
619
  if (!onResult) {
622
620
  throw new Error('onResult is required');
623
621
  }
@@ -723,7 +721,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
723
721
  * @returns A dispose function to stop watching for changes
724
722
  */
725
723
  onChangeWithCallback(handler, options) {
726
- const { onChange, onError = (e) => this.options.logger?.error(e) } = handler ?? {};
724
+ const { onChange, onError = (e) => this.logger.error(e) } = handler ?? {};
727
725
  if (!onChange) {
728
726
  throw new Error('onChange is required');
729
727
  }
@@ -136,7 +136,6 @@ export class ConnectionManager extends BaseObserver {
136
136
  await this.disconnectingPromise;
137
137
  this.logger.debug('Attempting to connect to PowerSync instance');
138
138
  await this.syncStreamImplementation?.connect(appliedOptions);
139
- this.syncStreamImplementation?.triggerCrudUpload();
140
139
  }
141
140
  /**
142
141
  * Close the sync connection.
@@ -169,9 +169,12 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
169
169
  }
170
170
  async getWriteCheckpoint() {
171
171
  const clientId = await this.options.adapter.getClientId();
172
+ this.logger.debug(`Creating write checkpoint for ${clientId}`);
172
173
  let path = `/write-checkpoint2.json?client_id=${clientId}`;
173
174
  const response = await this.options.remote.get(path);
174
- return response['data']['write_checkpoint'];
175
+ const checkpoint = response['data']['write_checkpoint'];
176
+ this.logger.debug(`Got write checkpoint: ${checkpoint}`);
177
+ return checkpoint;
175
178
  }
176
179
  async _uploadAllCrud() {
177
180
  return this.obtainLock({
@@ -182,17 +185,17 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
182
185
  */
183
186
  let checkedCrudItem;
184
187
  while (true) {
185
- this.updateSyncStatus({
186
- dataFlow: {
187
- uploading: true
188
- }
189
- });
190
188
  try {
191
189
  /**
192
190
  * This is the first item in the FIFO CRUD queue.
193
191
  */
194
192
  const nextCrudItem = await this.options.adapter.nextCrudItem();
195
193
  if (nextCrudItem) {
194
+ this.updateSyncStatus({
195
+ dataFlow: {
196
+ uploading: true
197
+ }
198
+ });
196
199
  if (nextCrudItem.clientId == checkedCrudItem?.clientId) {
197
200
  // This will force a higher log level than exceptions which are caught here.
198
201
  this.logger.warn(`Potentially previously uploaded CRUD entries are still present in the upload queue.
@@ -210,7 +213,11 @@ The next upload iteration will be delayed.`);
210
213
  }
211
214
  else {
212
215
  // Uploading is completed
213
- await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
216
+ this.logger.debug('Upload complete, creating write checkpoint');
217
+ const neededUpdate = await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
218
+ if (neededUpdate == false) {
219
+ this.logger.debug('No write checkpoint needed');
220
+ }
214
221
  break;
215
222
  }
216
223
  }
@@ -247,23 +254,17 @@ The next upload iteration will be delayed.`);
247
254
  const controller = new AbortController();
248
255
  this.abortController = controller;
249
256
  this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
250
- // Return a promise that resolves when the connection status is updated
257
+ // Return a promise that resolves when the connection status is updated to indicate that we're connected.
251
258
  return new Promise((resolve) => {
252
259
  const disposer = this.registerListener({
253
- statusUpdated: (update) => {
254
- // This is triggered as soon as a connection is read from
255
- if (typeof update.connected == 'undefined') {
256
- // only concern with connection updates
257
- return;
258
- }
259
- if (update.connected == false) {
260
- /**
261
- * This function does not reject if initial connect attempt failed.
262
- * Connected can be false if the connection attempt was aborted or if the initial connection
263
- * attempt failed.
264
- */
260
+ statusChanged: (status) => {
261
+ if (status.dataFlowStatus.downloadError != null) {
265
262
  this.logger.warn('Initial connect attempt did not successfully connect to server');
266
263
  }
264
+ else if (status.connecting) {
265
+ // Still connecting.
266
+ return;
267
+ }
267
268
  disposer();
268
269
  resolve();
269
270
  }
@@ -655,6 +656,7 @@ The next upload iteration will be delayed.`);
655
656
  const adapter = this.options.adapter;
656
657
  const remote = this.options.remote;
657
658
  let receivingLines = null;
659
+ let hadSyncLine = false;
658
660
  const abortController = new AbortController();
659
661
  signal.addEventListener('abort', () => abortController.abort());
660
662
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
@@ -698,6 +700,9 @@ The next upload iteration will be delayed.`);
698
700
  }
699
701
  });
700
702
  }
703
+ // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
704
+ // we're already connected here and can report that.
705
+ syncImplementation.updateSyncStatus({ connected: true });
701
706
  try {
702
707
  while (!controlInvocations.closed) {
703
708
  const line = await controlInvocations.read();
@@ -705,6 +710,10 @@ The next upload iteration will be delayed.`);
705
710
  return;
706
711
  }
707
712
  await control(line.command, line.payload);
713
+ if (!hadSyncLine) {
714
+ syncImplementation.triggerCrudUpload();
715
+ hadSyncLine = true;
716
+ }
708
717
  }
709
718
  }
710
719
  finally {
@@ -742,7 +751,7 @@ The next upload iteration will be delayed.`);
742
751
  return {
743
752
  priority: status.priority,
744
753
  hasSynced: status.has_synced ?? undefined,
745
- lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at) : undefined
754
+ lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at * 1000) : undefined
746
755
  };
747
756
  }
748
757
  const info = instruction.UpdateSyncStatus.status;
@@ -865,8 +874,9 @@ The next upload iteration will be delayed.`);
865
874
  // We have pending entries in the local upload queue or are waiting to confirm a write
866
875
  // checkpoint, which prevented this checkpoint from applying. Wait for that to complete and
867
876
  // try again.
868
- this.logger.debug('Could not apply checkpoint due to local data. Waiting for in-progress upload before retrying.');
877
+ this.logger.debug(`Could not apply checkpoint ${checkpoint.last_op_id} due to local data. Waiting for in-progress upload before retrying.`);
869
878
  await Promise.race([pending, onAbortPromise(abort)]);
879
+ this.logger.debug(`Pending uploads complete, retrying local checkpoint at ${checkpoint.last_op_id}`);
870
880
  if (abort.aborted) {
871
881
  return { applied: false, endIteration: true };
872
882
  }
@@ -1,4 +1,4 @@
1
- import { BucketProgress } from 'src/client/sync/stream/core-instruction.js';
1
+ import type { BucketProgress } from '../../client/sync/stream/core-instruction.js';
2
2
  /** @internal */
3
3
  export type InternalProgressInformation = Record<string, BucketProgress>;
4
4
  /**
@@ -34,6 +34,7 @@ export declare class DataStream<ParsedData, SourceData = any> extends BaseObserv
34
34
  dataQueue: SourceData[];
35
35
  protected isClosed: boolean;
36
36
  protected processingPromise: Promise<void> | null;
37
+ protected notifyDataAdded: (() => void) | null;
37
38
  protected logger: ILogger;
38
39
  protected mapLine: (line: SourceData) => ParsedData;
39
40
  constructor(options?: DataStreamOptions<ParsedData, SourceData> | undefined);
@@ -54,7 +55,7 @@ export declare class DataStream<ParsedData, SourceData = any> extends BaseObserv
54
55
  * Executes a callback for each data item in the stream
55
56
  */
56
57
  forEach(callback: DataStreamCallback<ParsedData>): () => void;
57
- protected processQueue(): Promise<void>;
58
+ protected processQueue(): Promise<void> | undefined;
58
59
  protected hasDataReader(): boolean;
59
60
  protected _processQueue(): Promise<void>;
60
61
  protected iterateAsyncErrored(cb: (l: Partial<DataStreamListener<ParsedData>>) => Promise<void>): Promise<void>;
@@ -14,6 +14,7 @@ export class DataStream extends BaseObserver {
14
14
  dataQueue;
15
15
  isClosed;
16
16
  processingPromise;
17
+ notifyDataAdded;
17
18
  logger;
18
19
  mapLine;
19
20
  constructor(options) {
@@ -58,6 +59,7 @@ export class DataStream extends BaseObserver {
58
59
  throw new Error('Cannot enqueue data into closed stream.');
59
60
  }
60
61
  this.dataQueue.push(data);
62
+ this.notifyDataAdded?.();
61
63
  this.processQueue();
62
64
  }
63
65
  /**
@@ -98,10 +100,20 @@ export class DataStream extends BaseObserver {
98
100
  data: callback
99
101
  });
100
102
  }
101
- async processQueue() {
103
+ processQueue() {
102
104
  if (this.processingPromise) {
103
105
  return;
104
106
  }
107
+ const promise = (this.processingPromise = this._processQueue());
108
+ promise.finally(() => {
109
+ return (this.processingPromise = null);
110
+ });
111
+ return promise;
112
+ }
113
+ hasDataReader() {
114
+ return Array.from(this.listeners.values()).some((l) => !!l.data);
115
+ }
116
+ async _processQueue() {
105
117
  /**
106
118
  * Allow listeners to mutate the queue before processing.
107
119
  * This allows for operations such as dropping or compressing data
@@ -110,14 +122,7 @@ export class DataStream extends BaseObserver {
110
122
  if (this.dataQueue.length >= this.highWatermark) {
111
123
  await this.iterateAsyncErrored(async (l) => l.highWater?.());
112
124
  }
113
- return (this.processingPromise = this._processQueue());
114
- }
115
- hasDataReader() {
116
- return Array.from(this.listeners.values()).some((l) => !!l.data);
117
- }
118
- async _processQueue() {
119
125
  if (this.isClosed || !this.hasDataReader()) {
120
- Promise.resolve().then(() => (this.processingPromise = null));
121
126
  return;
122
127
  }
123
128
  if (this.dataQueue.length) {
@@ -126,16 +131,22 @@ export class DataStream extends BaseObserver {
126
131
  await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
127
132
  }
128
133
  if (this.dataQueue.length <= this.lowWatermark) {
129
- await this.iterateAsyncErrored(async (l) => l.lowWater?.());
134
+ const dataAdded = new Promise((resolve) => {
135
+ this.notifyDataAdded = resolve;
136
+ });
137
+ await Promise.race([this.iterateAsyncErrored(async (l) => l.lowWater?.()), dataAdded]);
138
+ this.notifyDataAdded = null;
130
139
  }
131
- this.processingPromise = null;
132
- if (this.dataQueue.length) {
140
+ if (this.dataQueue.length > 0) {
133
141
  // Next tick
134
142
  setTimeout(() => this.processQueue());
135
143
  }
136
144
  }
137
145
  async iterateAsyncErrored(cb) {
138
- for (let i of this.listeners.values()) {
146
+ // Important: We need to copy the listeners, as calling a listener could result in adding another
147
+ // listener, resulting in infinite loops.
148
+ const listeners = Array.from(this.listeners.values());
149
+ for (let i of listeners) {
139
150
  try {
140
151
  await cb(i);
141
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "0.0.0-dev-20250701144132",
3
+ "version": "0.0.0-dev-20250710153817",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -13,8 +13,9 @@
13
13
  "exports": {
14
14
  ".": {
15
15
  "import": "./dist/bundle.mjs",
16
- "default": "./dist/bundle.mjs",
17
- "types": "./lib/index.d.ts"
16
+ "require": "./dist/bundle.cjs",
17
+ "types": "./lib/index.d.ts",
18
+ "default": "./dist/bundle.mjs"
18
19
  }
19
20
  },
20
21
  "author": "JOURNEYAPPS",