@powersync/web 1.28.1 → 1.29.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.
Files changed (35) hide show
  1. package/dist/index.umd.js +71 -9
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +8414 -4438
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +12887 -10914
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/lib/package.json +2 -2
  8. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +15 -0
  9. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +2 -1
  10. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +17 -2
  11. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +3 -0
  12. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +11 -0
  13. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +22 -0
  14. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +32 -0
  15. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +42 -0
  16. package/lib/src/worker/db/SharedWASQLiteConnection.js +90 -0
  17. package/lib/src/worker/db/WASQLiteDB.worker.js +22 -39
  18. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +9 -0
  19. package/lib/src/worker/db/WorkerWASQLiteConnection.js +12 -0
  20. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +2 -2
  21. package/lib/src/worker/sync/SharedSyncImplementation.js +7 -4
  22. package/lib/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +3 -3
  24. package/src/db/adapters/AsyncDatabaseConnection.ts +15 -0
  25. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +29 -10
  26. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +14 -0
  27. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +45 -0
  28. package/src/worker/db/SharedWASQLiteConnection.ts +131 -0
  29. package/src/worker/db/WASQLiteDB.worker.ts +25 -54
  30. package/src/worker/db/WorkerWASQLiteConnection.ts +14 -0
  31. package/src/worker/sync/SharedSyncImplementation.ts +15 -12
  32. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-81d3460.index.umd.js +0 -355
  33. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-81d3460.index.umd.js.map +0 -1
  34. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-81d3461.index.umd.js +0 -355
  35. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-81d3461.index.umd.js.map +0 -1
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/web",
3
- "version": "1.28.1",
3
+ "version": "1.29.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",
@@ -62,7 +62,7 @@
62
62
  "license": "Apache-2.0",
63
63
  "peerDependencies": {
64
64
  "@journeyapps/wa-sqlite": "^1.3.2",
65
- "@powersync/common": "workspace:^1.41.1"
65
+ "@powersync/common": "workspace:^1.43.0"
66
66
  },
67
67
  "dependencies": {
68
68
  "@powersync/common": "workspace:*",
@@ -22,6 +22,21 @@ export type OnTableChangeCallback = (event: BatchedUpdateNotification) => void;
22
22
  export interface AsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> {
23
23
  init(): Promise<void>;
24
24
  close(): Promise<void>;
25
+ /**
26
+ * Marks the connection as in-use by a certain actor.
27
+ * @returns A hold ID which can be used to release the hold.
28
+ */
29
+ markHold(): Promise<string>;
30
+ /**
31
+ * Releases a hold on the connection.
32
+ * @param holdId The hold ID to release.
33
+ */
34
+ releaseHold(holdId: string): Promise<void>;
35
+ /**
36
+ * Checks if the database connection is in autocommit mode.
37
+ * @returns true if in autocommit mode, false if in a transaction
38
+ */
39
+ isAutoCommit(): Promise<boolean>;
25
40
  execute(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
26
41
  executeRaw(sql: string, params?: any[]): Promise<any[][]>;
27
42
  executeBatch(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
@@ -1,4 +1,4 @@
1
- import { type ILogger, BaseObserver, DBAdapterListener, DBLockOptions, LockContext, QueryResult, Transaction } from '@powersync/common';
1
+ import { BaseObserver, DBAdapterListener, DBLockOptions, LockContext, QueryResult, Transaction, type ILogger } from '@powersync/common';
2
2
  import { AsyncDatabaseConnection } from './AsyncDatabaseConnection';
3
3
  import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
4
4
  import { ResolvedWebSQLOpenOptions } from './web-sql-flags';
@@ -30,6 +30,7 @@ export declare class LockedAsyncDatabaseAdapter extends BaseObserver<LockedAsync
30
30
  protected _disposeTableChangeListener: (() => void) | null;
31
31
  private _config;
32
32
  protected pendingAbortControllers: Set<AbortController>;
33
+ protected requiresHolds: boolean | null;
33
34
  closing: boolean;
34
35
  closed: boolean;
35
36
  constructor(options: LockedAsyncDatabaseAdapterOptions);
@@ -1,6 +1,7 @@
1
1
  import { BaseObserver, createLogger } from '@powersync/common';
2
2
  import { getNavigatorLocks } from '../..//shared/navigator';
3
3
  import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
4
+ import { WASQLiteVFS } from './wa-sqlite/WASQLiteConnection';
4
5
  /**
5
6
  * @internal
6
7
  * Wraps a {@link AsyncDatabaseConnection} and provides exclusive locking functions in
@@ -17,6 +18,7 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
17
18
  _disposeTableChangeListener = null;
18
19
  _config = null;
19
20
  pendingAbortControllers;
21
+ requiresHolds;
20
22
  closing;
21
23
  closed;
22
24
  constructor(options) {
@@ -27,6 +29,7 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
27
29
  this.pendingAbortControllers = new Set();
28
30
  this.closed = false;
29
31
  this.closing = false;
32
+ this.requiresHolds = null;
30
33
  // Set the name if provided. We can query for the name if not available yet
31
34
  this.debugMode = options.debugMode ?? false;
32
35
  if (this.debugMode) {
@@ -71,6 +74,10 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
71
74
  this._config = await this._db.getConfig();
72
75
  await this.registerOnChangeListener(this._db);
73
76
  this.iterateListeners((cb) => cb.initialized?.());
77
+ /**
78
+ * This is only required for the long-lived shared IndexedDB connections.
79
+ */
80
+ this.requiresHolds = this._config.vfs == WASQLiteVFS.IDBBatchAtomicVFS;
74
81
  }
75
82
  getConfiguration() {
76
83
  if (!this._config) {
@@ -160,12 +167,20 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
160
167
  this.pendingAbortControllers.delete(abortController);
161
168
  }, timeoutMs)
162
169
  : null;
163
- return getNavigatorLocks().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, () => {
170
+ return getNavigatorLocks().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, async () => {
164
171
  this.pendingAbortControllers.delete(abortController);
165
172
  if (timoutId) {
166
173
  clearTimeout(timoutId);
167
174
  }
168
- return callback();
175
+ const holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
176
+ try {
177
+ return await callback();
178
+ }
179
+ finally {
180
+ if (holdId) {
181
+ await this.baseDB.releaseHold(holdId);
182
+ }
183
+ }
169
184
  });
170
185
  }
171
186
  async readTransaction(fn, options) {
@@ -34,6 +34,9 @@ export declare class WorkerWrappedAsyncDatabaseConnection<Config extends Resolve
34
34
  * throw on all outstanding promises and forbid new calls.
35
35
  */
36
36
  markRemoteClosed(): void;
37
+ markHold(): Promise<string>;
38
+ releaseHold(holdId: string): Promise<void>;
39
+ isAutoCommit(): Promise<boolean>;
37
40
  private withRemote;
38
41
  /**
39
42
  * Get a MessagePort which can be used to share the internals of this connection.
@@ -31,12 +31,23 @@ export class WorkerWrappedAsyncDatabaseConnection {
31
31
  // set.
32
32
  this.notifyRemoteClosed.abort();
33
33
  }
34
+ markHold() {
35
+ return this.withRemote(() => this.baseConnection.markHold());
36
+ }
37
+ releaseHold(holdId) {
38
+ return this.withRemote(() => this.baseConnection.releaseHold(holdId));
39
+ }
40
+ isAutoCommit() {
41
+ return this.withRemote(() => this.baseConnection.isAutoCommit());
42
+ }
34
43
  withRemote(workerPromise) {
35
44
  const controller = this.notifyRemoteClosed;
36
45
  if (controller) {
37
46
  return new Promise((resolve, reject) => {
38
47
  if (controller.signal.aborted) {
39
48
  reject(new Error('Called operation on closed remote'));
49
+ // Don't run the operation if we're going to reject
50
+ return;
40
51
  }
41
52
  function handleAbort() {
42
53
  reject(new Error('Remote peer closed with request in flight'));
@@ -23,6 +23,14 @@ export type WASQLiteBroadCastTableUpdateEvent = {
23
23
  */
24
24
  export type WASQLiteConnectionListener = {
25
25
  tablesUpdated: (event: BatchedUpdateNotification) => void;
26
+ /**
27
+ * Triggered when an active hold is overwritten by a new hold.
28
+ * This is most likely to happen when a shared connection has been closed
29
+ * without releasing the hold.
30
+ * This listener can be used to cleanup any resources associated with the previous hold.
31
+ * @param holdId - The id of the hold that has been overwritten.
32
+ */
33
+ holdOverwritten: (holdId: string) => Promise<void>;
26
34
  };
27
35
  /**
28
36
  * @internal
@@ -94,9 +102,23 @@ export declare class WASqliteConnection extends BaseObserver<WASQLiteConnectionL
94
102
  * notification loops.
95
103
  */
96
104
  protected connectionId: number;
105
+ protected _holdCounter: number;
106
+ protected _holdId: string | null;
97
107
  constructor(options: ResolvedWASQLiteOpenFactoryOptions);
108
+ /**
109
+ * Gets the id for the current hold.
110
+ * This can be used to check for invalid states.
111
+ */
112
+ get currentHoldId(): string | null;
98
113
  protected get sqliteAPI(): SQLiteAPI;
99
114
  protected get dbP(): number;
115
+ /**
116
+ * Checks if the database connection is in autocommit mode.
117
+ * @returns true if in autocommit mode, false if in a transaction
118
+ */
119
+ isAutoCommit(): Promise<boolean>;
120
+ markHold(): Promise<string>;
121
+ releaseHold(holdId: string): Promise<void>;
100
122
  protected openDB(): Promise<number>;
101
123
  protected executeEncryptionPragma(): Promise<void>;
102
124
  protected openSQLiteAPI(): Promise<SQLiteAPI>;
@@ -107,6 +107,8 @@ export class WASqliteConnection extends BaseObserver {
107
107
  * notification loops.
108
108
  */
109
109
  connectionId;
110
+ _holdCounter;
111
+ _holdId;
110
112
  constructor(options) {
111
113
  super();
112
114
  this.options = options;
@@ -116,6 +118,15 @@ export class WASqliteConnection extends BaseObserver {
116
118
  this.connectionId = new Date().valueOf() + Math.random();
117
119
  this.statementMutex = new Mutex();
118
120
  this._moduleFactory = DEFAULT_MODULE_FACTORIES[this.options.vfs];
121
+ this._holdCounter = 0;
122
+ this._holdId = null;
123
+ }
124
+ /**
125
+ * Gets the id for the current hold.
126
+ * This can be used to check for invalid states.
127
+ */
128
+ get currentHoldId() {
129
+ return this._holdId;
119
130
  }
120
131
  get sqliteAPI() {
121
132
  if (!this._sqliteAPI) {
@@ -129,6 +140,27 @@ export class WASqliteConnection extends BaseObserver {
129
140
  }
130
141
  return this._dbP;
131
142
  }
143
+ /**
144
+ * Checks if the database connection is in autocommit mode.
145
+ * @returns true if in autocommit mode, false if in a transaction
146
+ */
147
+ async isAutoCommit() {
148
+ return this.sqliteAPI.get_autocommit(this.dbP) != 0;
149
+ }
150
+ async markHold() {
151
+ const previousHoldId = this._holdId;
152
+ this._holdId = `${++this._holdCounter}`;
153
+ if (previousHoldId) {
154
+ await this.iterateAsyncListeners(async (cb) => cb.holdOverwritten?.(previousHoldId));
155
+ }
156
+ return this._holdId;
157
+ }
158
+ async releaseHold(holdId) {
159
+ if (holdId != this._holdId) {
160
+ throw new Error(`Invalid hold state, expected ${this._holdId} but got ${holdId}`);
161
+ }
162
+ this._holdId = null;
163
+ }
132
164
  async openDB() {
133
165
  this._dbP = await this.sqliteAPI.open_v2(this.options.dbFilename);
134
166
  return this._dbP;
@@ -0,0 +1,42 @@
1
+ import { ILogger } from '@powersync/common';
2
+ import { AsyncDatabaseConnection, OnTableChangeCallback, ProxiedQueryResult } from '../../db/adapters/AsyncDatabaseConnection';
3
+ import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
4
+ /**
5
+ * Keeps track of open DB connections and the clients which
6
+ * are using it.
7
+ */
8
+ export type SharedDBWorkerConnection = {
9
+ clientIds: Set<number>;
10
+ db: AsyncDatabaseConnection;
11
+ };
12
+ export type SharedWASQLiteConnectionOptions = {
13
+ dbMap: Map<string, SharedDBWorkerConnection>;
14
+ dbFilename: string;
15
+ clientId: number;
16
+ logger: ILogger;
17
+ };
18
+ export declare class SharedWASQLiteConnection implements AsyncDatabaseConnection {
19
+ protected options: SharedWASQLiteConnectionOptions;
20
+ protected isClosing: boolean;
21
+ protected activeHoldId: string | null;
22
+ constructor(options: SharedWASQLiteConnectionOptions);
23
+ protected get logger(): ILogger;
24
+ protected get dbEntry(): SharedDBWorkerConnection;
25
+ protected get connection(): AsyncDatabaseConnection<ResolvedWebSQLOpenOptions>;
26
+ protected get clientIds(): Set<number>;
27
+ init(): Promise<void>;
28
+ markHold(): Promise<string>;
29
+ releaseHold(id: string): Promise<void>;
30
+ isAutoCommit(): Promise<boolean>;
31
+ /**
32
+ * Handles closing of a shared connection.
33
+ * The connection is only closed if there are no active clients using it.
34
+ */
35
+ close(): Promise<void>;
36
+ protected withClosing<T>(action: () => Promise<T>): Promise<T>;
37
+ execute(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
38
+ executeRaw(sql: string, params?: any[]): Promise<any[][]>;
39
+ executeBatch(sql: string, params?: any[] | undefined): Promise<ProxiedQueryResult>;
40
+ registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void>;
41
+ getConfig(): Promise<ResolvedWebSQLOpenOptions>;
42
+ }
@@ -0,0 +1,90 @@
1
+ export class SharedWASQLiteConnection {
2
+ options;
3
+ isClosing;
4
+ // Keeps track if this current hold if the shared connection has a hold
5
+ activeHoldId;
6
+ constructor(options) {
7
+ this.options = options;
8
+ // Add this client ID to the set of known clients
9
+ this.clientIds.add(options.clientId);
10
+ this.isClosing = false;
11
+ this.activeHoldId = null;
12
+ }
13
+ get logger() {
14
+ return this.options.logger;
15
+ }
16
+ get dbEntry() {
17
+ return this.options.dbMap.get(this.options.dbFilename);
18
+ }
19
+ get connection() {
20
+ return this.dbEntry.db;
21
+ }
22
+ get clientIds() {
23
+ return this.dbEntry.clientIds;
24
+ }
25
+ async init() {
26
+ // No-op since the connection is already initialized when it was created
27
+ }
28
+ async markHold() {
29
+ this.activeHoldId = await this.connection.markHold();
30
+ return this.activeHoldId;
31
+ }
32
+ async releaseHold(id) {
33
+ try {
34
+ await this.connection.releaseHold(id);
35
+ }
36
+ finally {
37
+ this.activeHoldId = null;
38
+ }
39
+ }
40
+ async isAutoCommit() {
41
+ return this.connection.isAutoCommit();
42
+ }
43
+ /**
44
+ * Handles closing of a shared connection.
45
+ * The connection is only closed if there are no active clients using it.
46
+ */
47
+ async close() {
48
+ // This prevents further statements on this connection from being executed
49
+ this.isClosing = true;
50
+ const { clientIds, logger } = this;
51
+ const { clientId, dbFilename, dbMap } = this.options;
52
+ logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
53
+ clientIds.delete(clientId);
54
+ if (this.activeHoldId) {
55
+ // We can't cleanup here since we're not in a lock context.
56
+ // The cleanup will occur once a new hold is acquired.
57
+ this.logger.info(`Hold ${this.activeHoldId} was still active when the connection was closed. Cleanup will occur once a new hold is acquired.`);
58
+ }
59
+ if (clientIds.size == 0) {
60
+ logger.debug(`Closing connection to ${this.options}.`);
61
+ const connection = this.connection;
62
+ dbMap.delete(dbFilename);
63
+ await connection.close();
64
+ return;
65
+ }
66
+ logger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`);
67
+ return;
68
+ }
69
+ async withClosing(action) {
70
+ if (this.isClosing) {
71
+ throw new Error('Connection is closing');
72
+ }
73
+ return action();
74
+ }
75
+ async execute(sql, params) {
76
+ return this.withClosing(() => this.connection.execute(sql, params));
77
+ }
78
+ async executeRaw(sql, params) {
79
+ return this.withClosing(() => this.connection.executeRaw(sql, params));
80
+ }
81
+ executeBatch(sql, params) {
82
+ return this.withClosing(() => this.connection.executeBatch(sql, params));
83
+ }
84
+ registerOnTableChange(callback) {
85
+ return this.connection.registerOnTableChange(callback);
86
+ }
87
+ getConfig() {
88
+ return this.connection.getConfig();
89
+ }
90
+ }
@@ -4,29 +4,15 @@
4
4
  import '@journeyapps/wa-sqlite';
5
5
  import { createBaseLogger, createLogger } from '@powersync/common';
6
6
  import * as Comlink from 'comlink';
7
- import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
8
7
  import { getNavigatorLocks } from '../../shared/navigator';
8
+ import { SharedWASQLiteConnection } from './SharedWASQLiteConnection';
9
+ import { WorkerWASQLiteConnection } from './WorkerWASQLiteConnection';
9
10
  const baseLogger = createBaseLogger();
10
11
  baseLogger.useDefaults();
11
12
  const logger = createLogger('db-worker');
12
13
  const DBMap = new Map();
13
14
  const OPEN_DB_LOCK = 'open-wasqlite-db';
14
15
  let nextClientId = 1;
15
- const openWorkerConnection = async (options) => {
16
- const connection = new WASqliteConnection(options);
17
- return {
18
- init: Comlink.proxy(() => connection.init()),
19
- getConfig: Comlink.proxy(() => connection.getConfig()),
20
- close: Comlink.proxy(() => connection.close()),
21
- execute: Comlink.proxy(async (sql, params) => connection.execute(sql, params)),
22
- executeRaw: Comlink.proxy(async (sql, params) => connection.executeRaw(sql, params)),
23
- executeBatch: Comlink.proxy(async (sql, params) => connection.executeBatch(sql, params)),
24
- registerOnTableChange: Comlink.proxy(async (callback) => {
25
- // Proxy the callback remove function
26
- return Comlink.proxy(await connection.registerOnTableChange(callback));
27
- })
28
- };
29
- };
30
16
  const openDBShared = async (options) => {
31
17
  // Prevent multiple simultaneous opens from causing race conditions
32
18
  return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
@@ -35,35 +21,32 @@ const openDBShared = async (options) => {
35
21
  logger.setLevel(logLevel);
36
22
  if (!DBMap.has(dbFilename)) {
37
23
  const clientIds = new Set();
38
- const connection = await openWorkerConnection(options);
24
+ // This format returns proxy objects for function callbacks
25
+ const connection = new WorkerWASQLiteConnection(options);
39
26
  await connection.init();
27
+ connection.registerListener({
28
+ holdOverwritten: async () => {
29
+ /**
30
+ * The previous hold has been overwritten, without being released.
31
+ * we need to cleanup any resources associated with it.
32
+ * We can perform a rollback to release any potential transactions that were started.
33
+ */
34
+ await connection.execute('ROLLBACK').catch(() => { });
35
+ }
36
+ });
40
37
  DBMap.set(dbFilename, {
41
38
  clientIds,
42
39
  db: connection
43
40
  });
44
41
  }
45
- const dbEntry = DBMap.get(dbFilename);
46
- dbEntry.clientIds.add(clientId);
47
- const { db } = dbEntry;
48
- const wrappedConnection = {
49
- ...db,
50
- init: Comlink.proxy(async () => {
51
- // the init has been done automatically
52
- }),
53
- close: Comlink.proxy(async () => {
54
- const { clientIds } = dbEntry;
55
- logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
56
- clientIds.delete(clientId);
57
- if (clientIds.size == 0) {
58
- logger.debug(`Closing connection to ${dbFilename}.`);
59
- DBMap.delete(dbFilename);
60
- return db.close?.();
61
- }
62
- logger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`);
63
- return;
64
- })
65
- };
66
- return Comlink.proxy(wrappedConnection);
42
+ // Associates this clientId with the shared connection entry
43
+ const sharedConnection = new SharedWASQLiteConnection({
44
+ dbMap: DBMap,
45
+ dbFilename,
46
+ clientId,
47
+ logger
48
+ });
49
+ return Comlink.proxy(sharedConnection);
67
50
  });
68
51
  };
69
52
  // Check if we're in a SharedWorker context
@@ -0,0 +1,9 @@
1
+ import { OnTableChangeCallback } from '../../db/adapters/AsyncDatabaseConnection';
2
+ import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
3
+ /**
4
+ * A Small proxy wrapper around the WASqliteConnection.
5
+ * This ensures that certain return types are properly proxied.
6
+ */
7
+ export declare class WorkerWASQLiteConnection extends WASqliteConnection {
8
+ registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void>;
9
+ }
@@ -0,0 +1,12 @@
1
+ import * as Comlink from 'comlink';
2
+ import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
3
+ /**
4
+ * A Small proxy wrapper around the WASqliteConnection.
5
+ * This ensures that certain return types are properly proxied.
6
+ */
7
+ export class WorkerWASQLiteConnection extends WASqliteConnection {
8
+ async registerOnTableChange(callback) {
9
+ // Proxy the callback remove function
10
+ return Comlink.proxy(await super.registerOnTableChange(callback));
11
+ }
12
+ }
@@ -1,4 +1,4 @@
1
- import { type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions, BaseObserver, ConnectionManager, DBAdapter, SubscribedStream, SyncStatus } from '@powersync/common';
1
+ import { BaseObserver, ConnectionManager, DBAdapter, SubscribedStream, SyncStatus, type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions } from '@powersync/common';
2
2
  import { Mutex } from 'async-mutex';
3
3
  import * as Comlink from 'comlink';
4
4
  import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from '../../db/sync/WebStreamingSyncImplementation';
@@ -44,7 +44,7 @@ export type WrappedSyncPort = {
44
44
  clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
45
45
  db?: DBAdapter;
46
46
  currentSubscriptions: SubscribedStream[];
47
- closeListeners: (() => void)[];
47
+ closeListeners: (() => void | Promise<void>)[];
48
48
  };
49
49
  /**
50
50
  * @internal
@@ -228,11 +228,14 @@ export class SharedSyncImplementation extends BaseObserver {
228
228
  return () => { };
229
229
  }
230
230
  for (const closeListener of trackedPort.closeListeners) {
231
- closeListener();
231
+ await closeListener();
232
232
  }
233
233
  if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
234
234
  // Unconditionally close the connection because the database it's writing to has just been closed.
235
- await this.connectionManager.disconnect();
235
+ // The connection has been closed previously, this might throw. We should be able to ignore it.
236
+ await this.connectionManager
237
+ .disconnect()
238
+ .catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
236
239
  // Clearing the adapter will result in a new one being opened in connect
237
240
  this.dbAdapter = null;
238
241
  if (shouldReconnect) {
@@ -361,9 +364,9 @@ export class SharedSyncImplementation extends BaseObserver {
361
364
  // that and ensure pending requests are aborted when the tab is closed.
362
365
  remoteCanCloseUnexpectedly: true
363
366
  });
364
- lastClient.closeListeners.push(() => {
367
+ lastClient.closeListeners.push(async () => {
365
368
  this.logger.info('Aborting open connection because associated tab closed.');
366
- wrapped.close();
369
+ await wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
367
370
  wrapped.markRemoteClosed();
368
371
  });
369
372
  return wrapped;