@powersync/web 0.0.0-dev-20250526133243 → 0.0.0-dev-20250528152729

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.
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/web",
3
- "version": "1.20.1",
3
+ "version": "1.21.0",
4
4
  "description": "PowerSync web SDK. Sync Postgres, MongoDB or MySQL with SQLite in your web app",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -61,7 +61,7 @@
61
61
  "license": "Apache-2.0",
62
62
  "peerDependencies": {
63
63
  "@journeyapps/wa-sqlite": "^1.2.4",
64
- "@powersync/common": "workspace:^1.30.0"
64
+ "@powersync/common": "workspace:^1.31.0"
65
65
  },
66
66
  "dependencies": {
67
67
  "@powersync/common": "workspace:*",
@@ -153,9 +153,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
153
153
  */
154
154
  async connect(options) {
155
155
  await this.waitForReady();
156
- // This is needed since a new tab won't have any reference to the
157
- // shared worker sync implementation since that is only created on the first call to `connect`.
158
- await this.disconnect();
159
156
  return this.syncManager.connect(options);
160
157
  }
161
158
  async disconnect() {
@@ -171,12 +168,21 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
171
168
  }
172
169
  async dispose() {
173
170
  await this.waitForReady();
174
- // Signal the shared worker that this client is closing its connection to the worker
175
- const closeMessagePayload = {
176
- event: SharedSyncClientEvent.CLOSE_CLIENT,
177
- data: {}
178
- };
179
- this.messagePort.postMessage(closeMessagePayload);
171
+ await new Promise((resolve) => {
172
+ // Listen for the close acknowledgment from the worker
173
+ this.messagePort.addEventListener('message', (event) => {
174
+ const payload = event.data;
175
+ if (payload?.event === SharedSyncClientEvent.CLOSE_ACK) {
176
+ resolve();
177
+ }
178
+ });
179
+ // Signal the shared worker that this client is closing its connection to the worker
180
+ const closeMessagePayload = {
181
+ event: SharedSyncClientEvent.CLOSE_CLIENT,
182
+ data: {}
183
+ };
184
+ this.messagePort.postMessage(closeMessagePayload);
185
+ });
180
186
  // Release the proxy
181
187
  this.syncManager[Comlink.releaseProxy]();
182
188
  this.messagePort.close();
@@ -1,4 +1,5 @@
1
- import { type AbstractStreamingSyncImplementation, type ILogger, type ILogLevel, type LockOptions, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions, BaseObserver, DBAdapter, SyncStatus } from '@powersync/common';
1
+ import { type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions, BaseObserver, ConnectionManager, DBAdapter, SyncStatus } from '@powersync/common';
2
+ import { Mutex } from 'async-mutex';
2
3
  import * as Comlink from 'comlink';
3
4
  import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from '../../db/sync/WebStreamingSyncImplementation';
4
5
  import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
@@ -11,7 +12,8 @@ export declare enum SharedSyncClientEvent {
11
12
  * This client requests the shared sync manager should
12
13
  * close it's connection to the client.
13
14
  */
14
- CLOSE_CLIENT = "close-client"
15
+ CLOSE_CLIENT = "close-client",
16
+ CLOSE_ACK = "close-ack"
15
17
  }
16
18
  export type ManualSharedSyncPayload = {
17
19
  event: SharedSyncClientEvent;
@@ -51,7 +53,6 @@ export type RemoteOperationAbortController = {
51
53
  */
52
54
  export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener> implements StreamingSyncImplementation {
53
55
  protected ports: WrappedSyncPort[];
54
- protected syncStreamClient: AbstractStreamingSyncImplementation | null;
55
56
  protected isInitialized: Promise<void>;
56
57
  protected statusListener?: () => void;
57
58
  protected fetchCredentialsController?: RemoteOperationAbortController;
@@ -60,41 +61,43 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
60
61
  protected syncParams: SharedSyncInitOptions | null;
61
62
  protected logger: ILogger;
62
63
  protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
64
+ protected portMutex: Mutex;
65
+ protected connectionManager: ConnectionManager;
63
66
  syncStatus: SyncStatus;
64
67
  broadCastLogger: ILogger;
65
68
  constructor();
66
- waitForStatus(status: SyncStatusOptions): Promise<void>;
67
- waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
68
69
  get lastSyncedAt(): Date | undefined;
69
70
  get isConnected(): boolean;
71
+ waitForStatus(status: SyncStatusOptions): Promise<void>;
72
+ waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
70
73
  waitForReady(): Promise<void>;
71
74
  setLogLevel(level: ILogLevel): void;
72
75
  /**
73
76
  * Configures the DBAdapter connection and a streaming sync client.
74
77
  */
75
78
  setParams(params: SharedSyncInitOptions): Promise<void>;
76
- dispose(): Promise<void | undefined>;
79
+ dispose(): Promise<void>;
77
80
  /**
78
81
  * Connects to the PowerSync backend instance.
79
82
  * Multiple tabs can safely call this in their initialization.
80
83
  * The connection will simply be reconnected whenever a new tab
81
84
  * connects.
82
85
  */
83
- connect(options?: PowerSyncConnectionOptions): Promise<any>;
84
- disconnect(): Promise<any>;
86
+ connect(options?: PowerSyncConnectionOptions): Promise<void>;
87
+ disconnect(): Promise<void>;
85
88
  /**
86
89
  * Adds a new client tab's message port to the list of connected ports
87
90
  */
88
- addPort(port: MessagePort): void;
91
+ addPort(port: MessagePort): Promise<void>;
89
92
  /**
90
93
  * Removes a message port client from this manager's managed
91
94
  * clients.
92
95
  */
93
- removePort(port: MessagePort): Promise<void>;
96
+ removePort(port: MessagePort): Promise<(() => void) | undefined>;
94
97
  triggerCrudUpload(): void;
95
- obtainLock<T>(lockOptions: LockOptions<T>): Promise<T>;
96
98
  hasCompletedSync(): Promise<boolean>;
97
99
  getWriteCheckpoint(): Promise<string>;
100
+ protected withSyncImplementation<T>(callback: (sync: StreamingSyncImplementation) => Promise<T>): Promise<T>;
98
101
  protected generateStreamingImplementation(): WebStreamingSyncImplementation;
99
102
  protected openInternalDB(): Promise<void>;
100
103
  /**
@@ -1,11 +1,10 @@
1
- import { AbortOperation, BaseObserver, createLogger, SqliteBucketStorage, SyncStatus } from '@powersync/common';
1
+ import { AbortOperation, BaseObserver, ConnectionManager, createLogger, SqliteBucketStorage, SyncStatus } from '@powersync/common';
2
2
  import { Mutex } from 'async-mutex';
3
3
  import * as Comlink from 'comlink';
4
4
  import { WebRemote } from '../../db/sync/WebRemote';
5
5
  import { WebStreamingSyncImplementation } from '../../db/sync/WebStreamingSyncImplementation';
6
6
  import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter';
7
7
  import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection';
8
- import { getNavigatorLocks } from '../../shared/navigator';
9
8
  import { BroadcastLogger } from './BroadcastLogger';
10
9
  /**
11
10
  * Manual message events for shared sync clients
@@ -17,14 +16,20 @@ export var SharedSyncClientEvent;
17
16
  * close it's connection to the client.
18
17
  */
19
18
  SharedSyncClientEvent["CLOSE_CLIENT"] = "close-client";
19
+ SharedSyncClientEvent["CLOSE_ACK"] = "close-ack";
20
20
  })(SharedSyncClientEvent || (SharedSyncClientEvent = {}));
21
+ /**
22
+ * HACK: The shared implementation wraps and provides its own
23
+ * PowerSyncBackendConnector when generating the streaming sync implementation.
24
+ * We provide this unused placeholder when connecting with the ConnectionManager.
25
+ */
26
+ const CONNECTOR_PLACEHOLDER = {};
21
27
  /**
22
28
  * @internal
23
29
  * Shared sync implementation which runs inside a shared webworker
24
30
  */
25
31
  export class SharedSyncImplementation extends BaseObserver {
26
32
  ports;
27
- syncStreamClient;
28
33
  isInitialized;
29
34
  statusListener;
30
35
  fetchCredentialsController;
@@ -33,6 +38,8 @@ export class SharedSyncImplementation extends BaseObserver {
33
38
  syncParams;
34
39
  logger;
35
40
  lastConnectOptions;
41
+ portMutex;
42
+ connectionManager;
36
43
  syncStatus;
37
44
  broadCastLogger;
38
45
  constructor() {
@@ -40,9 +47,9 @@ export class SharedSyncImplementation extends BaseObserver {
40
47
  this.ports = [];
41
48
  this.dbAdapter = null;
42
49
  this.syncParams = null;
43
- this.syncStreamClient = null;
44
50
  this.logger = createLogger('shared-sync');
45
51
  this.lastConnectOptions = undefined;
52
+ this.portMutex = new Mutex();
46
53
  this.isInitialized = new Promise((resolve) => {
47
54
  const callback = this.registerListener({
48
55
  initialized: () => {
@@ -53,20 +60,41 @@ export class SharedSyncImplementation extends BaseObserver {
53
60
  });
54
61
  this.syncStatus = new SyncStatus({});
55
62
  this.broadCastLogger = new BroadcastLogger(this.ports);
56
- }
57
- async waitForStatus(status) {
58
- await this.waitForReady();
59
- return this.syncStreamClient.waitForStatus(status);
60
- }
61
- async waitUntilStatusMatches(predicate) {
62
- await this.waitForReady();
63
- return this.syncStreamClient.waitUntilStatusMatches(predicate);
63
+ this.connectionManager = new ConnectionManager({
64
+ createSyncImplementation: async (connector, options) => {
65
+ await this.waitForReady();
66
+ if (!this.dbAdapter) {
67
+ await this.openInternalDB();
68
+ }
69
+ const sync = this.generateStreamingImplementation();
70
+ const onDispose = sync.registerListener({
71
+ statusChanged: (status) => {
72
+ this.updateAllStatuses(status.toJSON());
73
+ }
74
+ });
75
+ return {
76
+ sync,
77
+ onDispose
78
+ };
79
+ },
80
+ logger: this.logger
81
+ });
64
82
  }
65
83
  get lastSyncedAt() {
66
- return this.syncStreamClient?.lastSyncedAt;
84
+ return this.connectionManager.syncStreamImplementation?.lastSyncedAt;
67
85
  }
68
86
  get isConnected() {
69
- return this.syncStreamClient?.isConnected ?? false;
87
+ return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
88
+ }
89
+ async waitForStatus(status) {
90
+ return this.withSyncImplementation(async (sync) => {
91
+ return sync.waitForStatus(status);
92
+ });
93
+ }
94
+ async waitUntilStatusMatches(predicate) {
95
+ return this.withSyncImplementation(async (sync) => {
96
+ return sync.waitUntilStatusMatches(predicate);
97
+ });
70
98
  }
71
99
  async waitForReady() {
72
100
  return this.isInitialized;
@@ -79,25 +107,32 @@ export class SharedSyncImplementation extends BaseObserver {
79
107
  * Configures the DBAdapter connection and a streaming sync client.
80
108
  */
81
109
  async setParams(params) {
82
- if (this.syncParams) {
83
- // Cannot modify already existing sync implementation
84
- return;
85
- }
86
- this.syncParams = params;
87
- if (params.streamOptions?.flags?.broadcastLogs) {
88
- this.logger = this.broadCastLogger;
89
- }
90
- self.onerror = (event) => {
91
- // Share any uncaught events on the broadcast logger
92
- this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
93
- };
94
- await this.openInternalDB();
95
- this.iterateListeners((l) => l.initialized?.());
110
+ await this.portMutex.runExclusive(async () => {
111
+ if (this.syncParams) {
112
+ if (!this.dbAdapter) {
113
+ await this.openInternalDB();
114
+ }
115
+ // Cannot modify already existing sync implementation
116
+ return;
117
+ }
118
+ this.syncParams = params;
119
+ if (params.streamOptions?.flags?.broadcastLogs) {
120
+ this.logger = this.broadCastLogger;
121
+ }
122
+ self.onerror = (event) => {
123
+ // Share any uncaught events on the broadcast logger
124
+ this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
125
+ };
126
+ if (!this.dbAdapter) {
127
+ await this.openInternalDB();
128
+ }
129
+ this.iterateListeners((l) => l.initialized?.());
130
+ });
96
131
  }
97
132
  async dispose() {
98
133
  await this.waitForReady();
99
134
  this.statusListener?.();
100
- return this.syncStreamClient?.dispose();
135
+ return this.connectionManager.close();
101
136
  }
102
137
  /**
103
138
  * Connects to the PowerSync backend instance.
@@ -106,99 +141,103 @@ export class SharedSyncImplementation extends BaseObserver {
106
141
  * connects.
107
142
  */
108
143
  async connect(options) {
109
- await this.waitForReady();
110
- // This effectively queues connect and disconnect calls. Ensuring multiple tabs' requests are synchronized
111
- return getNavigatorLocks().request('shared-sync-connect', async () => {
112
- if (!this.dbAdapter) {
113
- await this.openInternalDB();
114
- }
115
- this.syncStreamClient = this.generateStreamingImplementation();
144
+ await this.portMutex.runExclusive(async () => {
145
+ // Keep track of the last connect options if we need to reconnect due to a lost client
116
146
  this.lastConnectOptions = options;
117
- this.syncStreamClient.registerListener({
118
- statusChanged: (status) => {
119
- this.updateAllStatuses(status.toJSON());
120
- }
121
- });
122
- await this.syncStreamClient.connect(options);
147
+ return this.connectionManager.connect(CONNECTOR_PLACEHOLDER, options);
123
148
  });
124
149
  }
125
150
  async disconnect() {
126
- await this.waitForReady();
127
- // This effectively queues connect and disconnect calls. Ensuring multiple tabs' requests are synchronized
128
- return getNavigatorLocks().request('shared-sync-connect', async () => {
129
- await this.syncStreamClient?.disconnect();
130
- await this.syncStreamClient?.dispose();
131
- this.syncStreamClient = null;
151
+ await this.portMutex.runExclusive(async () => {
152
+ await this.connectionManager.disconnect();
132
153
  });
133
154
  }
134
155
  /**
135
156
  * Adds a new client tab's message port to the list of connected ports
136
157
  */
137
- addPort(port) {
138
- const portProvider = {
139
- port,
140
- clientProvider: Comlink.wrap(port)
141
- };
142
- this.ports.push(portProvider);
143
- // Give the newly connected client the latest status
144
- const status = this.syncStreamClient?.syncStatus;
145
- if (status) {
146
- portProvider.clientProvider.statusChanged(status.toJSON());
147
- }
158
+ async addPort(port) {
159
+ await this.portMutex.runExclusive(() => {
160
+ const portProvider = {
161
+ port,
162
+ clientProvider: Comlink.wrap(port)
163
+ };
164
+ this.ports.push(portProvider);
165
+ // Give the newly connected client the latest status
166
+ const status = this.connectionManager.syncStreamImplementation?.syncStatus;
167
+ if (status) {
168
+ portProvider.clientProvider.statusChanged(status.toJSON());
169
+ }
170
+ });
148
171
  }
149
172
  /**
150
173
  * Removes a message port client from this manager's managed
151
174
  * clients.
152
175
  */
153
176
  async removePort(port) {
154
- const index = this.ports.findIndex((p) => p.port == port);
155
- if (index < 0) {
156
- this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
157
- return;
158
- }
159
- const trackedPort = this.ports[index];
160
- // Remove from the list of active ports
161
- this.ports.splice(index, 1);
162
- /**
163
- * The port might currently be in use. Any active functions might
164
- * not resolve. Abort them here.
165
- */
166
- [this.fetchCredentialsController, this.uploadDataController].forEach((abortController) => {
167
- if (abortController?.activePort.port == port) {
168
- abortController.controller.abort(new AbortOperation('Closing pending requests after client port is removed'));
177
+ return await this.portMutex.runExclusive(async () => {
178
+ const index = this.ports.findIndex((p) => p.port == port);
179
+ if (index < 0) {
180
+ this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
181
+ return;
169
182
  }
170
- });
171
- const shouldReconnect = !!this.syncStreamClient;
172
- if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
173
- if (shouldReconnect) {
174
- await this.disconnect();
183
+ const trackedPort = this.ports[index];
184
+ // Remove from the list of active ports
185
+ this.ports.splice(index, 1);
186
+ /**
187
+ * The port might currently be in use. Any active functions might
188
+ * not resolve. Abort them here.
189
+ */
190
+ [this.fetchCredentialsController, this.uploadDataController].forEach((abortController) => {
191
+ if (abortController?.activePort.port == port) {
192
+ abortController.controller.abort(new AbortOperation('Closing pending requests after client port is removed'));
193
+ }
194
+ });
195
+ const shouldReconnect = !!this.connectionManager.syncStreamImplementation && this.ports.length > 0;
196
+ if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
197
+ if (shouldReconnect) {
198
+ await this.connectionManager.disconnect();
199
+ }
200
+ // Clearing the adapter will result in a new one being opened in connect
201
+ this.dbAdapter = null;
202
+ if (shouldReconnect) {
203
+ await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions);
204
+ }
175
205
  }
176
- // Clearing the adapter will result in a new one being opened in connect
177
- this.dbAdapter = null;
178
- if (shouldReconnect) {
179
- await this.connect(this.lastConnectOptions);
206
+ if (trackedPort.db) {
207
+ await trackedPort.db.close();
180
208
  }
181
- }
182
- if (trackedPort.db) {
183
- trackedPort.db.close();
184
- }
185
- // Release proxy
186
- trackedPort.clientProvider[Comlink.releaseProxy]();
209
+ this.logger.debug(`Port ${port} removed from shared sync implementation.`);
210
+ // Release proxy
211
+ return () => trackedPort.clientProvider[Comlink.releaseProxy]();
212
+ });
187
213
  }
188
214
  triggerCrudUpload() {
189
- this.waitForReady().then(() => this.syncStreamClient?.triggerCrudUpload());
190
- }
191
- async obtainLock(lockOptions) {
192
- await this.waitForReady();
193
- return this.syncStreamClient.obtainLock(lockOptions);
215
+ this.waitForReady().then(() => this.connectionManager.syncStreamImplementation?.triggerCrudUpload());
194
216
  }
195
217
  async hasCompletedSync() {
196
- await this.waitForReady();
197
- return this.syncStreamClient.hasCompletedSync();
218
+ return this.withSyncImplementation(async (sync) => {
219
+ return sync.hasCompletedSync();
220
+ });
198
221
  }
199
222
  async getWriteCheckpoint() {
223
+ return this.withSyncImplementation(async (sync) => {
224
+ return sync.getWriteCheckpoint();
225
+ });
226
+ }
227
+ async withSyncImplementation(callback) {
200
228
  await this.waitForReady();
201
- return this.syncStreamClient.getWriteCheckpoint();
229
+ if (this.connectionManager.syncStreamImplementation) {
230
+ return callback(this.connectionManager.syncStreamImplementation);
231
+ }
232
+ const sync = await new Promise((resolve) => {
233
+ const dispose = this.connectionManager.registerListener({
234
+ syncStreamCreated: (sync) => {
235
+ resolve(sync);
236
+ dispose?.();
237
+ }
238
+ });
239
+ });
240
+ return callback(sync);
202
241
  }
203
242
  generateStreamingImplementation() {
204
243
  // This should only be called after initialization has completed
@@ -302,12 +341,12 @@ export class SharedSyncImplementation extends BaseObserver {
302
341
  * sync stream client and all tab client's sync status
303
342
  */
304
343
  _testUpdateAllStatuses(status) {
305
- if (!this.syncStreamClient) {
344
+ if (!this.connectionManager.syncStreamImplementation) {
306
345
  // This is just for testing purposes
307
- this.syncStreamClient = this.generateStreamingImplementation();
346
+ this.connectionManager.syncStreamImplementation = this.generateStreamingImplementation();
308
347
  }
309
348
  // Only assigning, don't call listeners for this test
310
- this.syncStreamClient.syncStatus = new SyncStatus(status);
349
+ this.connectionManager.syncStreamImplementation.syncStatus = new SyncStatus(status);
311
350
  this.updateAllStatuses(status);
312
351
  }
313
352
  }
@@ -1,22 +1,27 @@
1
- import * as Comlink from 'comlink';
2
- import { SharedSyncImplementation, SharedSyncClientEvent } from './SharedSyncImplementation';
3
1
  import { createBaseLogger } from '@powersync/common';
2
+ import * as Comlink from 'comlink';
3
+ import { SharedSyncClientEvent, SharedSyncImplementation } from './SharedSyncImplementation';
4
4
  const _self = self;
5
5
  const logger = createBaseLogger();
6
6
  logger.useDefaults();
7
7
  const sharedSyncImplementation = new SharedSyncImplementation();
8
- _self.onconnect = function (event) {
8
+ _self.onconnect = async function (event) {
9
9
  const port = event.ports[0];
10
10
  /**
11
11
  * Adds an extra listener which can remove this port
12
12
  * from the list of monitored ports.
13
13
  */
14
- port.addEventListener('message', (event) => {
14
+ port.addEventListener('message', async (event) => {
15
15
  const payload = event.data;
16
16
  if (payload?.event == SharedSyncClientEvent.CLOSE_CLIENT) {
17
- sharedSyncImplementation.removePort(port);
17
+ const release = await sharedSyncImplementation.removePort(port);
18
+ port.postMessage({
19
+ event: SharedSyncClientEvent.CLOSE_ACK,
20
+ data: {}
21
+ });
22
+ release?.();
18
23
  }
19
24
  });
25
+ await sharedSyncImplementation.addPort(port);
20
26
  Comlink.expose(sharedSyncImplementation, port);
21
- sharedSyncImplementation.addPort(port);
22
27
  };