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

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,6 +213,7 @@ The next upload iteration will be delayed.`);
210
213
  }
211
214
  else {
212
215
  // Uploading is completed
216
+ this.logger.debug('Upload complete, updating write checkpoint');
213
217
  await this.options.adapter.updateLocalTarget(() => this.getWriteCheckpoint());
214
218
  break;
215
219
  }
@@ -247,23 +251,17 @@ The next upload iteration will be delayed.`);
247
251
  const controller = new AbortController();
248
252
  this.abortController = controller;
249
253
  this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
250
- // Return a promise that resolves when the connection status is updated
254
+ // Return a promise that resolves when the connection status is updated to indicate that we're connected.
251
255
  return new Promise((resolve) => {
252
256
  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
- */
257
+ statusChanged: (status) => {
258
+ if (status.dataFlowStatus.downloadError != null) {
265
259
  this.logger.warn('Initial connect attempt did not successfully connect to server');
266
260
  }
261
+ else if (status.connecting) {
262
+ // Still connecting.
263
+ return;
264
+ }
267
265
  disposer();
268
266
  resolve();
269
267
  }
@@ -655,6 +653,7 @@ The next upload iteration will be delayed.`);
655
653
  const adapter = this.options.adapter;
656
654
  const remote = this.options.remote;
657
655
  let receivingLines = null;
656
+ let hadSyncLine = false;
658
657
  const abortController = new AbortController();
659
658
  signal.addEventListener('abort', () => abortController.abort());
660
659
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
@@ -698,6 +697,9 @@ The next upload iteration will be delayed.`);
698
697
  }
699
698
  });
700
699
  }
700
+ // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
701
+ // we're already connected here and can report that.
702
+ syncImplementation.updateSyncStatus({ connected: true });
701
703
  try {
702
704
  while (!controlInvocations.closed) {
703
705
  const line = await controlInvocations.read();
@@ -705,6 +707,10 @@ The next upload iteration will be delayed.`);
705
707
  return;
706
708
  }
707
709
  await control(line.command, line.payload);
710
+ if (!hadSyncLine) {
711
+ syncImplementation.triggerCrudUpload();
712
+ hadSyncLine = true;
713
+ }
708
714
  }
709
715
  }
710
716
  finally {
@@ -742,7 +748,7 @@ The next upload iteration will be delayed.`);
742
748
  return {
743
749
  priority: status.priority,
744
750
  hasSynced: status.has_synced ?? undefined,
745
- lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at) : undefined
751
+ lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at * 1000) : undefined
746
752
  };
747
753
  }
748
754
  const info = instruction.UpdateSyncStatus.status;
@@ -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-20250710151329",
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",