@powersync/common 1.54.0 → 1.56.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.
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var eventIterator = require('event-iterator');
4
3
  var node_buffer = require('node:buffer');
5
4
 
6
5
  /**
@@ -2140,11 +2139,7 @@ class SyncStatus {
2140
2139
  */
2141
2140
  const replacer = (_, value) => {
2142
2141
  if (value instanceof Error) {
2143
- return {
2144
- name: value.name,
2145
- message: value.message,
2146
- stack: value.stack
2147
- };
2142
+ return this.serializeError(value);
2148
2143
  }
2149
2144
  return value;
2150
2145
  };
@@ -2187,11 +2182,18 @@ class SyncStatus {
2187
2182
  if (typeof error == 'undefined') {
2188
2183
  return undefined;
2189
2184
  }
2190
- return {
2185
+ const serialized = {
2191
2186
  name: error.name,
2192
2187
  message: error.message,
2193
2188
  stack: error.stack
2194
2189
  };
2190
+ // `Error.cause` can be any value (the spec types it as unknown). Preserve it
2191
+ // so consumers reading uploadError/downloadError keep the failure context.
2192
+ // Recurse for Error causes so the whole chain is flattened the same way.
2193
+ if (typeof error.cause != 'undefined') {
2194
+ serialized.cause = error.cause instanceof Error ? this.serializeError(error.cause) : error.cause;
2195
+ }
2196
+ return serialized;
2195
2197
  }
2196
2198
  static comparePriorities(a, b) {
2197
2199
  return b.priority - a.priority; // Reverse because higher priorities have lower numbers
@@ -2341,6 +2343,210 @@ class ControlledExecutor {
2341
2343
  }
2342
2344
  }
2343
2345
 
2346
+ /**
2347
+ * Some JavaScript engines, in particular older versions of React Native, don't support Symbol.asyncIterator.
2348
+ *
2349
+ * For those, users relying on async generators typically lower them with a transpiler and [this polyfill](https://github.com/Azure/azure-sdk-for-js/blob/%40azure/core-asynciterator-polyfill_1.0.2/sdk/core/core-asynciterator-polyfill/src/index.ts#L4-L6).
2350
+ * This definition is compatible with that polyfill, so transpiled apps can use async iterables created by the PowerSync
2351
+ * SDK.
2352
+ */
2353
+ const symbolAsyncIterator = Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');
2354
+
2355
+ const doneResult = { done: true, value: undefined };
2356
+ function valueResult(value) {
2357
+ return { done: false, value };
2358
+ }
2359
+ /**
2360
+ * Expands a source async iterator by allowing to inject events asynchronously.
2361
+ *
2362
+ * The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
2363
+ * events are dropped once the main iterator completes, but are otherwise forwarded.
2364
+ *
2365
+ * The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
2366
+ * in response to a `next()` call from downstream if no pending injected events can be dispatched.
2367
+ */
2368
+ function injectable(source) {
2369
+ let sourceIsDone = false;
2370
+ let waiter = undefined; // An active, waiting next() call.
2371
+ // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
2372
+ let pendingSourceEvent = null;
2373
+ let sourceFetchInFlight = false;
2374
+ let pendingInjectedEvents = [];
2375
+ const consumeWaiter = () => {
2376
+ const pending = waiter;
2377
+ waiter = undefined;
2378
+ return pending;
2379
+ };
2380
+ const fetchFromSource = () => {
2381
+ const resolveWaiter = (propagate) => {
2382
+ sourceFetchInFlight = false;
2383
+ const active = consumeWaiter();
2384
+ if (active) {
2385
+ propagate(active);
2386
+ }
2387
+ else {
2388
+ pendingSourceEvent = propagate;
2389
+ }
2390
+ };
2391
+ sourceFetchInFlight = true;
2392
+ const nextFromSource = source.next();
2393
+ nextFromSource.then((value) => {
2394
+ sourceIsDone = value.done == true;
2395
+ resolveWaiter((w) => w.resolve(value));
2396
+ }, (error) => {
2397
+ resolveWaiter((w) => w.reject(error));
2398
+ });
2399
+ };
2400
+ return {
2401
+ next: () => {
2402
+ return new Promise((resolve, reject) => {
2403
+ // First priority: Dispatch ready upstream events.
2404
+ if (sourceIsDone) {
2405
+ return resolve(doneResult);
2406
+ }
2407
+ if (pendingSourceEvent) {
2408
+ pendingSourceEvent({ resolve, reject });
2409
+ pendingSourceEvent = null;
2410
+ return;
2411
+ }
2412
+ // Second priority: Dispatch injected events
2413
+ if (pendingInjectedEvents.length) {
2414
+ return resolve(valueResult(pendingInjectedEvents.shift()));
2415
+ }
2416
+ // Nothing pending? Fetch from source
2417
+ waiter = { resolve, reject };
2418
+ if (!sourceFetchInFlight) {
2419
+ fetchFromSource();
2420
+ }
2421
+ });
2422
+ },
2423
+ inject: (event) => {
2424
+ const pending = consumeWaiter();
2425
+ if (pending != null) {
2426
+ pending.resolve(valueResult(event));
2427
+ }
2428
+ else {
2429
+ pendingInjectedEvents.push(event);
2430
+ }
2431
+ }
2432
+ };
2433
+ }
2434
+ /**
2435
+ * Splits a byte stream at line endings, emitting each line as a string.
2436
+ */
2437
+ function extractJsonLines(source, decoder) {
2438
+ let buffer = '';
2439
+ const pendingLines = [];
2440
+ let isFinalEvent = false;
2441
+ return {
2442
+ next: async () => {
2443
+ while (true) {
2444
+ if (isFinalEvent) {
2445
+ return doneResult;
2446
+ }
2447
+ {
2448
+ const first = pendingLines.shift();
2449
+ if (first) {
2450
+ return { done: false, value: first };
2451
+ }
2452
+ }
2453
+ const { done, value } = await source.next();
2454
+ if (done) {
2455
+ const remaining = buffer.trim();
2456
+ if (remaining.length != 0) {
2457
+ isFinalEvent = true;
2458
+ return { done: false, value: remaining };
2459
+ }
2460
+ return doneResult;
2461
+ }
2462
+ const data = decoder.decode(value, { stream: true });
2463
+ buffer += data;
2464
+ const lines = buffer.split('\n');
2465
+ for (let i = 0; i < lines.length - 1; i++) {
2466
+ const l = lines[i].trim();
2467
+ if (l.length > 0) {
2468
+ pendingLines.push(l);
2469
+ }
2470
+ }
2471
+ buffer = lines[lines.length - 1];
2472
+ }
2473
+ }
2474
+ };
2475
+ }
2476
+ /**
2477
+ * Splits a concatenated stream of BSON objects by emitting individual objects.
2478
+ */
2479
+ function extractBsonObjects(source) {
2480
+ // Fully read but not emitted yet.
2481
+ const completedObjects = [];
2482
+ // Whether source has returned { done: true }. We do the same once completed objects have been emitted.
2483
+ let isDone = false;
2484
+ const lengthBuffer = new DataView(new ArrayBuffer(4));
2485
+ let objectBody = null;
2486
+ // If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
2487
+ // If we're consuming a document, the bytes remaining.
2488
+ let remainingLength = 4;
2489
+ return {
2490
+ async next() {
2491
+ while (true) {
2492
+ // Before fetching new data from upstream, return completed objects.
2493
+ if (completedObjects.length) {
2494
+ return valueResult(completedObjects.shift());
2495
+ }
2496
+ if (isDone) {
2497
+ return doneResult;
2498
+ }
2499
+ const upstreamEvent = await source.next();
2500
+ if (upstreamEvent.done) {
2501
+ isDone = true;
2502
+ if (objectBody || remainingLength != 4) {
2503
+ throw new Error('illegal end of stream in BSON object');
2504
+ }
2505
+ return doneResult;
2506
+ }
2507
+ const chunk = upstreamEvent.value;
2508
+ for (let i = 0; i < chunk.length;) {
2509
+ const availableInData = chunk.length - i;
2510
+ if (objectBody) {
2511
+ // We're in the middle of reading a BSON document.
2512
+ const bytesToRead = Math.min(availableInData, remainingLength);
2513
+ const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
2514
+ objectBody.set(copySource, objectBody.length - remainingLength);
2515
+ i += bytesToRead;
2516
+ remainingLength -= bytesToRead;
2517
+ if (remainingLength == 0) {
2518
+ completedObjects.push(objectBody);
2519
+ // Prepare to read another document, starting with its length
2520
+ objectBody = null;
2521
+ remainingLength = 4;
2522
+ }
2523
+ }
2524
+ else {
2525
+ // Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
2526
+ const bytesToRead = Math.min(availableInData, remainingLength);
2527
+ for (let j = 0; j < bytesToRead; j++) {
2528
+ lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
2529
+ }
2530
+ i += bytesToRead;
2531
+ remainingLength -= bytesToRead;
2532
+ if (remainingLength == 0) {
2533
+ // Transition from reading length header to reading document. Subtracting 4 because the length of the
2534
+ // header is included in length.
2535
+ const length = lengthBuffer.getInt32(0, true /* little endian */);
2536
+ remainingLength = length - 4;
2537
+ if (remainingLength < 1) {
2538
+ throw new Error(`invalid length for bson: ${length}`);
2539
+ }
2540
+ objectBody = new Uint8Array(length);
2541
+ new DataView(objectBody.buffer).setInt32(0, length, true);
2542
+ }
2543
+ }
2544
+ }
2545
+ }
2546
+ }
2547
+ };
2548
+ }
2549
+
2344
2550
  /**
2345
2551
  * Throttle a function to be called at most once every "wait" milliseconds,
2346
2552
  * on the trailing edge.
@@ -2360,45 +2566,128 @@ function throttleTrailing(func, wait) {
2360
2566
  };
2361
2567
  }
2362
2568
  function asyncNotifier() {
2363
- let waitingConsumer = null;
2364
- let hasPendingNotification = false;
2569
+ const queue = new EventQueue();
2365
2570
  return {
2366
2571
  notify() {
2367
- if (waitingConsumer != null) {
2368
- waitingConsumer();
2369
- waitingConsumer = null;
2370
- }
2572
+ if (queue.countOutstandingEvents > 0) ;
2371
2573
  else {
2372
- hasPendingNotification = true;
2574
+ queue.notify();
2373
2575
  }
2374
2576
  },
2375
2577
  waitForNotification(signal) {
2376
- return new Promise((resolve) => {
2377
- if (waitingConsumer != null) {
2378
- throw new Error('Illegal call to waitForNotification, already has a waiter.');
2379
- }
2380
- if (signal.aborted) {
2381
- resolve();
2578
+ return queue.waitForEvent(signal);
2579
+ }
2580
+ };
2581
+ }
2582
+ class EventQueue {
2583
+ options;
2584
+ waitingConsumer;
2585
+ outstandingEvents;
2586
+ constructor(options = {}) {
2587
+ this.options = options;
2588
+ this.outstandingEvents = [];
2589
+ }
2590
+ /**
2591
+ * The amount of buffered events not yet dispatched to listeners.
2592
+ */
2593
+ get countOutstandingEvents() {
2594
+ return this.outstandingEvents.length;
2595
+ }
2596
+ notifyInner(dispatch) {
2597
+ const existing = this.waitingConsumer;
2598
+ this.waitingConsumer = undefined;
2599
+ const dispatchAndNotifyListeners = (waiter) => {
2600
+ dispatch(waiter);
2601
+ this.options.eventDelivered?.();
2602
+ };
2603
+ if (existing) {
2604
+ dispatchAndNotifyListeners(existing);
2605
+ }
2606
+ else {
2607
+ this.outstandingEvents.push(dispatchAndNotifyListeners);
2608
+ }
2609
+ }
2610
+ notify(value) {
2611
+ this.notifyInner((l) => l.resolve(value));
2612
+ }
2613
+ notifyError(error) {
2614
+ this.notifyInner((l) => l.reject(error));
2615
+ }
2616
+ waitForEvent(signal) {
2617
+ return new Promise((resolve, reject) => {
2618
+ if (this.waitingConsumer != null) {
2619
+ throw new Error('Illegal call to waitForEvent, already has a waiter.');
2620
+ }
2621
+ const complete = () => {
2622
+ signal?.removeEventListener('abort', onAbort);
2623
+ };
2624
+ const onAbort = () => {
2625
+ complete();
2626
+ this.waitingConsumer = undefined;
2627
+ resolve(undefined);
2628
+ };
2629
+ const waiter = {
2630
+ resolve: (value) => {
2631
+ complete();
2632
+ resolve(value);
2633
+ },
2634
+ reject: (error) => {
2635
+ complete();
2636
+ reject(error);
2382
2637
  }
2383
- else if (hasPendingNotification) {
2384
- resolve();
2385
- hasPendingNotification = false;
2638
+ };
2639
+ if (signal.aborted) {
2640
+ resolve(undefined);
2641
+ }
2642
+ else if (this.countOutstandingEvents > 0) {
2643
+ const [event] = this.outstandingEvents.splice(0, 1);
2644
+ event(waiter);
2645
+ }
2646
+ else {
2647
+ this.waitingConsumer = waiter;
2648
+ signal.addEventListener('abort', onAbort);
2649
+ }
2650
+ });
2651
+ }
2652
+ /**
2653
+ * Creates an async iterable backed by event queues.
2654
+ *
2655
+ * @param run A function invoked for every new listener. It receives a queue backing the async iterator.
2656
+ * @param abort An additional abort signal. The `run` callback will also be aborted when `AsyncIterator.return` is
2657
+ * called.
2658
+ * @returns An object conforming to the async iterable protocol.
2659
+ */
2660
+ static queueBasedAsyncIterable(run, abort) {
2661
+ return {
2662
+ [symbolAsyncIterator]: () => {
2663
+ const queue = new EventQueue();
2664
+ const controller = new AbortController();
2665
+ function dispose() {
2666
+ controller.abort();
2667
+ abort?.removeEventListener('abort', dispose);
2386
2668
  }
2387
- else {
2388
- function complete() {
2389
- signal.removeEventListener('abort', onAbort);
2390
- resolve();
2669
+ if (abort) {
2670
+ if (abort.aborted) {
2671
+ controller.abort();
2391
2672
  }
2392
- function onAbort() {
2393
- waitingConsumer = null;
2394
- resolve();
2673
+ else {
2674
+ abort.addEventListener('abort', dispose);
2395
2675
  }
2396
- waitingConsumer = complete;
2397
- signal.addEventListener('abort', onAbort);
2398
2676
  }
2399
- });
2400
- }
2401
- };
2677
+ run(queue, controller.signal);
2678
+ return {
2679
+ async next() {
2680
+ const event = await queue.waitForEvent(controller.signal);
2681
+ return event == null ? doneResult : valueResult(event);
2682
+ },
2683
+ async return() {
2684
+ dispose();
2685
+ return doneResult;
2686
+ }
2687
+ };
2688
+ }
2689
+ };
2690
+ }
2402
2691
  }
2403
2692
 
2404
2693
  /**
@@ -8258,7 +8547,7 @@ function requireDist () {
8258
8547
 
8259
8548
  var distExports = requireDist();
8260
8549
 
8261
- var version = "1.54.0";
8550
+ var version = "1.56.0";
8262
8551
  var PACKAGE = {
8263
8552
  version: version};
8264
8553
 
@@ -8425,201 +8714,6 @@ class WebsocketClientTransport {
8425
8714
  }
8426
8715
  }
8427
8716
 
8428
- const doneResult = { done: true, value: undefined };
8429
- function valueResult(value) {
8430
- return { done: false, value };
8431
- }
8432
- /**
8433
- * Expands a source async iterator by allowing to inject events asynchronously.
8434
- *
8435
- * The resulting iterator will emit all events from its source. Additionally though, events can be injected. These
8436
- * events are dropped once the main iterator completes, but are otherwise forwarded.
8437
- *
8438
- * The iterator completes when its source completes, and it supports backpressure by only calling `next()` on the source
8439
- * in response to a `next()` call from downstream if no pending injected events can be dispatched.
8440
- */
8441
- function injectable(source) {
8442
- let sourceIsDone = false;
8443
- let waiter = undefined; // An active, waiting next() call.
8444
- // A pending upstream event that couldn't be dispatched because inject() has been called before it was resolved.
8445
- let pendingSourceEvent = null;
8446
- let sourceFetchInFlight = false;
8447
- let pendingInjectedEvents = [];
8448
- const consumeWaiter = () => {
8449
- const pending = waiter;
8450
- waiter = undefined;
8451
- return pending;
8452
- };
8453
- const fetchFromSource = () => {
8454
- const resolveWaiter = (propagate) => {
8455
- sourceFetchInFlight = false;
8456
- const active = consumeWaiter();
8457
- if (active) {
8458
- propagate(active);
8459
- }
8460
- else {
8461
- pendingSourceEvent = propagate;
8462
- }
8463
- };
8464
- sourceFetchInFlight = true;
8465
- const nextFromSource = source.next();
8466
- nextFromSource.then((value) => {
8467
- sourceIsDone = value.done == true;
8468
- resolveWaiter((w) => w.resolve(value));
8469
- }, (error) => {
8470
- resolveWaiter((w) => w.reject(error));
8471
- });
8472
- };
8473
- return {
8474
- next: () => {
8475
- return new Promise((resolve, reject) => {
8476
- // First priority: Dispatch ready upstream events.
8477
- if (sourceIsDone) {
8478
- return resolve(doneResult);
8479
- }
8480
- if (pendingSourceEvent) {
8481
- pendingSourceEvent({ resolve, reject });
8482
- pendingSourceEvent = null;
8483
- return;
8484
- }
8485
- // Second priority: Dispatch injected events
8486
- if (pendingInjectedEvents.length) {
8487
- return resolve(valueResult(pendingInjectedEvents.shift()));
8488
- }
8489
- // Nothing pending? Fetch from source
8490
- waiter = { resolve, reject };
8491
- if (!sourceFetchInFlight) {
8492
- fetchFromSource();
8493
- }
8494
- });
8495
- },
8496
- inject: (event) => {
8497
- const pending = consumeWaiter();
8498
- if (pending != null) {
8499
- pending.resolve(valueResult(event));
8500
- }
8501
- else {
8502
- pendingInjectedEvents.push(event);
8503
- }
8504
- }
8505
- };
8506
- }
8507
- /**
8508
- * Splits a byte stream at line endings, emitting each line as a string.
8509
- */
8510
- function extractJsonLines(source, decoder) {
8511
- let buffer = '';
8512
- const pendingLines = [];
8513
- let isFinalEvent = false;
8514
- return {
8515
- next: async () => {
8516
- while (true) {
8517
- if (isFinalEvent) {
8518
- return doneResult;
8519
- }
8520
- {
8521
- const first = pendingLines.shift();
8522
- if (first) {
8523
- return { done: false, value: first };
8524
- }
8525
- }
8526
- const { done, value } = await source.next();
8527
- if (done) {
8528
- const remaining = buffer.trim();
8529
- if (remaining.length != 0) {
8530
- isFinalEvent = true;
8531
- return { done: false, value: remaining };
8532
- }
8533
- return doneResult;
8534
- }
8535
- const data = decoder.decode(value, { stream: true });
8536
- buffer += data;
8537
- const lines = buffer.split('\n');
8538
- for (let i = 0; i < lines.length - 1; i++) {
8539
- const l = lines[i].trim();
8540
- if (l.length > 0) {
8541
- pendingLines.push(l);
8542
- }
8543
- }
8544
- buffer = lines[lines.length - 1];
8545
- }
8546
- }
8547
- };
8548
- }
8549
- /**
8550
- * Splits a concatenated stream of BSON objects by emitting individual objects.
8551
- */
8552
- function extractBsonObjects(source) {
8553
- // Fully read but not emitted yet.
8554
- const completedObjects = [];
8555
- // Whether source has returned { done: true }. We do the same once completed objects have been emitted.
8556
- let isDone = false;
8557
- const lengthBuffer = new DataView(new ArrayBuffer(4));
8558
- let objectBody = null;
8559
- // If we're parsing the length field, a number between 1 and 4 (inclusive) describing remaining bytes in the header.
8560
- // If we're consuming a document, the bytes remaining.
8561
- let remainingLength = 4;
8562
- return {
8563
- async next() {
8564
- while (true) {
8565
- // Before fetching new data from upstream, return completed objects.
8566
- if (completedObjects.length) {
8567
- return valueResult(completedObjects.shift());
8568
- }
8569
- if (isDone) {
8570
- return doneResult;
8571
- }
8572
- const upstreamEvent = await source.next();
8573
- if (upstreamEvent.done) {
8574
- isDone = true;
8575
- if (objectBody || remainingLength != 4) {
8576
- throw new Error('illegal end of stream in BSON object');
8577
- }
8578
- return doneResult;
8579
- }
8580
- const chunk = upstreamEvent.value;
8581
- for (let i = 0; i < chunk.length;) {
8582
- const availableInData = chunk.length - i;
8583
- if (objectBody) {
8584
- // We're in the middle of reading a BSON document.
8585
- const bytesToRead = Math.min(availableInData, remainingLength);
8586
- const copySource = new Uint8Array(chunk.buffer, chunk.byteOffset + i, bytesToRead);
8587
- objectBody.set(copySource, objectBody.length - remainingLength);
8588
- i += bytesToRead;
8589
- remainingLength -= bytesToRead;
8590
- if (remainingLength == 0) {
8591
- completedObjects.push(objectBody);
8592
- // Prepare to read another document, starting with its length
8593
- objectBody = null;
8594
- remainingLength = 4;
8595
- }
8596
- }
8597
- else {
8598
- // Copy up to 4 bytes into lengthBuffer, depending on how many we still need.
8599
- const bytesToRead = Math.min(availableInData, remainingLength);
8600
- for (let j = 0; j < bytesToRead; j++) {
8601
- lengthBuffer.setUint8(4 - remainingLength + j, chunk[i + j]);
8602
- }
8603
- i += bytesToRead;
8604
- remainingLength -= bytesToRead;
8605
- if (remainingLength == 0) {
8606
- // Transition from reading length header to reading document. Subtracting 4 because the length of the
8607
- // header is included in length.
8608
- const length = lengthBuffer.getInt32(0, true /* little endian */);
8609
- remainingLength = length - 4;
8610
- if (remainingLength < 1) {
8611
- throw new Error(`invalid length for bson: ${length}`);
8612
- }
8613
- objectBody = new Uint8Array(length);
8614
- new DataView(objectBody.buffer).setInt32(0, length, true);
8615
- }
8616
- }
8617
- }
8618
- }
8619
- }
8620
- };
8621
- }
8622
-
8623
8717
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
8624
8718
  const POWERSYNC_JS_VERSION = PACKAGE.version;
8625
8719
  const SYNC_QUEUE_REQUEST_HIGH_WATER = 10;
@@ -8838,8 +8932,19 @@ class AbstractRemote {
8838
8932
  let pendingSocket = null;
8839
8933
  let keepAliveTimeout;
8840
8934
  let rsocket = null;
8841
- let queue = null;
8935
+ let paused = false;
8936
+ const queue = new EventQueue({
8937
+ eventDelivered: () => {
8938
+ if (queue.countOutstandingEvents <= SYNC_QUEUE_REQUEST_LOW_WATER) {
8939
+ paused = false;
8940
+ requestMore();
8941
+ }
8942
+ }
8943
+ });
8842
8944
  let didClose = false;
8945
+ let connectionEstablished = false;
8946
+ let pendingEventsCount = syncQueueRequestSize;
8947
+ let res = null;
8843
8948
  const abortRequest = () => {
8844
8949
  if (didClose) {
8845
8950
  return;
@@ -8852,10 +8957,23 @@ class AbstractRemote {
8852
8957
  if (rsocket) {
8853
8958
  rsocket.close();
8854
8959
  }
8855
- if (queue) {
8856
- queue.stop();
8857
- }
8960
+ // Send a bogus event to the queue to ensure a pending listener gets woken up. We check for didClose and would
8961
+ // return a doneEvent.
8962
+ queue.notify(null);
8858
8963
  };
8964
+ function push(event) {
8965
+ queue.notify(event);
8966
+ if (queue.countOutstandingEvents >= SYNC_QUEUE_REQUEST_HIGH_WATER) {
8967
+ paused = true;
8968
+ }
8969
+ }
8970
+ function requestMore() {
8971
+ const delta = syncQueueRequestSize - pendingEventsCount;
8972
+ if (!paused && delta > 0) {
8973
+ res?.request(delta);
8974
+ pendingEventsCount = syncQueueRequestSize;
8975
+ }
8976
+ }
8859
8977
  // Handle upstream abort
8860
8978
  if (options.abortSignal.aborted) {
8861
8979
  throw new AbortOperation('Connection request aborted');
@@ -8910,25 +9028,19 @@ class AbstractRemote {
8910
9028
  // Helps to prevent double close scenarios
8911
9029
  rsocket.onClose(() => (rsocket = null));
8912
9030
  return await new Promise((resolve, reject) => {
8913
- let connectionEstablished = false;
8914
- let pendingEventsCount = syncQueueRequestSize;
8915
- let paused = false;
8916
- let res = null;
8917
- function requestMore() {
8918
- const delta = syncQueueRequestSize - pendingEventsCount;
8919
- if (!paused && delta > 0) {
8920
- res?.request(delta);
8921
- pendingEventsCount = syncQueueRequestSize;
9031
+ const queueAsIterator = {
9032
+ next: async () => {
9033
+ if (didClose)
9034
+ return doneResult;
9035
+ const notification = await queue.waitForEvent(options.abortSignal);
9036
+ if (didClose) {
9037
+ return doneResult;
9038
+ }
9039
+ else {
9040
+ return valueResult(notification);
9041
+ }
8922
9042
  }
8923
- }
8924
- const events = new eventIterator.EventIterator((q) => {
8925
- queue = q;
8926
- q.on('highWater', () => (paused = true));
8927
- q.on('lowWater', () => {
8928
- paused = false;
8929
- requestMore();
8930
- });
8931
- }, { highWaterMark: SYNC_QUEUE_REQUEST_HIGH_WATER, lowWaterMark: SYNC_QUEUE_REQUEST_LOW_WATER })[Symbol.asyncIterator]();
9043
+ };
8932
9044
  res = rsocket.requestStream({
8933
9045
  data: toBuffer(options.data),
8934
9046
  metadata: toBuffer({
@@ -8964,11 +9076,11 @@ class AbstractRemote {
8964
9076
  // The connection is active
8965
9077
  if (!connectionEstablished) {
8966
9078
  connectionEstablished = true;
8967
- resolve(events);
9079
+ resolve(queueAsIterator);
8968
9080
  }
8969
9081
  const { data } = payload;
8970
9082
  if (data) {
8971
- queue.push(data);
9083
+ push(data);
8972
9084
  }
8973
9085
  // Less events are now pending
8974
9086
  pendingEventsCount--;
@@ -10566,7 +10678,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
10566
10678
  this.logger.warn('Schema validation failed. Unexpected behaviour could occur', ex);
10567
10679
  }
10568
10680
  this._schema = schema;
10569
- await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
10681
+ await this.database.writeTransaction((tx) => tx.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]));
10570
10682
  await this.database.refreshSchema();
10571
10683
  this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
10572
10684
  }
@@ -10736,7 +10848,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
10736
10848
  * @returns A transaction of CRUD operations to upload, or null if there are none
10737
10849
  */
10738
10850
  async getNextCrudTransaction() {
10739
- const iterator = this.getCrudTransactions()[Symbol.asyncIterator]();
10851
+ const iterator = this.getCrudTransactions()[symbolAsyncIterator]();
10740
10852
  return (await iterator.next()).value;
10741
10853
  }
10742
10854
  /**
@@ -10772,7 +10884,7 @@ class AbstractPowerSyncDatabase extends BaseObserver {
10772
10884
  */
10773
10885
  getCrudTransactions() {
10774
10886
  return {
10775
- [Symbol.asyncIterator]: () => {
10887
+ [symbolAsyncIterator]: () => {
10776
10888
  let lastCrudItemId = -1;
10777
10889
  const sql = `
10778
10890
  WITH RECURSIVE crud_entries AS (
@@ -11086,20 +11198,17 @@ SELECT * FROM crud_entries;
11086
11198
  * @returns An AsyncIterable that yields QueryResults whenever the data changes
11087
11199
  */
11088
11200
  watchWithAsyncGenerator(sql, parameters, options) {
11089
- return new eventIterator.EventIterator((eventOptions) => {
11201
+ return EventQueue.queueBasedAsyncIterable((queue, abort) => {
11090
11202
  const handler = {
11091
11203
  onResult: (result) => {
11092
- eventOptions.push(result);
11204
+ queue.notify(result);
11093
11205
  },
11094
11206
  onError: (error) => {
11095
- eventOptions.fail(error);
11207
+ queue.notifyError(error);
11096
11208
  }
11097
11209
  };
11098
- this.watchWithCallback(sql, parameters, handler, options);
11099
- options?.signal?.addEventListener('abort', () => {
11100
- eventOptions.stop();
11101
- });
11102
- });
11210
+ this.watchWithCallback(sql, parameters, handler, { ...options, signal: abort });
11211
+ }, options?.signal);
11103
11212
  }
11104
11213
  /**
11105
11214
  * Resolves the list of tables that are used in a SQL query.
@@ -11189,28 +11298,23 @@ SELECT * FROM crud_entries;
11189
11298
  * This is preferred over {@link AbstractPowerSyncDatabase.watchWithAsyncGenerator} when multiple queries need to be
11190
11299
  * performed together when data is changed.
11191
11300
  *
11192
- * Note: do not declare this as `async *onChange` as it will not work in React Native.
11193
- *
11194
11301
  * @param options - Options for configuring watch behavior
11195
11302
  * @returns An AsyncIterable that yields change events whenever the specified tables change
11196
11303
  */
11304
+ // Note: do not declare this as `async *onChange` as it will not work in React Native.
11197
11305
  onChangeWithAsyncGenerator(options) {
11198
- const resolvedOptions = options ?? {};
11199
- return new eventIterator.EventIterator((eventOptions) => {
11200
- const dispose = this.onChangeWithCallback({
11306
+ return EventQueue.queueBasedAsyncIterable((queue, abort) => {
11307
+ this.onChangeWithCallback({
11201
11308
  onChange: (event) => {
11202
- eventOptions.push(event);
11309
+ queue.notify(event);
11203
11310
  },
11204
11311
  onError: (error) => {
11205
- eventOptions.fail(error);
11312
+ queue.notifyError(error);
11206
11313
  }
11207
- }, options);
11208
- resolvedOptions.signal?.addEventListener('abort', () => {
11209
- eventOptions.stop();
11210
- // Maybe fail?
11211
- });
11212
- return () => dispose();
11213
- });
11314
+ }, { ...options, signal: abort });
11315
+ // Note: We don't have to track the dispose function returned by onChangeWithCallback, it cleans up
11316
+ // after the abort signal completes.
11317
+ }, options?.signal);
11214
11318
  }
11215
11319
  handleTableChanges(changedTables, watchedTables, onDetectedChanges) {
11216
11320
  if (changedTables.size > 0) {