@powersync/common 0.0.0-dev-20250528152729 → 0.0.0-dev-20250529141956

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.
@@ -122,7 +122,7 @@ export declare abstract class AbstractPowerSyncDatabase extends BaseObserver<Pow
122
122
  get syncStreamImplementation(): StreamingSyncImplementation | null;
123
123
  protected _schema: Schema;
124
124
  private _database;
125
- protected connectionMutex: Mutex;
125
+ protected runExclusiveMutex: Mutex;
126
126
  constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
127
127
  constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
128
128
  constructor(options: PowerSyncDatabaseOptionsWithSettings);
@@ -69,7 +69,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
69
69
  }
70
70
  _schema;
71
71
  _database;
72
- connectionMutex;
72
+ runExclusiveMutex;
73
73
  constructor(options) {
74
74
  super();
75
75
  this.options = options;
@@ -96,7 +96,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
96
96
  this._schema = schema;
97
97
  this.ready = false;
98
98
  this.sdkVersion = '';
99
- this.connectionMutex = new Mutex();
99
+ this.runExclusiveMutex = new Mutex();
100
100
  // Start async init
101
101
  this.connectionManager = new ConnectionManager({
102
102
  createSyncImplementation: async (connector, options) => {
@@ -298,7 +298,7 @@ export class AbstractPowerSyncDatabase extends BaseObserver {
298
298
  * Locking here is mostly only important on web for multiple tab scenarios.
299
299
  */
300
300
  runExclusive(callback) {
301
- return this.connectionMutex.runExclusive(callback);
301
+ return this.runExclusiveMutex.runExclusive(callback);
302
302
  }
303
303
  /**
304
304
  * Connects to stream of events from the PowerSync instance.
@@ -7,6 +7,10 @@ import { PowerSyncConnectionOptions, StreamingSyncImplementation } from './sync/
7
7
  */
8
8
  export interface ConnectionManagerSyncImplementationResult {
9
9
  sync: StreamingSyncImplementation;
10
+ /**
11
+ * Additional cleanup function which is called after the sync stream implementation
12
+ * is disposed.
13
+ */
10
14
  onDispose: () => Promise<void> | void;
11
15
  }
12
16
  /**
@@ -54,7 +58,11 @@ export declare class ConnectionManager extends BaseObserver<ConnectionManagerLis
54
58
  */
55
59
  protected pendingConnectionOptions: StoredConnectionOptions | null;
56
60
  syncStreamImplementation: StreamingSyncImplementation | null;
57
- syncDisposer: (() => Promise<void> | void) | null;
61
+ /**
62
+ * Additional cleanup function which is called after the sync stream implementation
63
+ * is disposed.
64
+ */
65
+ protected syncDisposer: (() => Promise<void> | void) | null;
58
66
  constructor(options: ConnectionManagerOptions);
59
67
  get logger(): ILogger;
60
68
  close(): Promise<void>;
@@ -27,6 +27,10 @@ export class ConnectionManager extends BaseObserver {
27
27
  */
28
28
  pendingConnectionOptions;
29
29
  syncStreamImplementation;
30
+ /**
31
+ * Additional cleanup function which is called after the sync stream implementation
32
+ * is disposed.
33
+ */
30
34
  syncDisposer;
31
35
  constructor(options) {
32
36
  super();
@@ -42,6 +46,7 @@ export class ConnectionManager extends BaseObserver {
42
46
  return this.options.logger;
43
47
  }
44
48
  async close() {
49
+ await this.syncStreamImplementation?.dispose();
45
50
  await this.syncDisposer?.();
46
51
  }
47
52
  async connect(connector, options) {
@@ -58,7 +63,7 @@ export class ConnectionManager extends BaseObserver {
58
63
  // If we do already have pending options, a disconnect has already been performed.
59
64
  // The connectInternal method also does a sanity disconnect to prevent straggler connections.
60
65
  // We should also disconnect if we have already completed a connection attempt.
61
- if (!hadPendingOptions) {
66
+ if (!hadPendingOptions || this.syncStreamImplementation) {
62
67
  await this.disconnectInternal();
63
68
  }
64
69
  // Triggers a connect which checks if pending options are available after the connect completes.
@@ -97,6 +102,9 @@ export class ConnectionManager extends BaseObserver {
97
102
  // A disconnect could have cleared this.
98
103
  return;
99
104
  }
105
+ if (this.disconnectingPromise) {
106
+ return;
107
+ }
100
108
  const { connector, options } = this.pendingConnectionOptions;
101
109
  appliedOptions = options;
102
110
  this.pendingConnectionOptions = null;
@@ -139,14 +147,14 @@ export class ConnectionManager extends BaseObserver {
139
147
  // A disconnect is already in progress
140
148
  return this.disconnectingPromise;
141
149
  }
142
- // Wait if a sync stream implementation is being created before closing it
143
- // (syncStreamImplementation must be assigned before we can properly dispose it)
144
- await this.syncStreamInitPromise;
145
150
  this.disconnectingPromise = this.performDisconnect();
146
151
  await this.disconnectingPromise;
147
152
  this.disconnectingPromise = null;
148
153
  }
149
154
  async performDisconnect() {
155
+ // Wait if a sync stream implementation is being created before closing it
156
+ // (syncStreamImplementation must be assigned before we can properly dispose it)
157
+ await this.syncStreamInitPromise;
150
158
  // Keep reference to the sync stream implementation and disposer
151
159
  // The class members will be cleared before we trigger the disconnect
152
160
  // to prevent any further calls to the sync stream implementation.
@@ -2,10 +2,10 @@ import { Buffer } from 'buffer';
2
2
  import ndjsonStream from 'can-ndjson-stream';
3
3
  import Logger from 'js-logger';
4
4
  import { RSocketConnector } from 'rsocket-core';
5
- import { WebsocketClientTransport } from 'rsocket-websocket-client';
6
5
  import PACKAGE from '../../../../package.json' with { type: 'json' };
7
6
  import { AbortOperation } from '../../../utils/AbortOperation.js';
8
7
  import { DataStream } from '../../../utils/DataStream.js';
8
+ import { WebsocketClientTransport } from './WebsocketClientTransport.js';
9
9
  const POWERSYNC_TRAILING_SLASH_MATCH = /\/+$/;
10
10
  const POWERSYNC_JS_VERSION = PACKAGE.version;
11
11
  const SYNC_QUEUE_REQUEST_LOW_WATER = 5;
@@ -298,12 +298,16 @@ The next upload iteration will be delayed.`);
298
298
  * Either:
299
299
  * - A network request failed with a failed connection or not OKAY response code.
300
300
  * - There was a sync processing error.
301
- * This loop will retry.
301
+ * - The connection was aborted.
302
+ * This loop will retry after a delay if the connection was not aborted.
302
303
  * The nested abort controller will cleanup any open network requests and streams.
303
304
  * The WebRemote should only abort pending fetch requests or close active Readable streams.
304
305
  */
306
+ let delay = true;
305
307
  if (ex instanceof AbortOperation) {
306
308
  this.logger.warn(ex);
309
+ delay = false;
310
+ // A disconnect was requested, we should not delay since there is no explicit retry
307
311
  }
308
312
  else {
309
313
  this.logger.error(ex);
@@ -314,7 +318,9 @@ The next upload iteration will be delayed.`);
314
318
  }
315
319
  });
316
320
  // On error, wait a little before retrying
317
- await this.delayRetry();
321
+ if (delay) {
322
+ await this.delayRetry();
323
+ }
318
324
  }
319
325
  finally {
320
326
  if (!signal.aborted) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Adapted from rsocket-websocket-client
3
+ * https://github.com/rsocket/rsocket-js/blob/e224cf379e747c4f1ddc4f2fa111854626cc8575/packages/rsocket-websocket-client/src/WebsocketClientTransport.ts#L17
4
+ * This adds additional error handling for React Native iOS.
5
+ * This particularly adds a close listener to handle cases where the WebSocket
6
+ * connection closes immediately after opening without emitting an error.
7
+ */
8
+ import { ClientTransport, Closeable, Demultiplexer, DuplexConnection, FrameHandler, Multiplexer, Outbound } from 'rsocket-core';
9
+ import { ClientOptions } from 'rsocket-websocket-client';
10
+ export declare class WebsocketClientTransport implements ClientTransport {
11
+ private readonly url;
12
+ private readonly factory;
13
+ constructor(options: ClientOptions);
14
+ connect(multiplexerDemultiplexerFactory: (outbound: Outbound & Closeable) => Multiplexer & Demultiplexer & FrameHandler): Promise<DuplexConnection>;
15
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Adapted from rsocket-websocket-client
3
+ * https://github.com/rsocket/rsocket-js/blob/e224cf379e747c4f1ddc4f2fa111854626cc8575/packages/rsocket-websocket-client/src/WebsocketClientTransport.ts#L17
4
+ * This adds additional error handling for React Native iOS.
5
+ * This particularly adds a close listener to handle cases where the WebSocket
6
+ * connection closes immediately after opening without emitting an error.
7
+ */
8
+ import { Deserializer } from 'rsocket-core';
9
+ import { WebsocketDuplexConnection } from 'rsocket-websocket-client/dist/WebsocketDuplexConnection.js';
10
+ export class WebsocketClientTransport {
11
+ url;
12
+ factory;
13
+ constructor(options) {
14
+ this.url = options.url;
15
+ this.factory = options.wsCreator ?? ((url) => new WebSocket(url));
16
+ }
17
+ connect(multiplexerDemultiplexerFactory) {
18
+ return new Promise((resolve, reject) => {
19
+ const websocket = this.factory(this.url);
20
+ websocket.binaryType = 'arraybuffer';
21
+ let removeListeners;
22
+ const openListener = () => {
23
+ removeListeners();
24
+ resolve(new WebsocketDuplexConnection(websocket, new Deserializer(), multiplexerDemultiplexerFactory));
25
+ };
26
+ const errorListener = (ev) => {
27
+ removeListeners();
28
+ reject(ev.error);
29
+ };
30
+ /**
31
+ * In some cases, such as React Native iOS, the WebSocket connection may close immediately after opening
32
+ * without and error. In such cases, we need to handle the close event to reject the promise.
33
+ */
34
+ const closeListener = () => {
35
+ removeListeners();
36
+ reject(new Error('WebSocket connection closed while opening'));
37
+ };
38
+ removeListeners = () => {
39
+ websocket.removeEventListener('open', openListener);
40
+ websocket.removeEventListener('error', errorListener);
41
+ websocket.removeEventListener('close', closeListener);
42
+ };
43
+ websocket.addEventListener('open', openListener);
44
+ websocket.addEventListener('error', errorListener);
45
+ websocket.addEventListener('close', closeListener);
46
+ });
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "0.0.0-dev-20250528152729",
3
+ "version": "0.0.0-dev-20250529141956",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"