@powersync/web 0.0.0-dev-20251119142638 → 0.0.0-dev-20251120085122

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 (27) hide show
  1. package/dist/index.umd.js +21 -6
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +14 -6
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +28 -34
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +13 -0
  8. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +1 -0
  9. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +7 -2
  10. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +5 -0
  11. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +7 -0
  12. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +4 -3
  13. package/lib/src/worker/db/SharedWASQLiteConnection.js +17 -17
  14. package/lib/src/worker/db/WASQLiteDB.worker.js +2 -2
  15. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +3 -3
  16. package/lib/src/worker/db/WorkerWASQLiteConnection.js +2 -14
  17. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +2 -2
  18. package/lib/src/worker/sync/SharedSyncImplementation.js +7 -4
  19. package/lib/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +1 -1
  21. package/src/db/adapters/AsyncDatabaseConnection.ts +13 -0
  22. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +8 -2
  23. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +8 -0
  24. package/src/worker/db/SharedWASQLiteConnection.ts +23 -20
  25. package/src/worker/db/WASQLiteDB.worker.ts +2 -2
  26. package/src/worker/db/WorkerWASQLiteConnection.ts +3 -18
  27. package/src/worker/sync/SharedSyncImplementation.ts +15 -12
@@ -22,8 +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
+ */
25
29
  markHold(): Promise<string>;
30
+ /**
31
+ * Releases a hold on the connection.
32
+ * @param holdId The hold ID to release.
33
+ */
26
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>;
27
40
  execute(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
28
41
  executeRaw(sql: string, params?: any[]): Promise<any[][]>;
29
42
  executeBatch(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
@@ -36,6 +36,7 @@ export declare class WorkerWrappedAsyncDatabaseConnection<Config extends Resolve
36
36
  markRemoteClosed(): void;
37
37
  markHold(): Promise<string>;
38
38
  releaseHold(holdId: string): Promise<void>;
39
+ isAutoCommit(): Promise<boolean>;
39
40
  private withRemote;
40
41
  /**
41
42
  * Get a MessagePort which can be used to share the internals of this connection.
@@ -32,10 +32,13 @@ export class WorkerWrappedAsyncDatabaseConnection {
32
32
  this.notifyRemoteClosed.abort();
33
33
  }
34
34
  markHold() {
35
- return this.baseConnection.markHold();
35
+ return this.withRemote(() => this.baseConnection.markHold());
36
36
  }
37
37
  releaseHold(holdId) {
38
- return this.baseConnection.releaseHold(holdId);
38
+ return this.withRemote(() => this.baseConnection.releaseHold(holdId));
39
+ }
40
+ isAutoCommit() {
41
+ return this.baseConnection.isAutoCommit();
39
42
  }
40
43
  withRemote(workerPromise) {
41
44
  const controller = this.notifyRemoteClosed;
@@ -43,6 +46,8 @@ export class WorkerWrappedAsyncDatabaseConnection {
43
46
  return new Promise((resolve, reject) => {
44
47
  if (controller.signal.aborted) {
45
48
  reject(new Error('Called operation on closed remote'));
49
+ // Don't run the operation if we're going to reject
50
+ return;
46
51
  }
47
52
  function handleAbort() {
48
53
  reject(new Error('Remote peer closed with request in flight'));
@@ -112,6 +112,11 @@ export declare class WASqliteConnection extends BaseObserver<WASQLiteConnectionL
112
112
  get currentHoldId(): string | null;
113
113
  protected get sqliteAPI(): SQLiteAPI;
114
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>;
115
120
  markHold(): Promise<string>;
116
121
  releaseHold(holdId: string): Promise<void>;
117
122
  protected openDB(): Promise<number>;
@@ -140,6 +140,13 @@ export class WASqliteConnection extends BaseObserver {
140
140
  }
141
141
  return this._dbP;
142
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
+ }
143
150
  async markHold() {
144
151
  const previousHoldId = this._holdId;
145
152
  this._holdId = `${++this._holdCounter}`;
@@ -20,13 +20,14 @@ export declare class SharedWASQLiteConnection implements AsyncDatabaseConnection
20
20
  protected isClosing: boolean;
21
21
  protected activeHoldId: string | null;
22
22
  constructor(options: SharedWASQLiteConnectionOptions);
23
- init(): Promise<void>;
24
- markHold(): Promise<string>;
25
- releaseHold(id: string): Promise<void>;
26
23
  protected get logger(): ILogger;
27
24
  protected get dbEntry(): SharedDBWorkerConnection;
28
25
  protected get connection(): AsyncDatabaseConnection<ResolvedWebSQLOpenOptions>;
29
26
  protected get clientIds(): Set<number>;
27
+ init(): Promise<void>;
28
+ markHold(): Promise<string>;
29
+ releaseHold(id: string): Promise<void>;
30
+ isAutoCommit(): Promise<boolean>;
30
31
  /**
31
32
  * Handles closing of a shared connection.
32
33
  * The connection is only closed if there are no active clients using it.
@@ -10,6 +10,18 @@ export class SharedWASQLiteConnection {
10
10
  this.isClosing = false;
11
11
  this.activeHoldId = null;
12
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
+ }
13
25
  async init() {
14
26
  // No-op since the connection is already initialized when it was created
15
27
  }
@@ -25,17 +37,8 @@ export class SharedWASQLiteConnection {
25
37
  this.activeHoldId = null;
26
38
  }
27
39
  }
28
- get logger() {
29
- return this.options.logger;
30
- }
31
- get dbEntry() {
32
- return this.options.dbMap.get(this.options.dbFilename);
33
- }
34
- get connection() {
35
- return this.dbEntry.db;
36
- }
37
- get clientIds() {
38
- return this.dbEntry.clientIds;
40
+ async isAutoCommit() {
41
+ return this.connection.isAutoCommit();
39
42
  }
40
43
  /**
41
44
  * Handles closing of a shared connection.
@@ -49,12 +52,9 @@ export class SharedWASQLiteConnection {
49
52
  logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
50
53
  clientIds.delete(clientId);
51
54
  if (this.activeHoldId) {
52
- /**
53
- * The hold hasn't been released, but we're closing now.
54
- * We can proactively cleanup and release the hold.
55
- */
56
- await this.connection.execute('ROLLBACK').catch(() => { });
57
- await this.connection.releaseHold(this.activeHoldId).catch(() => { });
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
58
  }
59
59
  if (clientIds.size == 0) {
60
60
  logger.debug(`Closing connection to ${this.options}.`);
@@ -6,7 +6,7 @@ import { createBaseLogger, createLogger } from '@powersync/common';
6
6
  import * as Comlink from 'comlink';
7
7
  import { getNavigatorLocks } from '../../shared/navigator';
8
8
  import { SharedWASQLiteConnection } from './SharedWASQLiteConnection';
9
- import { WorkerWASQLiteConnection, proxyWASQLiteConnection } from './WorkerWASQLiteConnection';
9
+ import { WorkerWASQLiteConnection } from './WorkerWASQLiteConnection';
10
10
  const baseLogger = createBaseLogger();
11
11
  baseLogger.useDefaults();
12
12
  const logger = createLogger('db-worker');
@@ -46,7 +46,7 @@ const openDBShared = async (options) => {
46
46
  clientId,
47
47
  logger
48
48
  });
49
- return proxyWASQLiteConnection(sharedConnection);
49
+ return Comlink.proxy(sharedConnection);
50
50
  });
51
51
  };
52
52
  // Check if we're in a SharedWorker context
@@ -1,9 +1,9 @@
1
- import { AsyncDatabaseConnection, OnTableChangeCallback } from '../../db/adapters/AsyncDatabaseConnection';
1
+ import { OnTableChangeCallback } from '../../db/adapters/AsyncDatabaseConnection';
2
2
  import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
3
3
  /**
4
- * Fully proxies a WASQLiteConnection to be used as an AsyncDatabaseConnection.
4
+ * A Small proxy wrapper around the WASqliteConnection.
5
+ * This ensures that certain return types are properly proxied.
5
6
  */
6
- export declare function proxyWASQLiteConnection(connection: AsyncDatabaseConnection): AsyncDatabaseConnection;
7
7
  export declare class WorkerWASQLiteConnection extends WASqliteConnection {
8
8
  registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void>;
9
9
  }
@@ -1,21 +1,9 @@
1
1
  import * as Comlink from 'comlink';
2
2
  import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
3
3
  /**
4
- * Fully proxies a WASQLiteConnection to be used as an AsyncDatabaseConnection.
4
+ * A Small proxy wrapper around the WASqliteConnection.
5
+ * This ensures that certain return types are properly proxied.
5
6
  */
6
- export function proxyWASQLiteConnection(connection) {
7
- return Comlink.proxy({
8
- init: Comlink.proxy(() => connection.init()),
9
- close: Comlink.proxy(() => connection.close()),
10
- markHold: Comlink.proxy(() => connection.markHold()),
11
- releaseHold: Comlink.proxy((holdId) => connection.releaseHold(holdId)),
12
- execute: Comlink.proxy((sql, params) => connection.execute(sql, params)),
13
- executeRaw: Comlink.proxy((sql, params) => connection.executeRaw(sql, params)),
14
- executeBatch: Comlink.proxy((sql, params) => connection.executeBatch(sql, params)),
15
- registerOnTableChange: Comlink.proxy((callback) => connection.registerOnTableChange(callback)),
16
- getConfig: Comlink.proxy(() => connection.getConfig())
17
- });
18
- }
19
7
  export class WorkerWASQLiteConnection extends WASqliteConnection {
20
8
  async registerOnTableChange(callback) {
21
9
  // Proxy the callback remove function
@@ -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;