@powersync/web 1.21.1 → 1.22.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.
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/web",
3
- "version": "1.21.1",
3
+ "version": "1.22.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.31.1"
64
+ "@powersync/common": "workspace:^1.32.0"
65
65
  },
66
66
  "dependencies": {
67
67
  "@powersync/common": "workspace:*",
@@ -1,4 +1,4 @@
1
- import { type BucketStorageAdapter, type PowerSyncBackendConnector, type PowerSyncCloseOptions, type PowerSyncConnectionOptions, type RequiredAdditionalConnectionOptions, AbstractPowerSyncDatabase, DBAdapter, PowerSyncDatabaseOptions, PowerSyncDatabaseOptionsWithDBAdapter, PowerSyncDatabaseOptionsWithOpenFactory, PowerSyncDatabaseOptionsWithSettings, StreamingSyncImplementation } from '@powersync/common';
1
+ import { type BucketStorageAdapter, type PowerSyncBackendConnector, type PowerSyncCloseOptions, type RequiredAdditionalConnectionOptions, AbstractPowerSyncDatabase, DBAdapter, PowerSyncDatabaseOptions, PowerSyncDatabaseOptionsWithDBAdapter, PowerSyncDatabaseOptionsWithOpenFactory, PowerSyncDatabaseOptionsWithSettings, StreamingSyncImplementation } from '@powersync/common';
2
2
  import { Mutex } from 'async-mutex';
3
3
  import { ResolvedWebSQLOpenOptions, WebSQLFlags } from './adapters/web-sql-flags';
4
4
  export interface WebPowerSyncFlags extends WebSQLFlags {
@@ -69,7 +69,6 @@ export declare class PowerSyncDatabase extends AbstractPowerSyncDatabase {
69
69
  * multiple tabs are not enabled.
70
70
  */
71
71
  close(options?: PowerSyncCloseOptions): Promise<void>;
72
- connect(connector: PowerSyncBackendConnector, options?: PowerSyncConnectionOptions): Promise<void>;
73
72
  protected generateBucketStorageAdapter(): BucketStorageAdapter;
74
73
  protected runExclusive<T>(cb: () => Promise<T>): Promise<any>;
75
74
  protected generateSyncStreamImplementation(connector: PowerSyncBackendConnector, options: RequiredAdditionalConnectionOptions): StreamingSyncImplementation;
@@ -81,21 +81,10 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
81
81
  disconnect: options.disconnect ?? !this.resolvedFlags.enableMultiTabs
82
82
  });
83
83
  }
84
- connect(connector, options) {
85
- /**
86
- * Using React strict mode might cause calls to connect to fire multiple times
87
- * Connect is wrapped inside a lock in order to prevent race conditions internally between multiple
88
- * connection attempts.
89
- */
90
- return this.runExclusive(() => {
91
- this.options.logger?.debug('Attempting to connect to PowerSync instance');
92
- return super.connect(connector, options);
93
- });
94
- }
95
84
  generateBucketStorageAdapter() {
96
85
  return new SqliteBucketStorage(this.database, AbstractPowerSyncDatabase.transactionMutex);
97
86
  }
98
- runExclusive(cb) {
87
+ async runExclusive(cb) {
99
88
  if (this.resolvedFlags.ssrMode) {
100
89
  return PowerSyncDatabase.SHARED_MUTEX.runExclusive(cb);
101
90
  }
@@ -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,9 +1,11 @@
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';
5
6
  import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
6
7
  /**
8
+ * @internal
7
9
  * Manual message events for shared sync clients
8
10
  */
9
11
  export declare enum SharedSyncClientEvent {
@@ -11,8 +13,12 @@ export declare enum SharedSyncClientEvent {
11
13
  * This client requests the shared sync manager should
12
14
  * close it's connection to the client.
13
15
  */
14
- CLOSE_CLIENT = "close-client"
16
+ CLOSE_CLIENT = "close-client",
17
+ CLOSE_ACK = "close-ack"
15
18
  }
19
+ /**
20
+ * @internal
21
+ */
16
22
  export type ManualSharedSyncPayload = {
17
23
  event: SharedSyncClientEvent;
18
24
  data: any;
@@ -51,7 +57,6 @@ export type RemoteOperationAbortController = {
51
57
  */
52
58
  export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImplementationListener> implements StreamingSyncImplementation {
53
59
  protected ports: WrappedSyncPort[];
54
- protected syncStreamClient: AbstractStreamingSyncImplementation | null;
55
60
  protected isInitialized: Promise<void>;
56
61
  protected statusListener?: () => void;
57
62
  protected fetchCredentialsController?: RemoteOperationAbortController;
@@ -60,41 +65,43 @@ export declare class SharedSyncImplementation extends BaseObserver<SharedSyncImp
60
65
  protected syncParams: SharedSyncInitOptions | null;
61
66
  protected logger: ILogger;
62
67
  protected lastConnectOptions: PowerSyncConnectionOptions | undefined;
68
+ protected portMutex: Mutex;
69
+ protected connectionManager: ConnectionManager;
63
70
  syncStatus: SyncStatus;
64
71
  broadCastLogger: ILogger;
65
72
  constructor();
66
- waitForStatus(status: SyncStatusOptions): Promise<void>;
67
- waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
68
73
  get lastSyncedAt(): Date | undefined;
69
74
  get isConnected(): boolean;
75
+ waitForStatus(status: SyncStatusOptions): Promise<void>;
76
+ waitUntilStatusMatches(predicate: (status: SyncStatus) => boolean): Promise<void>;
70
77
  waitForReady(): Promise<void>;
71
78
  setLogLevel(level: ILogLevel): void;
72
79
  /**
73
80
  * Configures the DBAdapter connection and a streaming sync client.
74
81
  */
75
82
  setParams(params: SharedSyncInitOptions): Promise<void>;
76
- dispose(): Promise<void | undefined>;
83
+ dispose(): Promise<void>;
77
84
  /**
78
85
  * Connects to the PowerSync backend instance.
79
86
  * Multiple tabs can safely call this in their initialization.
80
87
  * The connection will simply be reconnected whenever a new tab
81
88
  * connects.
82
89
  */
83
- connect(options?: PowerSyncConnectionOptions): Promise<any>;
84
- disconnect(): Promise<any>;
90
+ connect(options?: PowerSyncConnectionOptions): Promise<void>;
91
+ disconnect(): Promise<void>;
85
92
  /**
86
93
  * Adds a new client tab's message port to the list of connected ports
87
94
  */
88
- addPort(port: MessagePort): void;
95
+ addPort(port: MessagePort): Promise<void>;
89
96
  /**
90
97
  * Removes a message port client from this manager's managed
91
98
  * clients.
92
99
  */
93
- removePort(port: MessagePort): Promise<void>;
100
+ removePort(port: MessagePort): Promise<() => void>;
94
101
  triggerCrudUpload(): void;
95
- obtainLock<T>(lockOptions: LockOptions<T>): Promise<T>;
96
102
  hasCompletedSync(): Promise<boolean>;
97
103
  getWriteCheckpoint(): Promise<string>;
104
+ protected withSyncImplementation<T>(callback: (sync: StreamingSyncImplementation) => Promise<T>): Promise<T>;
98
105
  protected generateStreamingImplementation(): WebStreamingSyncImplementation;
99
106
  protected openInternalDB(): Promise<void>;
100
107
  /**
@@ -1,13 +1,13 @@
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
  /**
10
+ * @internal
11
11
  * Manual message events for shared sync clients
12
12
  */
13
13
  export var SharedSyncClientEvent;
@@ -17,14 +17,20 @@ export var SharedSyncClientEvent;
17
17
  * close it's connection to the client.
18
18
  */
19
19
  SharedSyncClientEvent["CLOSE_CLIENT"] = "close-client";
20
+ SharedSyncClientEvent["CLOSE_ACK"] = "close-ack";
20
21
  })(SharedSyncClientEvent || (SharedSyncClientEvent = {}));
22
+ /**
23
+ * HACK: The shared implementation wraps and provides its own
24
+ * PowerSyncBackendConnector when generating the streaming sync implementation.
25
+ * We provide this unused placeholder when connecting with the ConnectionManager.
26
+ */
27
+ const CONNECTOR_PLACEHOLDER = {};
21
28
  /**
22
29
  * @internal
23
30
  * Shared sync implementation which runs inside a shared webworker
24
31
  */
25
32
  export class SharedSyncImplementation extends BaseObserver {
26
33
  ports;
27
- syncStreamClient;
28
34
  isInitialized;
29
35
  statusListener;
30
36
  fetchCredentialsController;
@@ -33,6 +39,8 @@ export class SharedSyncImplementation extends BaseObserver {
33
39
  syncParams;
34
40
  logger;
35
41
  lastConnectOptions;
42
+ portMutex;
43
+ connectionManager;
36
44
  syncStatus;
37
45
  broadCastLogger;
38
46
  constructor() {
@@ -40,9 +48,9 @@ export class SharedSyncImplementation extends BaseObserver {
40
48
  this.ports = [];
41
49
  this.dbAdapter = null;
42
50
  this.syncParams = null;
43
- this.syncStreamClient = null;
44
51
  this.logger = createLogger('shared-sync');
45
52
  this.lastConnectOptions = undefined;
53
+ this.portMutex = new Mutex();
46
54
  this.isInitialized = new Promise((resolve) => {
47
55
  const callback = this.registerListener({
48
56
  initialized: () => {
@@ -53,20 +61,43 @@ export class SharedSyncImplementation extends BaseObserver {
53
61
  });
54
62
  this.syncStatus = new SyncStatus({});
55
63
  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);
64
+ this.connectionManager = new ConnectionManager({
65
+ createSyncImplementation: async () => {
66
+ return this.portMutex.runExclusive(async () => {
67
+ await this.waitForReady();
68
+ if (!this.dbAdapter) {
69
+ await this.openInternalDB();
70
+ }
71
+ const sync = this.generateStreamingImplementation();
72
+ const onDispose = sync.registerListener({
73
+ statusChanged: (status) => {
74
+ this.updateAllStatuses(status.toJSON());
75
+ }
76
+ });
77
+ return {
78
+ sync,
79
+ onDispose
80
+ };
81
+ });
82
+ },
83
+ logger: this.logger
84
+ });
64
85
  }
65
86
  get lastSyncedAt() {
66
- return this.syncStreamClient?.lastSyncedAt;
87
+ return this.connectionManager.syncStreamImplementation?.lastSyncedAt;
67
88
  }
68
89
  get isConnected() {
69
- return this.syncStreamClient?.isConnected ?? false;
90
+ return this.connectionManager.syncStreamImplementation?.isConnected ?? false;
91
+ }
92
+ async waitForStatus(status) {
93
+ return this.withSyncImplementation(async (sync) => {
94
+ return sync.waitForStatus(status);
95
+ });
96
+ }
97
+ async waitUntilStatusMatches(predicate) {
98
+ return this.withSyncImplementation(async (sync) => {
99
+ return sync.waitUntilStatusMatches(predicate);
100
+ });
70
101
  }
71
102
  async waitForReady() {
72
103
  return this.isInitialized;
@@ -79,25 +110,34 @@ export class SharedSyncImplementation extends BaseObserver {
79
110
  * Configures the DBAdapter connection and a streaming sync client.
80
111
  */
81
112
  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?.());
113
+ await this.portMutex.runExclusive(async () => {
114
+ if (this.syncParams) {
115
+ // Cannot modify already existing sync implementation params
116
+ // But we can ask for a DB adapter, if required, at this point.
117
+ if (!this.dbAdapter) {
118
+ await this.openInternalDB();
119
+ }
120
+ return;
121
+ }
122
+ // First time setting params
123
+ this.syncParams = params;
124
+ if (params.streamOptions?.flags?.broadcastLogs) {
125
+ this.logger = this.broadCastLogger;
126
+ }
127
+ self.onerror = (event) => {
128
+ // Share any uncaught events on the broadcast logger
129
+ this.logger.error('Uncaught exception in PowerSync shared sync worker', event);
130
+ };
131
+ if (!this.dbAdapter) {
132
+ await this.openInternalDB();
133
+ }
134
+ this.iterateListeners((l) => l.initialized?.());
135
+ });
96
136
  }
97
137
  async dispose() {
98
138
  await this.waitForReady();
99
139
  this.statusListener?.();
100
- return this.syncStreamClient?.dispose();
140
+ return this.connectionManager.close();
101
141
  }
102
142
  /**
103
143
  * Connects to the PowerSync backend instance.
@@ -106,99 +146,110 @@ export class SharedSyncImplementation extends BaseObserver {
106
146
  * connects.
107
147
  */
108
148
  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();
116
- this.lastConnectOptions = options;
117
- this.syncStreamClient.registerListener({
118
- statusChanged: (status) => {
119
- this.updateAllStatuses(status.toJSON());
120
- }
121
- });
122
- await this.syncStreamClient.connect(options);
123
- });
149
+ this.lastConnectOptions = options;
150
+ return this.connectionManager.connect(CONNECTOR_PLACEHOLDER, options);
124
151
  }
125
152
  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;
132
- });
153
+ return this.connectionManager.disconnect();
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
+ // Remove the port within a mutex context.
178
+ // Warns if the port is not found. This should not happen in practice.
179
+ // We return early if the port is not found.
180
+ const { trackedPort, shouldReconnect } = await this.portMutex.runExclusive(async () => {
181
+ const index = this.ports.findIndex((p) => p.port == port);
182
+ if (index < 0) {
183
+ this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`);
184
+ return {};
169
185
  }
186
+ const trackedPort = this.ports[index];
187
+ // Remove from the list of active ports
188
+ this.ports.splice(index, 1);
189
+ /**
190
+ * The port might currently be in use. Any active functions might
191
+ * not resolve. Abort them here.
192
+ */
193
+ [this.fetchCredentialsController, this.uploadDataController].forEach((abortController) => {
194
+ if (abortController?.activePort.port == port) {
195
+ abortController.controller.abort(new AbortOperation('Closing pending requests after client port is removed'));
196
+ }
197
+ });
198
+ const shouldReconnect = !!this.connectionManager.syncStreamImplementation && this.ports.length > 0;
199
+ return {
200
+ shouldReconnect,
201
+ trackedPort
202
+ };
170
203
  });
171
- const shouldReconnect = !!this.syncStreamClient;
204
+ if (!trackedPort) {
205
+ // We could not find the port to remove
206
+ return () => { };
207
+ }
172
208
  if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
173
209
  if (shouldReconnect) {
174
- await this.disconnect();
210
+ await this.connectionManager.disconnect();
175
211
  }
176
212
  // Clearing the adapter will result in a new one being opened in connect
177
213
  this.dbAdapter = null;
178
214
  if (shouldReconnect) {
179
- await this.connect(this.lastConnectOptions);
215
+ await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions);
180
216
  }
181
217
  }
182
218
  if (trackedPort.db) {
183
- trackedPort.db.close();
219
+ await trackedPort.db.close();
184
220
  }
185
221
  // Release proxy
186
- trackedPort.clientProvider[Comlink.releaseProxy]();
222
+ return () => trackedPort.clientProvider[Comlink.releaseProxy]();
187
223
  }
188
224
  triggerCrudUpload() {
189
- this.waitForReady().then(() => this.syncStreamClient?.triggerCrudUpload());
190
- }
191
- async obtainLock(lockOptions) {
192
- await this.waitForReady();
193
- return this.syncStreamClient.obtainLock(lockOptions);
225
+ this.withSyncImplementation(async (sync) => {
226
+ sync.triggerCrudUpload();
227
+ });
194
228
  }
195
229
  async hasCompletedSync() {
196
- await this.waitForReady();
197
- return this.syncStreamClient.hasCompletedSync();
230
+ return this.withSyncImplementation(async (sync) => {
231
+ return sync.hasCompletedSync();
232
+ });
198
233
  }
199
234
  async getWriteCheckpoint() {
235
+ return this.withSyncImplementation(async (sync) => {
236
+ return sync.getWriteCheckpoint();
237
+ });
238
+ }
239
+ async withSyncImplementation(callback) {
200
240
  await this.waitForReady();
201
- return this.syncStreamClient.getWriteCheckpoint();
241
+ if (this.connectionManager.syncStreamImplementation) {
242
+ return callback(this.connectionManager.syncStreamImplementation);
243
+ }
244
+ const sync = await new Promise((resolve) => {
245
+ const dispose = this.connectionManager.registerListener({
246
+ syncStreamCreated: (sync) => {
247
+ resolve(sync);
248
+ dispose?.();
249
+ }
250
+ });
251
+ });
252
+ return callback(sync);
202
253
  }
203
254
  generateStreamingImplementation() {
204
255
  // This should only be called after initialization has completed
@@ -301,13 +352,13 @@ export class SharedSyncImplementation extends BaseObserver {
301
352
  * A function only used for unit tests which updates the internal
302
353
  * sync stream client and all tab client's sync status
303
354
  */
304
- _testUpdateAllStatuses(status) {
305
- if (!this.syncStreamClient) {
355
+ async _testUpdateAllStatuses(status) {
356
+ if (!this.connectionManager.syncStreamImplementation) {
306
357
  // This is just for testing purposes
307
- this.syncStreamClient = this.generateStreamingImplementation();
358
+ this.connectionManager.syncStreamImplementation = this.generateStreamingImplementation();
308
359
  }
309
360
  // Only assigning, don't call listeners for this test
310
- this.syncStreamClient.syncStatus = new SyncStatus(status);
361
+ this.connectionManager.syncStreamImplementation.syncStatus = new SyncStatus(status);
311
362
  this.updateAllStatuses(status);
312
363
  }
313
364
  }
@@ -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
  };