Skip to content

Commit aede9e7

Browse files
[Improvement] Shared Sync Worker (#72)
1 parent 60f666b commit aede9e7

27 files changed

+1267
-328
lines changed

.changeset/few-beds-camp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@journeyapps/powersync-sdk-web': minor
3+
---
4+
5+
Improved multiple tab syncing by unloading stream and sync bucket adapter functionality to shared webworker.

.changeset/nasty-tigers-reflect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@journeyapps/powersync-sdk-common': patch
3+
---
4+
5+
Internally moved crud upload watching to `SqliteBucketStorageAdapter`. Added `dispose` methods for sync stream clients and better closing of clients.

packages/powersync-sdk-common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnecto
99
import {
1010
AbstractStreamingSyncImplementation,
1111
DEFAULT_CRUD_UPLOAD_THROTTLE_MS,
12-
StreamingSyncImplementationListener
12+
StreamingSyncImplementationListener,
13+
StreamingSyncImplementation
1314
} from './sync/stream/AbstractStreamingSyncImplementation';
1415
import { CrudBatch } from './sync/bucket/CrudBatch';
1516
import { CrudTransaction } from './sync/bucket/CrudTransaction';
@@ -63,12 +64,25 @@ export interface PowerSyncDBListener extends StreamingSyncImplementationListener
6364
initialized: () => void;
6465
}
6566

67+
export interface PowerSyncCloseOptions {
68+
/**
69+
* Disconnect the sync stream client if connected.
70+
* This is usually true, but can be false for Web when using
71+
* multiple tabs and a shared sync provider.
72+
*/
73+
disconnect?: boolean;
74+
}
75+
6676
const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
6777

6878
const DEFAULT_DISCONNECT_CLEAR_OPTIONS: DisconnectAndClearOptions = {
6979
clearLocal: true
7080
};
7181

82+
export const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions = {
83+
disconnect: true
84+
};
85+
7286
export const DEFAULT_WATCH_THROTTLE_MS = 30;
7387

7488
export const DEFAULT_POWERSYNC_DB_OPTIONS = {
@@ -101,10 +115,9 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
101115
* Current connection status.
102116
*/
103117
currentStatus?: SyncStatus;
104-
syncStreamImplementation?: AbstractStreamingSyncImplementation;
118+
syncStreamImplementation?: StreamingSyncImplementation;
105119
sdkVersion: string;
106120

107-
private abortController: AbortController | null;
108121
protected bucketStorageAdapter: BucketStorageAdapter;
109122
private syncStatusListenerDisposer?: () => void;
110123
protected _isReadyPromise: Promise<void>;
@@ -113,7 +126,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
113126
constructor(protected options: PowerSyncDatabaseOptions) {
114127
super();
115128
this.bucketStorageAdapter = this.generateBucketStorageAdapter();
116-
this.closed = true;
129+
this.closed = false;
117130
this.currentStatus = undefined;
118131
this.options = { ...DEFAULT_POWERSYNC_DB_OPTIONS, ...options };
119132
this._schema = options.schema;
@@ -189,7 +202,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
189202
* Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
190203
*/
191204
async updateSchema(schema: Schema) {
192-
if (this.abortController) {
205+
if (this.syncStreamImplementation) {
193206
throw new Error('Cannot update schema while connected');
194207
}
195208

@@ -207,19 +220,6 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
207220
await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
208221
}
209222

210-
/**
211-
* Queues a CRUD upload when internal CRUD tables have been updated.
212-
*/
213-
protected async watchCrudUploads() {
214-
for await (const event of this.onChange({
215-
tables: [PSInternalTable.CRUD],
216-
rawTableNames: true,
217-
signal: this.abortController?.signal
218-
})) {
219-
this.syncStreamImplementation?.triggerCrudUpload();
220-
}
221-
}
222-
223223
/**
224224
* Wait for initialization to complete.
225225
* While initializing is automatic, this helps to catch and report initialization errors.
@@ -232,10 +232,15 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
232232
* Connects to stream of events from the PowerSync instance.
233233
*/
234234
async connect(connector: PowerSyncBackendConnector) {
235+
await this.waitForReady();
236+
235237
// close connection if one is open
236238
await this.disconnect();
237239

238-
await this.waitForReady();
240+
if (this.closed) {
241+
throw new Error('Cannot connect using a closed client');
242+
}
243+
239244
this.syncStreamImplementation = this.generateSyncStreamImplementation(connector);
240245
this.syncStatusListenerDisposer = this.syncStreamImplementation.registerListener({
241246
statusChanged: (status) => {
@@ -244,11 +249,9 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
244249
}
245250
});
246251

247-
this.abortController = new AbortController();
248-
// Begin network stream
252+
await this.syncStreamImplementation.waitForReady();
249253
this.syncStreamImplementation.triggerCrudUpload();
250-
this.syncStreamImplementation.streamingSync(this.abortController.signal);
251-
this.watchCrudUploads();
254+
this.syncStreamImplementation.connect();
252255
}
253256

254257
/**
@@ -257,9 +260,10 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
257260
* Use {@link connect} to connect again.
258261
*/
259262
async disconnect() {
260-
this.abortController?.abort();
263+
await this.syncStreamImplementation?.disconnect();
261264
this.syncStatusListenerDisposer?.();
262-
this.abortController = null;
265+
await this.syncStreamImplementation?.dispose();
266+
this.syncStreamImplementation = undefined;
263267
}
264268

265269
/**
@@ -308,11 +312,17 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
308312
* Once close is called, this connection cannot be used again - a new one
309313
* must be constructed.
310314
*/
311-
async close() {
315+
async close(options: PowerSyncCloseOptions = DEFAULT_POWERSYNC_CLOSE_OPTIONS) {
312316
await this.waitForReady();
313317

314-
await this.disconnect();
318+
const { disconnect } = options;
319+
if (disconnect) {
320+
await this.disconnect();
321+
}
322+
323+
await this.syncStreamImplementation?.dispose();
315324
this.database.close();
325+
this.closed = true;
316326
}
317327

318328
/**

packages/powersync-sdk-common/src/client/sync/bucket/BucketStorageAdapter.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { OpId } from './CrudEntry';
22
import { CrudBatch } from './CrudBatch';
33
import { SyncDataBatch } from './SyncDataBatch';
4+
import { BaseListener, BaseObserver, Disposable } from '../../../utils/BaseObserver';
45

56
export interface Checkpoint {
67
last_op_id: OpId;
@@ -44,7 +45,11 @@ export enum PSInternalTable {
4445
OPLOG = 'ps_oplog'
4546
}
4647

47-
export interface BucketStorageAdapter {
48+
export interface BucketStorageListener extends BaseListener {
49+
crudUpdate: () => void;
50+
}
51+
52+
export interface BucketStorageAdapter extends BaseObserver<BucketStorageListener>, Disposable {
4853
init(): Promise<void>;
4954
saveSyncData(batch: SyncDataBatch): Promise<void>;
5055
removeBuckets(buckets: string[]): Promise<void>;

packages/powersync-sdk-common/src/client/sync/bucket/SqliteBucketStorage.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
import { v4 as uuid } from 'uuid';
22
import { Mutex } from 'async-mutex';
3-
import { DBAdapter, Transaction } from '../../../db/DBAdapter';
4-
import { BucketState, BucketStorageAdapter, Checkpoint, SyncLocalDatabaseResult } from './BucketStorageAdapter';
3+
import { DBAdapter, Transaction, extractTableUpdates } from '../../../db/DBAdapter';
4+
import {
5+
BucketState,
6+
BucketStorageAdapter,
7+
BucketStorageListener,
8+
Checkpoint,
9+
PSInternalTable,
10+
SyncLocalDatabaseResult
11+
} from './BucketStorageAdapter';
512
import { OpTypeEnum } from './OpType';
613
import { CrudBatch } from './CrudBatch';
714
import { CrudEntry } from './CrudEntry';
815
import { SyncDataBatch } from './SyncDataBatch';
916
import Logger, { ILogger } from 'js-logger';
17+
import { BaseObserver } from '../../../utils/BaseObserver';
1018

1119
const COMPACT_OPERATION_INTERVAL = 1_000;
1220

13-
export class SqliteBucketStorage implements BucketStorageAdapter {
21+
export class SqliteBucketStorage extends BaseObserver<BucketStorageListener> implements BucketStorageAdapter {
1422
static MAX_OP_ID = '9223372036854775807';
1523

1624
public tableNames: Set<string>;
1725
private pendingBucketDeletes: boolean;
1826
private _hasCompletedSync: boolean;
27+
private updateListener: () => void;
1928

2029
/**
2130
* Count up, and do a compact on startup.
@@ -27,9 +36,18 @@ export class SqliteBucketStorage implements BucketStorageAdapter {
2736
private mutex: Mutex,
2837
private logger: ILogger = Logger.get('SqliteBucketStorage')
2938
) {
39+
super();
3040
this._hasCompletedSync = false;
3141
this.pendingBucketDeletes = true;
3242
this.tableNames = new Set();
43+
this.updateListener = db.registerListener({
44+
tablesUpdated: (update) => {
45+
const tables = extractTableUpdates(update);
46+
if (tables.includes(PSInternalTable.CRUD)) {
47+
this.iterateListeners((l) => l.crudUpdate?.());
48+
}
49+
}
50+
});
3351
}
3452

3553
async init() {
@@ -42,6 +60,10 @@ export class SqliteBucketStorage implements BucketStorageAdapter {
4260
}
4361
}
4462

63+
async dispose() {
64+
this.updateListener?.();
65+
}
66+
4567
getMaxOpId() {
4668
return SqliteBucketStorage.MAX_OP_ID;
4769
}

0 commit comments

Comments
 (0)