@powersync/common 1.33.0 → 1.33.1

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.
@@ -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.
@@ -82,14 +82,6 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
82
82
  getCrudBatch(limit?: number): Promise<CrudBatch | null>;
83
83
  hasCompletedSync(): Promise<boolean>;
84
84
  updateLocalTarget(cb: () => Promise<string>): Promise<boolean>;
85
- /**
86
- * Exposed for tests only.
87
- */
88
- autoCompact(): Promise<void>;
89
- /**
90
- * Exposed for tests only.
91
- */
92
- forceCompact(): Promise<void>;
93
85
  getMaxOpId(): string;
94
86
  /**
95
87
  * Get an unique client id.
@@ -98,5 +90,5 @@ export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener
98
90
  /**
99
91
  * Invokes the `powersync_control` function for the sync client.
100
92
  */
101
- control(op: PowerSyncControlCommand, payload: string | ArrayBuffer | null): Promise<string>;
93
+ control(op: PowerSyncControlCommand, payload: string | Uint8Array | null): Promise<string>;
102
94
  }
@@ -11,14 +11,9 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
11
11
  private mutex;
12
12
  private logger;
13
13
  tableNames: Set<string>;
14
- private pendingBucketDeletes;
15
14
  private _hasCompletedSync;
16
15
  private updateListener;
17
16
  private _clientId?;
18
- /**
19
- * Count up, and do a compact on startup.
20
- */
21
- private compactCounter;
22
17
  constructor(db: DBAdapter, mutex: Mutex, logger?: ILogger);
23
18
  init(): Promise<void>;
24
19
  dispose(): Promise<void>;
@@ -46,13 +41,6 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
46
41
  */
47
42
  private updateObjectsFromBuckets;
48
43
  validateChecksums(checkpoint: Checkpoint, priority: number | undefined): Promise<SyncLocalDatabaseResult>;
49
- /**
50
- * Force a compact, for tests.
51
- */
52
- forceCompact(): Promise<void>;
53
- autoCompact(): Promise<void>;
54
- private deletePendingBuckets;
55
- private clearRemoveOps;
56
44
  updateLocalTarget(cb: () => Promise<string>): Promise<boolean>;
57
45
  nextCrudItem(): Promise<CrudEntry | undefined>;
58
46
  hasCrud(): Promise<boolean>;
@@ -68,7 +56,7 @@ export declare class SqliteBucketStorage extends BaseObserver<BucketStorageListe
68
56
  * Set a target checkpoint.
69
57
  */
70
58
  setTargetCheckpoint(checkpoint: Checkpoint): Promise<void>;
71
- control(op: PowerSyncControlCommand, payload: string | ArrayBuffer | null): Promise<string>;
59
+ control(op: PowerSyncControlCommand, payload: string | Uint8Array | ArrayBuffer | null): Promise<string>;
72
60
  hasMigratedSubkeys(): Promise<boolean>;
73
61
  migrateToFixedSubkeys(): Promise<void>;
74
62
  static _subkeyMigrationKey: string;
@@ -4,27 +4,20 @@ import { BaseObserver } from '../../../utils/BaseObserver.js';
4
4
  import { MAX_OP_ID } from '../../constants.js';
5
5
  import { PSInternalTable } from './BucketStorageAdapter.js';
6
6
  import { CrudEntry } from './CrudEntry.js';
7
- const COMPACT_OPERATION_INTERVAL = 1_000;
8
7
  export class SqliteBucketStorage extends BaseObserver {
9
8
  db;
10
9
  mutex;
11
10
  logger;
12
11
  tableNames;
13
- pendingBucketDeletes;
14
12
  _hasCompletedSync;
15
13
  updateListener;
16
14
  _clientId;
17
- /**
18
- * Count up, and do a compact on startup.
19
- */
20
- compactCounter = COMPACT_OPERATION_INTERVAL;
21
15
  constructor(db, mutex, logger = Logger.get('SqliteBucketStorage')) {
22
16
  super();
23
17
  this.db = db;
24
18
  this.mutex = mutex;
25
19
  this.logger = logger;
26
20
  this._hasCompletedSync = false;
27
- this.pendingBucketDeletes = true;
28
21
  this.tableNames = new Set();
29
22
  this.updateListener = db.registerListener({
30
23
  tablesUpdated: (update) => {
@@ -72,16 +65,13 @@ export class SqliteBucketStorage extends BaseObserver {
72
65
  }
73
66
  async saveSyncData(batch, fixedKeyFormat = false) {
74
67
  await this.writeTransaction(async (tx) => {
75
- let count = 0;
76
68
  for (const b of batch.buckets) {
77
69
  const result = await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', [
78
70
  'save',
79
71
  JSON.stringify({ buckets: [b.toJSON(fixedKeyFormat)] })
80
72
  ]);
81
73
  this.logger.debug('saveSyncData', JSON.stringify(result));
82
- count += b.data.length;
83
74
  }
84
- this.compactCounter += count;
85
75
  });
86
76
  }
87
77
  async removeBuckets(buckets) {
@@ -97,7 +87,6 @@ export class SqliteBucketStorage extends BaseObserver {
97
87
  await tx.execute('INSERT INTO powersync_operations(op, data) VALUES(?, ?)', ['delete_bucket', bucket]);
98
88
  });
99
89
  this.logger.debug('done deleting bucket');
100
- this.pendingBucketDeletes = true;
101
90
  }
102
91
  async hasCompletedSync() {
103
92
  if (this._hasCompletedSync) {
@@ -138,7 +127,6 @@ export class SqliteBucketStorage extends BaseObserver {
138
127
  this.logger.debug('Not at a consistent checkpoint - cannot update local db');
139
128
  return { ready: false, checkpointValid: true };
140
129
  }
141
- await this.forceCompact();
142
130
  return {
143
131
  ready: true,
144
132
  checkpointValid: true
@@ -209,36 +197,6 @@ export class SqliteBucketStorage extends BaseObserver {
209
197
  };
210
198
  }
211
199
  }
212
- /**
213
- * Force a compact, for tests.
214
- */
215
- async forceCompact() {
216
- this.compactCounter = COMPACT_OPERATION_INTERVAL;
217
- this.pendingBucketDeletes = true;
218
- await this.autoCompact();
219
- }
220
- async autoCompact() {
221
- await this.deletePendingBuckets();
222
- await this.clearRemoveOps();
223
- }
224
- async deletePendingBuckets() {
225
- if (this.pendingBucketDeletes !== false) {
226
- await this.writeTransaction(async (tx) => {
227
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES (?, ?)', ['delete_pending_buckets', '']);
228
- });
229
- // Executed once after start-up, and again when there are pending deletes.
230
- this.pendingBucketDeletes = false;
231
- }
232
- }
233
- async clearRemoveOps() {
234
- if (this.compactCounter < COMPACT_OPERATION_INTERVAL) {
235
- return;
236
- }
237
- await this.writeTransaction(async (tx) => {
238
- await tx.execute('INSERT INTO powersync_operations(op, data) VALUES (?, ?)', ['clear_remove_ops', '']);
239
- });
240
- this.compactCounter = 0;
241
- }
242
200
  async updateLocalTarget(cb) {
243
201
  const rs1 = await this.db.getAll("SELECT target_op FROM ps_buckets WHERE name = '$local' AND target_op = CAST(? as INTEGER)", [MAX_OP_ID]);
244
202
  if (!rs1.length) {
@@ -1,5 +1,4 @@
1
1
  import type { BSON } from 'bson';
2
- import { Buffer } from 'buffer';
3
2
  import { type fetch } from 'cross-fetch';
4
3
  import Logger, { ILogger } from 'js-logger';
5
4
  import { DataStream } from '../../../utils/DataStream.js';
@@ -133,7 +132,7 @@ export declare abstract class AbstractRemote {
133
132
  * @param bson A BSON encoder and decoder. When set, the data stream will be requested with a BSON payload
134
133
  * (required for compatibility with older sync services).
135
134
  */
136
- socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Buffer) => T, bson?: typeof BSON): Promise<DataStream>;
135
+ socketStreamRaw<T>(options: SocketSyncStreamOptions, map: (buffer: Uint8Array) => T, bson?: typeof BSON): Promise<DataStream<T>>;
137
136
  /**
138
137
  * Connects to the sync/stream http endpoint, parsing lines as JSON.
139
138
  */
@@ -10,8 +10,12 @@ const POWERSYNC_JS_VERSION = PACKAGE.version;
10
10
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
11
11
  // Keep alive message is sent every period
12
12
  const KEEP_ALIVE_MS = 20_000;
13
- // The ACK must be received in this period
14
- const KEEP_ALIVE_LIFETIME_MS = 30_000;
13
+ // One message of any type must be received in this period.
14
+ const SOCKET_TIMEOUT_MS = 30_000;
15
+ // One keepalive message must be received in this period.
16
+ // If there is a backlog of messages (for example on slow connections), keepalive messages could be delayed
17
+ // significantly. Therefore this is longer than the socket timeout.
18
+ const KEEP_ALIVE_LIFETIME_MS = 90_000;
15
19
  export const DEFAULT_REMOTE_LOGGER = Logger.get('PowerSyncRemote');
16
20
  export var FetchStrategy;
17
21
  (function (FetchStrategy) {
@@ -208,12 +212,25 @@ export class AbstractRemote {
208
212
  // headers with websockets on web. The browser userAgent is however added
209
213
  // automatically as a header.
210
214
  const userAgent = this.getUserAgent();
215
+ let keepAliveTimeout;
216
+ const resetTimeout = () => {
217
+ clearTimeout(keepAliveTimeout);
218
+ keepAliveTimeout = setTimeout(() => {
219
+ this.logger.error(`No data received on WebSocket in ${SOCKET_TIMEOUT_MS}ms, closing connection.`);
220
+ stream.close();
221
+ }, SOCKET_TIMEOUT_MS);
222
+ };
223
+ resetTimeout();
211
224
  const url = this.options.socketUrlTransformer(request.url);
212
225
  const connector = new RSocketConnector({
213
226
  transport: new WebsocketClientTransport({
214
227
  url,
215
228
  wsCreator: (url) => {
216
- return this.createSocket(url);
229
+ const socket = this.createSocket(url);
230
+ socket.addEventListener('message', (event) => {
231
+ resetTimeout();
232
+ });
233
+ return socket;
217
234
  }
218
235
  }),
219
236
  setup: {
@@ -236,16 +253,20 @@ export class AbstractRemote {
236
253
  }
237
254
  catch (ex) {
238
255
  this.logger.error(`Failed to connect WebSocket`, ex);
256
+ clearTimeout(keepAliveTimeout);
239
257
  throw ex;
240
258
  }
259
+ resetTimeout();
241
260
  const stream = new DataStream({
242
261
  logger: this.logger,
243
262
  pressure: {
244
263
  lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER
245
- }
264
+ },
265
+ mapLine: map
246
266
  });
247
267
  let socketIsClosed = false;
248
268
  const closeSocket = () => {
269
+ clearTimeout(keepAliveTimeout);
249
270
  if (socketIsClosed) {
250
271
  return;
251
272
  }
@@ -307,7 +328,7 @@ export class AbstractRemote {
307
328
  if (!data) {
308
329
  return;
309
330
  }
310
- stream.enqueueData(map(data));
331
+ stream.enqueueData(data);
311
332
  },
312
333
  onComplete: () => {
313
334
  stream.close();
@@ -418,7 +439,8 @@ export class AbstractRemote {
418
439
  const decoder = new TextDecoder();
419
440
  let buffer = '';
420
441
  const stream = new DataStream({
421
- logger: this.logger
442
+ logger: this.logger,
443
+ mapLine: mapLine
422
444
  });
423
445
  const l = stream.registerListener({
424
446
  lowWater: async () => {
@@ -429,7 +451,7 @@ export class AbstractRemote {
429
451
  if (done) {
430
452
  const remaining = buffer.trim();
431
453
  if (remaining.length != 0) {
432
- stream.enqueueData(mapLine(remaining));
454
+ stream.enqueueData(remaining);
433
455
  }
434
456
  stream.close();
435
457
  await closeReader();
@@ -441,7 +463,7 @@ export class AbstractRemote {
441
463
  for (var i = 0; i < lines.length - 1; i++) {
442
464
  var l = lines[i].trim();
443
465
  if (l.length > 0) {
444
- stream.enqueueData(mapLine(l));
466
+ stream.enqueueData(l);
445
467
  didCompleteLine = true;
446
468
  }
447
469
  }
@@ -182,17 +182,17 @@ export class AbstractStreamingSyncImplementation extends BaseObserver {
182
182
  */
183
183
  let checkedCrudItem;
184
184
  while (true) {
185
- this.updateSyncStatus({
186
- dataFlow: {
187
- uploading: true
188
- }
189
- });
190
185
  try {
191
186
  /**
192
187
  * This is the first item in the FIFO CRUD queue.
193
188
  */
194
189
  const nextCrudItem = await this.options.adapter.nextCrudItem();
195
190
  if (nextCrudItem) {
191
+ this.updateSyncStatus({
192
+ dataFlow: {
193
+ uploading: true
194
+ }
195
+ });
196
196
  if (nextCrudItem.clientId == checkedCrudItem?.clientId) {
197
197
  // This will force a higher log level than exceptions which are caught here.
198
198
  this.logger.warn(`Potentially previously uploaded CRUD entries are still present in the upload queue.
@@ -247,23 +247,17 @@ The next upload iteration will be delayed.`);
247
247
  const controller = new AbortController();
248
248
  this.abortController = controller;
249
249
  this.streamingSyncPromise = this.streamingSync(this.abortController.signal, options);
250
- // Return a promise that resolves when the connection status is updated
250
+ // Return a promise that resolves when the connection status is updated to indicate that we're connected.
251
251
  return new Promise((resolve) => {
252
252
  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
- */
253
+ statusChanged: (status) => {
254
+ if (status.dataFlowStatus.downloadError != null) {
265
255
  this.logger.warn('Initial connect attempt did not successfully connect to server');
266
256
  }
257
+ else if (status.connecting) {
258
+ // Still connecting.
259
+ return;
260
+ }
267
261
  disposer();
268
262
  resolve();
269
263
  }
@@ -655,6 +649,7 @@ The next upload iteration will be delayed.`);
655
649
  const adapter = this.options.adapter;
656
650
  const remote = this.options.remote;
657
651
  let receivingLines = null;
652
+ let hadSyncLine = false;
658
653
  const abortController = new AbortController();
659
654
  signal.addEventListener('abort', () => abortController.abort());
660
655
  // Pending sync lines received from the service, as well as local events that trigger a powersync_control
@@ -668,20 +663,39 @@ The next upload iteration will be delayed.`);
668
663
  data: instr.request
669
664
  };
670
665
  if (resolvedOptions.connectionMethod == SyncStreamConnectionMethod.HTTP) {
671
- controlInvocations = await remote.postStreamRaw(syncOptions, (line) => ({
672
- command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
673
- payload: line
674
- }));
666
+ controlInvocations = await remote.postStreamRaw(syncOptions, (line) => {
667
+ if (typeof line == 'string') {
668
+ return {
669
+ command: PowerSyncControlCommand.PROCESS_TEXT_LINE,
670
+ payload: line
671
+ };
672
+ }
673
+ else {
674
+ // Directly enqueued by us
675
+ return line;
676
+ }
677
+ });
675
678
  }
676
679
  else {
677
680
  controlInvocations = await remote.socketStreamRaw({
678
681
  ...syncOptions,
679
682
  fetchStrategy: resolvedOptions.fetchStrategy
680
- }, (buffer) => ({
681
- command: PowerSyncControlCommand.PROCESS_BSON_LINE,
682
- payload: buffer
683
- }));
683
+ }, (payload) => {
684
+ if (payload instanceof Uint8Array) {
685
+ return {
686
+ command: PowerSyncControlCommand.PROCESS_BSON_LINE,
687
+ payload: payload
688
+ };
689
+ }
690
+ else {
691
+ // Directly enqueued by us
692
+ return payload;
693
+ }
694
+ });
684
695
  }
696
+ // The rust client will set connected: true after the first sync line because that's when it gets invoked, but
697
+ // we're already connected here and can report that.
698
+ syncImplementation.updateSyncStatus({ connected: true });
685
699
  try {
686
700
  while (!controlInvocations.closed) {
687
701
  const line = await controlInvocations.read();
@@ -689,6 +703,10 @@ The next upload iteration will be delayed.`);
689
703
  return;
690
704
  }
691
705
  await control(line.command, line.payload);
706
+ if (!hadSyncLine) {
707
+ syncImplementation.triggerCrudUpload();
708
+ hadSyncLine = true;
709
+ }
692
710
  }
693
711
  }
694
712
  finally {
@@ -726,7 +744,7 @@ The next upload iteration will be delayed.`);
726
744
  return {
727
745
  priority: status.priority,
728
746
  hasSynced: status.has_synced ?? undefined,
729
- lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at) : undefined
747
+ lastSyncedAt: status?.last_synced_at != null ? new Date(status.last_synced_at * 1000) : undefined
730
748
  };
731
749
  }
732
750
  const info = instruction.UpdateSyncStatus.status;
@@ -1,6 +1,7 @@
1
1
  import { ILogger } from 'js-logger';
2
2
  import { BaseListener, BaseObserver } from './BaseObserver.js';
3
- export type DataStreamOptions = {
3
+ export type DataStreamOptions<ParsedData, SourceData> = {
4
+ mapLine?: (line: SourceData) => ParsedData;
4
5
  /**
5
6
  * Close the stream if any consumer throws an error
6
7
  */
@@ -28,13 +29,15 @@ export declare const DEFAULT_PRESSURE_LIMITS: {
28
29
  * native JS streams or async iterators.
29
30
  * This is handy for environments such as React Native which need polyfills for the above.
30
31
  */
31
- export declare class DataStream<Data extends any = any> extends BaseObserver<DataStreamListener<Data>> {
32
- protected options?: DataStreamOptions | undefined;
33
- dataQueue: Data[];
32
+ export declare class DataStream<ParsedData, SourceData = any> extends BaseObserver<DataStreamListener<ParsedData>> {
33
+ protected options?: DataStreamOptions<ParsedData, SourceData> | undefined;
34
+ dataQueue: SourceData[];
34
35
  protected isClosed: boolean;
35
36
  protected processingPromise: Promise<void> | null;
37
+ protected notifyDataAdded: (() => void) | null;
36
38
  protected logger: ILogger;
37
- constructor(options?: DataStreamOptions | undefined);
39
+ protected mapLine: (line: SourceData) => ParsedData;
40
+ constructor(options?: DataStreamOptions<ParsedData, SourceData> | undefined);
38
41
  get highWatermark(): number;
39
42
  get lowWatermark(): number;
40
43
  get closed(): boolean;
@@ -42,22 +45,18 @@ export declare class DataStream<Data extends any = any> extends BaseObserver<Dat
42
45
  /**
43
46
  * Enqueues data for the consumers to read
44
47
  */
45
- enqueueData(data: Data): void;
48
+ enqueueData(data: SourceData): void;
46
49
  /**
47
50
  * Reads data once from the data stream
48
51
  * @returns a Data payload or Null if the stream closed.
49
52
  */
50
- read(): Promise<Data | null>;
53
+ read(): Promise<ParsedData | null>;
51
54
  /**
52
55
  * Executes a callback for each data item in the stream
53
56
  */
54
- forEach(callback: DataStreamCallback<Data>): () => void;
55
- protected processQueue(): Promise<void>;
56
- /**
57
- * Creates a new data stream which is a map of the original
58
- */
59
- map<ReturnData>(callback: (data: Data) => ReturnData): DataStream<ReturnData>;
57
+ forEach(callback: DataStreamCallback<ParsedData>): () => void;
58
+ protected processQueue(): Promise<void> | undefined;
60
59
  protected hasDataReader(): boolean;
61
60
  protected _processQueue(): Promise<void>;
62
- protected iterateAsyncErrored(cb: (l: BaseListener) => Promise<void>): Promise<void>;
61
+ protected iterateAsyncErrored(cb: (l: Partial<DataStreamListener<ParsedData>>) => Promise<void>): Promise<void>;
63
62
  }
@@ -14,13 +14,16 @@ export class DataStream extends BaseObserver {
14
14
  dataQueue;
15
15
  isClosed;
16
16
  processingPromise;
17
+ notifyDataAdded;
17
18
  logger;
19
+ mapLine;
18
20
  constructor(options) {
19
21
  super();
20
22
  this.options = options;
21
23
  this.processingPromise = null;
22
24
  this.isClosed = false;
23
25
  this.dataQueue = [];
26
+ this.mapLine = options?.mapLine ?? ((line) => line);
24
27
  this.logger = options?.logger ?? Logger.get('DataStream');
25
28
  if (options?.closeOnError) {
26
29
  const l = this.registerListener({
@@ -56,6 +59,7 @@ export class DataStream extends BaseObserver {
56
59
  throw new Error('Cannot enqueue data into closed stream.');
57
60
  }
58
61
  this.dataQueue.push(data);
62
+ this.notifyDataAdded?.();
59
63
  this.processQueue();
60
64
  }
61
65
  /**
@@ -96,10 +100,20 @@ export class DataStream extends BaseObserver {
96
100
  data: callback
97
101
  });
98
102
  }
99
- async processQueue() {
103
+ processQueue() {
100
104
  if (this.processingPromise) {
101
105
  return;
102
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() {
103
117
  /**
104
118
  * Allow listeners to mutate the queue before processing.
105
119
  * This allows for operations such as dropping or compressing data
@@ -108,47 +122,31 @@ export class DataStream extends BaseObserver {
108
122
  if (this.dataQueue.length >= this.highWatermark) {
109
123
  await this.iterateAsyncErrored(async (l) => l.highWater?.());
110
124
  }
111
- return (this.processingPromise = this._processQueue());
112
- }
113
- /**
114
- * Creates a new data stream which is a map of the original
115
- */
116
- map(callback) {
117
- const stream = new DataStream(this.options);
118
- const l = this.registerListener({
119
- data: async (data) => {
120
- stream.enqueueData(callback(data));
121
- },
122
- closed: () => {
123
- stream.close();
124
- l?.();
125
- }
126
- });
127
- return stream;
128
- }
129
- hasDataReader() {
130
- return Array.from(this.listeners.values()).some((l) => !!l.data);
131
- }
132
- async _processQueue() {
133
125
  if (this.isClosed || !this.hasDataReader()) {
134
- Promise.resolve().then(() => (this.processingPromise = null));
135
126
  return;
136
127
  }
137
128
  if (this.dataQueue.length) {
138
129
  const data = this.dataQueue.shift();
139
- await this.iterateAsyncErrored(async (l) => l.data?.(data));
130
+ const mapped = this.mapLine(data);
131
+ await this.iterateAsyncErrored(async (l) => l.data?.(mapped));
140
132
  }
141
133
  if (this.dataQueue.length <= this.lowWatermark) {
142
- 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;
143
139
  }
144
- this.processingPromise = null;
145
- if (this.dataQueue.length) {
140
+ if (this.dataQueue.length > 0) {
146
141
  // Next tick
147
142
  setTimeout(() => this.processQueue());
148
143
  }
149
144
  }
150
145
  async iterateAsyncErrored(cb) {
151
- for (let i of Array.from(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) {
152
150
  try {
153
151
  await cb(i);
154
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "1.33.0",
3
+ "version": "1.33.1",
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",