Skip to content

Commit 96ddd5d

Browse files
Improve Connection Locks (#607)
1 parent dea6877 commit 96ddd5d

26 files changed

+803
-308
lines changed

.changeset/cyan-penguins-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/react-native': minor
3+
---
4+
5+
Fixed issue where iOS WebSockets could fail to reconnect after a connection issue.

.changeset/empty-pants-give.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@powersync/react-native': minor
3+
'@powersync/common': minor
4+
'@powersync/node': minor
5+
'@powersync/web': minor
6+
---
7+
8+
Improved behaviour when connect is called multiple times in quick succession. Updating client parameters should now be more responsive.

.changeset/fuzzy-beers-occur.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@powersync/diagnostics-app': minor
3+
---
4+
5+
- Added Sync error alert banner to all views.
6+
- Fix bug where clicking signOut would not disconnect from the PowerSync service.
7+
- Updated implementation to fetch sync errors from the SyncStatus.

.changeset/fuzzy-ties-double.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/common': patch
3+
---
4+
5+
Fixed bug where changes in SyncStatus downloadError and uploadError might not be reported.

demos/react-native-supabase-todolist/library/powersync/system.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ import React from 'react';
55
import { SupabaseStorageAdapter } from '../storage/SupabaseStorageAdapter';
66

77
import { type AttachmentRecord } from '@powersync/attachments';
8+
import { configureFts } from '../fts/fts_setup';
89
import { KVStorage } from '../storage/KVStorage';
910
import { AppConfig } from '../supabase/AppConfig';
1011
import { SupabaseConnector } from '../supabase/SupabaseConnector';
1112
import { AppSchema } from './AppSchema';
1213
import { PhotoAttachmentQueue } from './PhotoAttachmentQueue';
13-
import { configureFts } from '../fts/fts_setup';
1414

1515
const logger = createBaseLogger();
1616
logger.useDefaults();
17-
logger.setLevel(LogLevel.INFO);
17+
logger.setLevel(LogLevel.DEBUG);
1818

1919
export class System {
2020
kvStorage: KVStorage;
@@ -31,19 +31,22 @@ export class System {
3131
schema: AppSchema,
3232
database: {
3333
dbFilename: 'sqlite.db'
34-
}
34+
},
35+
logger
3536
});
3637
/**
3738
* The snippet below uses OP-SQLite as the default database adapter.
3839
* You will have to uninstall `@journeyapps/react-native-quick-sqlite` and
3940
* install both `@powersync/op-sqlite` and `@op-engineering/op-sqlite` to use this.
4041
*
42+
* ```typescript
4143
* import { OPSqliteOpenFactory } from '@powersync/op-sqlite'; // Add this import
4244
*
4345
* const factory = new OPSqliteOpenFactory({
44-
* dbFilename: 'sqlite.db'
46+
* dbFilename: 'sqlite.db'
4547
* });
4648
* this.powersync = new PowerSyncDatabase({ database: factory, schema: AppSchema });
49+
* ```
4750
*/
4851

4952
if (AppConfig.supabaseBucket) {

packages/common/rollup.config.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import json from '@rollup/plugin-json';
44
import nodeResolve from '@rollup/plugin-node-resolve';
55
import terser from '@rollup/plugin-terser';
66

7+
/**
8+
* @returns {import('rollup').RollupOptions}
9+
*/
710
export default (commandLineArgs) => {
8-
const sourcemap = (commandLineArgs.sourceMap || 'true') == 'true';
11+
const sourceMap = (commandLineArgs.sourceMap || 'true') == 'true';
912

1013
// Clears rollup CLI warning https://github.com/rollup/rollup/issues/2694
1114
delete commandLineArgs.sourceMap;
@@ -15,7 +18,7 @@ export default (commandLineArgs) => {
1518
output: {
1619
file: 'dist/bundle.mjs',
1720
format: 'esm',
18-
sourcemap: sourcemap
21+
sourcemap: sourceMap
1922
},
2023
plugins: [
2124
json(),
@@ -27,7 +30,7 @@ export default (commandLineArgs) => {
2730
// Used by can-ndjson-stream
2831
TextDecoder: ['text-encoding', 'TextDecoder']
2932
}),
30-
terser()
33+
terser({ sourceMap })
3134
],
3235
// This makes life easier
3336
external: [

packages/common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import {
99
UpdateNotification,
1010
isBatchedUpdateNotification
1111
} from '../db/DBAdapter.js';
12+
import { FULL_SYNC_PRIORITY } from '../db/crud/SyncProgress.js';
1213
import { SyncPriorityStatus, SyncStatus } from '../db/crud/SyncStatus.js';
1314
import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
1415
import { Schema } from '../db/schema/Schema.js';
1516
import { BaseObserver } from '../utils/BaseObserver.js';
1617
import { ControlledExecutor } from '../utils/ControlledExecutor.js';
17-
import { mutexRunExclusive } from '../utils/mutex.js';
1818
import { throttleTrailing } from '../utils/async.js';
19+
import { mutexRunExclusive } from '../utils/mutex.js';
20+
import { ConnectionManager } from './ConnectionManager.js';
1921
import { SQLOpenFactory, SQLOpenOptions, isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
2022
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
2123
import { runOnSchemaChange } from './runOnSchemaChange.js';
@@ -32,7 +34,6 @@ import {
3234
type PowerSyncConnectionOptions,
3335
type RequiredAdditionalConnectionOptions
3436
} from './sync/stream/AbstractStreamingSyncImplementation.js';
35-
import { FULL_SYNC_PRIORITY } from '../db/crud/SyncProgress.js';
3637

3738
export interface DisconnectAndClearOptions {
3839
/** When set to false, data in local-only tables is preserved. */
@@ -165,17 +166,22 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
165166
*/
166167
currentStatus: SyncStatus;
167168

168-
syncStreamImplementation?: StreamingSyncImplementation;
169169
sdkVersion: string;
170170

171171
protected bucketStorageAdapter: BucketStorageAdapter;
172-
private syncStatusListenerDisposer?: () => void;
173172
protected _isReadyPromise: Promise<void>;
173+
protected connectionManager: ConnectionManager;
174+
175+
get syncStreamImplementation() {
176+
return this.connectionManager.syncStreamImplementation;
177+
}
174178

175179
protected _schema: Schema;
176180

177181
private _database: DBAdapter;
178182

183+
protected runExclusiveMutex: Mutex;
184+
179185
constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
180186
constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
181187
constructor(options: PowerSyncDatabaseOptionsWithSettings);
@@ -206,7 +212,33 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
206212
this._schema = schema;
207213
this.ready = false;
208214
this.sdkVersion = '';
215+
this.runExclusiveMutex = new Mutex();
209216
// Start async init
217+
this.connectionManager = new ConnectionManager({
218+
createSyncImplementation: async (connector, options) => {
219+
await this.waitForReady();
220+
221+
return this.runExclusive(async () => {
222+
const sync = this.generateSyncStreamImplementation(connector, this.resolvedConnectionOptions(options));
223+
const onDispose = sync.registerListener({
224+
statusChanged: (status) => {
225+
this.currentStatus = new SyncStatus({
226+
...status.toJSON(),
227+
hasSynced: this.currentStatus?.hasSynced || !!status.lastSyncedAt
228+
});
229+
this.iterateListeners((cb) => cb.statusChanged?.(this.currentStatus));
230+
}
231+
});
232+
await sync.waitForReady();
233+
234+
return {
235+
sync,
236+
onDispose
237+
};
238+
});
239+
},
240+
logger: this.logger
241+
});
210242
this._isReadyPromise = this.initialize();
211243
}
212244

@@ -425,34 +457,19 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
425457
};
426458
}
427459

460+
/**
461+
* Locking mechanism for exclusively running critical portions of connect/disconnect operations.
462+
* Locking here is mostly only important on web for multiple tab scenarios.
463+
*/
464+
protected runExclusive<T>(callback: () => Promise<T>): Promise<T> {
465+
return this.runExclusiveMutex.runExclusive(callback);
466+
}
467+
428468
/**
429469
* Connects to stream of events from the PowerSync instance.
430470
*/
431471
async connect(connector: PowerSyncBackendConnector, options?: PowerSyncConnectionOptions) {
432-
await this.waitForReady();
433-
434-
// close connection if one is open
435-
await this.disconnect();
436-
if (this.closed) {
437-
throw new Error('Cannot connect using a closed client');
438-
}
439-
440-
const resolvedConnectOptions = this.resolvedConnectionOptions(options);
441-
442-
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector, resolvedConnectOptions);
443-
this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
444-
statusChanged: (status) => {
445-
this.currentStatus = new SyncStatus({
446-
...status.toJSON(),
447-
hasSynced: this.currentStatus?.hasSynced || !!status.lastSyncedAt
448-
});
449-
this.iterateListeners((cb) => cb.statusChanged?.(this.currentStatus));
450-
}
451-
});
452-
453-
await this.syncStreamImplementation.waitForReady();
454-
this.syncStreamImplementation.triggerCrudUpload();
455-
await this.syncStreamImplementation.connect(options);
472+
return this.connectionManager.connect(connector, options);
456473
}
457474

458475
/**
@@ -461,11 +478,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
461478
* Use {@link connect} to connect again.
462479
*/
463480
async disconnect() {
464-
await this.waitForReady();
465-
await this.syncStreamImplementation?.disconnect();
466-
this.syncStatusListenerDisposer?.();
467-
await this.syncStreamImplementation?.dispose();
468-
this.syncStreamImplementation = undefined;
481+
return this.connectionManager.disconnect();
469482
}
470483

471484
/**
@@ -512,7 +525,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
512525
await this.disconnect();
513526
}
514527

515-
await this.syncStreamImplementation?.dispose();
528+
await this.connectionManager.close();
516529
await this.database.close();
517530
this.closed = true;
518531
}

0 commit comments

Comments
 (0)