@powersync/web 1.35.0 → 1.37.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 (91) hide show
  1. package/dist/index.umd.js +1126 -1231
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +599 -3086
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +860 -868
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/lib/package.json +2 -3
  8. package/lib/src/db/PowerSyncDatabase.d.ts +1 -2
  9. package/lib/src/db/PowerSyncDatabase.js +3 -4
  10. package/lib/src/db/adapters/AsyncWebAdapter.d.ts +40 -0
  11. package/lib/src/db/adapters/AsyncWebAdapter.js +69 -0
  12. package/lib/src/db/adapters/SSRDBAdapter.d.ts +1 -2
  13. package/lib/src/db/adapters/SSRDBAdapter.js +5 -6
  14. package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.d.ts +56 -0
  15. package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.js +121 -0
  16. package/lib/src/db/adapters/wa-sqlite/DatabaseClient.d.ts +54 -0
  17. package/lib/src/db/adapters/wa-sqlite/DatabaseClient.js +227 -0
  18. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.d.ts +47 -0
  19. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.js +146 -0
  20. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +46 -0
  21. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.js +147 -0
  22. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +14 -6
  23. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +66 -39
  24. package/lib/src/db/adapters/wa-sqlite/vfs.d.ts +61 -0
  25. package/lib/src/db/adapters/wa-sqlite/vfs.js +91 -0
  26. package/lib/src/db/adapters/web-sql-flags.d.ts +5 -0
  27. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +1 -2
  28. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +2 -3
  29. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +4 -19
  30. package/lib/src/index.d.ts +1 -4
  31. package/lib/src/index.js +1 -4
  32. package/lib/src/shared/tab_close_signal.d.ts +11 -0
  33. package/lib/src/shared/tab_close_signal.js +26 -0
  34. package/lib/src/worker/db/MultiDatabaseServer.d.ts +17 -0
  35. package/lib/src/worker/db/MultiDatabaseServer.js +86 -0
  36. package/lib/src/worker/db/WASQLiteDB.worker.js +9 -48
  37. package/lib/src/worker/db/open-worker-database.d.ts +3 -3
  38. package/lib/src/worker/db/open-worker-database.js +1 -1
  39. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +5 -6
  40. package/lib/src/worker/sync/SharedSyncImplementation.js +92 -54
  41. package/lib/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +3 -4
  43. package/src/db/PowerSyncDatabase.ts +3 -3
  44. package/src/db/adapters/AsyncWebAdapter.ts +91 -0
  45. package/src/db/adapters/SSRDBAdapter.ts +7 -7
  46. package/src/db/adapters/wa-sqlite/ConcurrentConnection.ts +137 -0
  47. package/src/db/adapters/wa-sqlite/DatabaseClient.ts +325 -0
  48. package/src/db/adapters/wa-sqlite/DatabaseServer.ts +201 -0
  49. package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +191 -0
  50. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +87 -43
  51. package/src/db/adapters/wa-sqlite/vfs.ts +112 -0
  52. package/src/db/adapters/web-sql-flags.ts +6 -0
  53. package/src/db/sync/SSRWebStreamingSyncImplementation.ts +2 -3
  54. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +4 -20
  55. package/src/index.ts +1 -4
  56. package/src/shared/tab_close_signal.ts +28 -0
  57. package/src/worker/db/MultiDatabaseServer.ts +104 -0
  58. package/src/worker/db/WASQLiteDB.worker.ts +10 -57
  59. package/src/worker/db/open-worker-database.ts +3 -3
  60. package/src/worker/sync/SharedSyncImplementation.ts +118 -58
  61. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +0 -1878
  62. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +0 -1
  63. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js +0 -555
  64. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js.map +0 -1
  65. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +0 -17
  66. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +0 -33
  67. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +0 -49
  68. package/lib/src/db/adapters/AsyncDatabaseConnection.js +0 -1
  69. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +0 -109
  70. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +0 -401
  71. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +0 -59
  72. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +0 -147
  73. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +0 -12
  74. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +0 -19
  75. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +0 -155
  76. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +0 -401
  77. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +0 -32
  78. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +0 -49
  79. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +0 -42
  80. package/lib/src/worker/db/SharedWASQLiteConnection.js +0 -90
  81. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +0 -9
  82. package/lib/src/worker/db/WorkerWASQLiteConnection.js +0 -12
  83. package/src/db/adapters/AbstractWebSQLOpenFactory.ts +0 -48
  84. package/src/db/adapters/AsyncDatabaseConnection.ts +0 -55
  85. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +0 -490
  86. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +0 -201
  87. package/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.ts +0 -23
  88. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +0 -497
  89. package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +0 -86
  90. package/src/worker/db/SharedWASQLiteConnection.ts +0 -131
  91. package/src/worker/db/WorkerWASQLiteConnection.ts +0 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/web",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
4
4
  "description": "PowerSync Web SDK",
5
5
  "main": "lib/src/index.js",
6
6
  "module": "lib/src/index.js",
@@ -56,14 +56,13 @@
56
56
  "license": "Apache-2.0",
57
57
  "peerDependencies": {
58
58
  "@journeyapps/wa-sqlite": "^1.5.0",
59
- "@powersync/common": "^1.48.0"
59
+ "@powersync/common": "^1.50.0"
60
60
  },
61
61
  "dependencies": {
62
- "async-mutex": "^0.5.0",
63
62
  "bson": "^6.10.4",
64
63
  "comlink": "^4.4.2",
65
64
  "commander": "^12.1.0",
66
- "@powersync/common": "1.48.0"
65
+ "@powersync/common": "1.50.0"
67
66
  },
68
67
  "devDependencies": {
69
68
  "@journeyapps/wa-sqlite": "^1.5.0",
@@ -10,15 +10,14 @@ import {
10
10
  TriggerManagerConfig,
11
11
  isDBAdapter,
12
12
  isSQLOpenFactory,
13
+ Mutex,
13
14
  type BucketStorageAdapter,
14
15
  type PowerSyncBackendConnector,
15
16
  type PowerSyncCloseOptions,
16
17
  type RequiredAdditionalConnectionOptions
17
18
  } from '@powersync/common';
18
- import { Mutex } from 'async-mutex';
19
19
  import { getNavigatorLocks } from '../shared/navigator.js';
20
20
  import { NAVIGATOR_TRIGGER_CLAIM_MANAGER } from './NavigatorTriggerClaimManager.js';
21
- import { LockedAsyncDatabaseAdapter } from './adapters/LockedAsyncDatabaseAdapter.js';
22
21
  import { WebDBAdapter } from './adapters/WebDBAdapter.js';
23
22
  import { WASQLiteOpenFactory } from './adapters/wa-sqlite/WASQLiteOpenFactory.js';
24
23
  import {
@@ -35,6 +34,7 @@ import {
35
34
  WebStreamingSyncImplementation,
36
35
  WebStreamingSyncImplementationOptions
37
36
  } from './sync/WebStreamingSyncImplementation.js';
37
+ import { AsyncDbAdapter } from './adapters/AsyncWebAdapter.js';
38
38
 
39
39
  export interface WebPowerSyncFlags extends WebSQLFlags {
40
40
  /**
@@ -148,7 +148,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
148
148
  }
149
149
 
150
150
  async _initialize(): Promise<void> {
151
- if (this.database instanceof LockedAsyncDatabaseAdapter) {
151
+ if (this.database instanceof AsyncDbAdapter) {
152
152
  /**
153
153
  * While init is done automatically,
154
154
  * LockedAsyncDatabaseAdapter only exposes config after init.
@@ -0,0 +1,91 @@
1
+ import {
2
+ ConnectionPool,
3
+ DBAdapterDefaultMixin,
4
+ DBAdapterListener,
5
+ DBLockOptions,
6
+ LockContext
7
+ } from '@powersync/common';
8
+ import { SharedConnectionWorker, WebDBAdapter, WebDBAdapterConfiguration } from './WebDBAdapter.js';
9
+ import { DatabaseClient } from './wa-sqlite/DatabaseClient.js';
10
+
11
+ type PendingListener = { listener: Partial<DBAdapterListener>; closeAfterRegisteredOnResolvedPool?: () => void };
12
+
13
+ /**
14
+ * A connection pool implementation delegating to another pool opened asynchronnously.
15
+ */
16
+ class AsyncConnectionPool implements ConnectionPool {
17
+ protected readonly inner: Promise<DatabaseClient>;
18
+
19
+ protected resolvedClient?: DatabaseClient;
20
+ private readonly pendingListeners = new Set<PendingListener>();
21
+
22
+ constructor(
23
+ inner: Promise<DatabaseClient>,
24
+ readonly name: string
25
+ ) {
26
+ this.inner = inner.then((client) => {
27
+ for (const pending of this.pendingListeners) {
28
+ pending.closeAfterRegisteredOnResolvedPool = client.registerListener(pending.listener);
29
+ }
30
+ this.pendingListeners.clear();
31
+
32
+ this.resolvedClient = client;
33
+ return client;
34
+ });
35
+ }
36
+
37
+ async init() {
38
+ await this.inner;
39
+ }
40
+
41
+ async close() {
42
+ const inner = await this.inner;
43
+ return await inner.close();
44
+ }
45
+
46
+ async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
47
+ const inner = await this.inner;
48
+ return await inner.readLock(fn, options);
49
+ }
50
+
51
+ async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
52
+ const inner = await this.inner;
53
+ return await inner.writeLock(fn, options);
54
+ }
55
+
56
+ async refreshSchema(): Promise<void> {
57
+ await (await this.inner).refreshSchema();
58
+ }
59
+
60
+ registerListener(listener: Partial<DBAdapterListener>): () => void {
61
+ if (this.resolvedClient) {
62
+ return this.resolvedClient.registerListener(listener);
63
+ } else {
64
+ const pending: PendingListener = { listener };
65
+ this.pendingListeners.add(pending);
66
+ return () => {
67
+ if (pending.closeAfterRegisteredOnResolvedPool) {
68
+ return pending.closeAfterRegisteredOnResolvedPool();
69
+ } else {
70
+ // Has not been registered yet, we can just remove the pending listener.
71
+ this.pendingListeners.delete(pending);
72
+ }
73
+ };
74
+ }
75
+ }
76
+ }
77
+
78
+ export class AsyncDbAdapter extends DBAdapterDefaultMixin(AsyncConnectionPool) implements WebDBAdapter {
79
+ async shareConnection(): Promise<SharedConnectionWorker> {
80
+ const inner = await this.inner;
81
+ return inner.shareConnection();
82
+ }
83
+
84
+ getConfiguration(): WebDBAdapterConfiguration {
85
+ if (this.resolvedClient) {
86
+ return this.resolvedClient.getConfiguration();
87
+ }
88
+
89
+ throw new Error('AsyncDbAdapter.getConfiguration() can only be called after initializing it.');
90
+ }
91
+ }
@@ -5,11 +5,11 @@ import {
5
5
  DBLockOptions,
6
6
  LockContext,
7
7
  QueryResult,
8
- Transaction
8
+ Transaction,
9
+ Mutex,
10
+ timeoutSignal
9
11
  } from '@powersync/common';
10
12
 
11
- import { Mutex } from 'async-mutex';
12
-
13
13
  const MOCK_QUERY_RESPONSE: QueryResult = {
14
14
  rowsAffected: 0
15
15
  };
@@ -34,19 +34,19 @@ export class SSRDBAdapter extends BaseObserver<DBAdapterListener> implements DBA
34
34
  close() {}
35
35
 
36
36
  async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) {
37
- return this.readMutex.runExclusive(() => fn(this));
37
+ return this.readMutex.runExclusive(() => fn(this), timeoutSignal(options?.timeoutMs));
38
38
  }
39
39
 
40
40
  async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) {
41
- return this.readLock(() => fn(this.generateMockTransactionContext()));
41
+ return this.readLock(() => fn(this.generateMockTransactionContext()), options);
42
42
  }
43
43
 
44
44
  async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) {
45
- return this.writeMutex.runExclusive(() => fn(this));
45
+ return this.writeMutex.runExclusive(() => fn(this), timeoutSignal(options?.timeoutMs));
46
46
  }
47
47
 
48
48
  async writeTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) {
49
- return this.writeLock(() => fn(this.generateMockTransactionContext()));
49
+ return this.writeLock(() => fn(this.generateMockTransactionContext()), options);
50
50
  }
51
51
 
52
52
  async execute(query: string, params?: any[]): Promise<QueryResult> {
@@ -0,0 +1,137 @@
1
+ import { Mutex, UnlockFn } from '@powersync/common';
2
+ import { RawSqliteConnection } from './RawSqliteConnection.js';
3
+ import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory.js';
4
+
5
+ /**
6
+ * A wrapper around a {@link RawSqliteConnection} allowing multiple tabs to access it.
7
+ *
8
+ * To allow potentially concurrent accesses from different clients, this requires a local mutex implementation here.
9
+ *
10
+ * Note that instances of this class are not safe to proxy across context boundaries with comlink! We need to be able to
11
+ * rely on mutexes being returned reliably, so additional checks to detect say a client tab closing are required to
12
+ * avoid deadlocks.
13
+ */
14
+ export class ConcurrentSqliteConnection {
15
+ /**
16
+ * An outer mutex ensuring at most one {@link ConnectionLeaseToken} can exist for this connection at a time.
17
+ *
18
+ * If null, we'll use navigator locks instead.
19
+ */
20
+ private leaseMutex: Mutex | null;
21
+
22
+ /**
23
+ * @param needsNavigatorLocks Whether access to the database needs an additional navigator lock guard.
24
+ *
25
+ * While {@link ConcurrentSqliteConnection} prevents concurrent access to a database _connection_, it's possible we
26
+ * might have multiple connections to the same physical database (e.g. if multiple tabs use dedicated workers).
27
+ * In those setups, we use navigator locks instead of an internal mutex to guard access..
28
+ */
29
+ constructor(
30
+ private readonly inner: RawSqliteConnection,
31
+ needsNavigatorLocks: boolean
32
+ ) {
33
+ this.leaseMutex = needsNavigatorLocks ? null : new Mutex();
34
+ }
35
+
36
+ get options(): ResolvedWASQLiteOpenFactoryOptions {
37
+ return this.inner.options;
38
+ }
39
+
40
+ acquireMutex(abort?: AbortSignal): Promise<UnlockFn> {
41
+ if (this.leaseMutex) {
42
+ return this.leaseMutex.acquire(abort);
43
+ }
44
+
45
+ return new Promise((resolve, reject) => {
46
+ const options: LockOptions = { signal: abort };
47
+
48
+ navigator.locks
49
+ .request(`db-lock-${this.options.dbFilename}`, options, (_) => {
50
+ return new Promise<void>((returnLock) => {
51
+ return resolve(() => {
52
+ returnLock();
53
+ });
54
+ });
55
+ })
56
+ .catch(reject);
57
+ });
58
+ }
59
+
60
+ // Unsafe, unguarded access to the SQLite connection.
61
+ unsafeUseInner(): RawSqliteConnection {
62
+ return this.inner;
63
+ }
64
+
65
+ /**
66
+ * @returns A {@link ConnectionLeaseToken}. Until that token is returned, no other client can use the database.
67
+ */
68
+ async acquireConnection(abort?: AbortSignal): Promise<ConnectionLeaseToken> {
69
+ const returnMutex = await this.acquireMutex(abort);
70
+ const token = new ConnectionLeaseToken(returnMutex, this.inner);
71
+
72
+ try {
73
+ // Assert that the inner connection is initialized at this point, fail early if it's not.
74
+ this.inner.requireSqlite();
75
+
76
+ // If a previous client was interrupted in the middle of a transaction AND this is a shared worker, it's possible
77
+ // for the connection to still be in a transaction. To avoid inconsistent state, we roll back connection leases
78
+ // that haven't been comitted.
79
+ if (!this.inner.isAutoCommit()) {
80
+ await this.inner.executeRaw('ROLLBACK');
81
+ }
82
+ } catch (e) {
83
+ returnMutex();
84
+ throw e;
85
+ }
86
+
87
+ return token;
88
+ }
89
+
90
+ async close(): Promise<void> {
91
+ const returnMutex = await this.acquireMutex();
92
+ try {
93
+ await this.inner.close();
94
+ } finally {
95
+ returnMutex();
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * An instance representing temporary exclusive access to a {@link ConcurrentSqliteConnection}.
102
+ */
103
+ export class ConnectionLeaseToken {
104
+ /** Ensures that the client with access to this token can't run statements concurrently. */
105
+ private useMutex: Mutex = new Mutex();
106
+ private closed = false;
107
+
108
+ constructor(
109
+ private returnMutex: UnlockFn,
110
+ private connection: RawSqliteConnection
111
+ ) {}
112
+
113
+ /**
114
+ * Returns this lease, allowing another client to use the database connection.
115
+ */
116
+ async returnLease() {
117
+ await this.useMutex.runExclusive(async () => {
118
+ if (!this.closed) {
119
+ this.closed = true;
120
+ this.returnMutex();
121
+ }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * This should only be used internally, since the callback must not use the raw connection after resolving.
127
+ */
128
+ async use<T>(callback: (conn: RawSqliteConnection) => Promise<T>): Promise<T> {
129
+ return await this.useMutex.runExclusive(async () => {
130
+ if (this.closed) {
131
+ throw new Error('lease token has already been closed');
132
+ }
133
+
134
+ return await callback(this.connection);
135
+ });
136
+ }
137
+ }
@@ -0,0 +1,325 @@
1
+ import {
2
+ QueryResult,
3
+ LockContext,
4
+ DBLockOptions,
5
+ DBAdapterListener,
6
+ ConnectionPool,
7
+ SqlExecutor,
8
+ DBGetUtilsDefaultMixin,
9
+ BatchedUpdateNotification,
10
+ BaseObserver,
11
+ ConnectionClosedError,
12
+ SQLOpenOptions
13
+ } from '@powersync/common';
14
+ import { SharedConnectionWorker, WebDBAdapterConfiguration } from '../WebDBAdapter.js';
15
+ import { ClientConnectionView } from './DatabaseServer.js';
16
+ import { RawQueryResult } from './RawSqliteConnection.js';
17
+ import * as Comlink from 'comlink';
18
+ import { WorkerDBOpenerOptions } from './WASQLiteOpenFactory.js';
19
+
20
+ export interface OpenWorkerConnection {
21
+ connect(config: WorkerDBOpenerOptions): Promise<ClientConnectionView>;
22
+ connectToExisting(options: { identifier: string; lockName: string }): Promise<ClientConnectionView>;
23
+ }
24
+
25
+ export interface ClientOptions {
26
+ connection: ClientConnectionView;
27
+ /**
28
+ * The remote from which the {@link connection} has been obtained.
29
+ *
30
+ * We use this to be able to expose this port to other clients wanting to connect to the database (e.g. the shared
31
+ * sync worker).
32
+ *
33
+ * For sources not based on workers, returns null.
34
+ */
35
+ source: Comlink.Remote<OpenWorkerConnection> | null;
36
+ /**
37
+ * Whether the remote we're connecting to can close unexpectedly (e.g. because we're a shared worker connecting to a
38
+ * dedicated worker handle we've received from a tab).
39
+ */
40
+ remoteCanCloseUnexpectedly: boolean;
41
+ onClose?: () => void;
42
+ }
43
+
44
+ /**
45
+ * A single-connection {@link ConnectionPool} implementation based on a worker connection.
46
+ */
47
+ export class DatabaseClient<Config extends SQLOpenOptions = WebDBAdapterConfiguration>
48
+ extends BaseObserver<DBAdapterListener>
49
+ implements ConnectionPool
50
+ {
51
+ #connection: ConnectionState;
52
+ #shareConnectionAbortController = new AbortController();
53
+ #receiveTableUpdates: MessagePort;
54
+
55
+ constructor(
56
+ private readonly options: ClientOptions,
57
+ private readonly config: Config
58
+ ) {
59
+ super();
60
+ this.#connection = {
61
+ connection: options.connection,
62
+ notifyRemoteClosed: options.remoteCanCloseUnexpectedly ? new AbortController() : undefined,
63
+ traceQueries: config.debugMode === true
64
+ };
65
+
66
+ const { port1, port2 } = new MessageChannel();
67
+ options.connection.setUpdateListener(Comlink.transfer(port1, [port1]));
68
+
69
+ this.#receiveTableUpdates = port2;
70
+ port2.onmessage = (event) => {
71
+ const tables = event.data as string[];
72
+ const notification: BatchedUpdateNotification = {
73
+ tables,
74
+ groupedUpdates: {},
75
+ rawUpdates: []
76
+ };
77
+ this.iterateListeners((l) => {
78
+ l.tablesUpdated && l.tablesUpdated(notification);
79
+ });
80
+ };
81
+ }
82
+
83
+ get name(): string {
84
+ return this.config.dbFilename;
85
+ }
86
+
87
+ /**
88
+ * Marks the remote as closed.
89
+ *
90
+ * This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
91
+ * it happens, all outstanding requests on this pool would never resolve. To avoid livelocks in this scenario, we
92
+ * throw on all outstanding promises and forbid new calls.
93
+ */
94
+ markRemoteClosed() {
95
+ // Can non-null assert here because this function is only supposed to be called when remoteCanCloseUnexpectedly was
96
+ // set.
97
+ this.#connection.notifyRemoteClosed!.abort();
98
+ }
99
+
100
+ async close(): Promise<void> {
101
+ // This connection is no longer shared, so we can close locks held for shareConnection calls.
102
+ this.#shareConnectionAbortController.abort();
103
+ this.#receiveTableUpdates.close();
104
+
105
+ await useConnectionState(this.#connection, (c) => c.close(), true);
106
+ this.options.onClose?.();
107
+ this.options.source?.[Comlink.releaseProxy]();
108
+ }
109
+
110
+ readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
111
+ return this.#lock(false, fn, options);
112
+ }
113
+
114
+ writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
115
+ return this.#lock(true, fn, options);
116
+ }
117
+
118
+ async #lock<T>(write: boolean, fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
119
+ const token = await useConnectionState(this.#connection, (c) => c.requestAccess(write, options?.timeoutMs));
120
+ try {
121
+ return await fn(new ClientLockContext(this.#connection, token));
122
+ } finally {
123
+ await useConnectionState(this.#connection, (c) => c.completeAccess(token));
124
+ }
125
+ }
126
+
127
+ async refreshSchema(): Promise<void> {
128
+ // Currently a no-op on the web.
129
+ }
130
+
131
+ async shareConnection(): Promise<SharedConnectionWorker> {
132
+ /**
133
+ * Hold a navigator lock in order to avoid features such as Chrome's frozen tabs,
134
+ * or Edge's sleeping tabs from pausing the thread for this connection.
135
+ * This promise resolves once a lock is obtained.
136
+ * This lock will be held as long as this connection is open.
137
+ * The `shareConnection` method should not be called on multiple tabs concurrently.
138
+ */
139
+ const abort = this.#shareConnectionAbortController;
140
+ const source = this.options.source;
141
+ if (source == null) {
142
+ throw new Error(`shareConnection() is only available for connections based by workers.`);
143
+ }
144
+
145
+ await new Promise<void>((resolve, reject) =>
146
+ navigator.locks
147
+ .request(
148
+ `shared-connection-${this.name}-${Date.now()}-${Math.round(Math.random() * 10000)}`,
149
+ {
150
+ signal: abort.signal
151
+ },
152
+ async () => {
153
+ resolve();
154
+
155
+ // Free the lock when the connection is already closed.
156
+ if (abort.signal.aborted) {
157
+ return;
158
+ }
159
+
160
+ // Hold the lock while the shared connection is in use.
161
+ await new Promise<void>((releaseLock) => {
162
+ abort.signal.addEventListener('abort', () => {
163
+ releaseLock();
164
+ });
165
+ });
166
+ }
167
+ )
168
+ // We aren't concerned with abort errors here
169
+ .catch((ex) => {
170
+ if (ex.name == 'AbortError') {
171
+ resolve();
172
+ } else {
173
+ reject(ex);
174
+ }
175
+ })
176
+ );
177
+
178
+ const newPort = await source[Comlink.createEndpoint]();
179
+ return { port: newPort, identifier: this.name };
180
+ }
181
+
182
+ getConfiguration(): Config {
183
+ return this.config;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * A {@link SqlExecutor} implemented by sending commands to a worker.
189
+ *
190
+ * While an instance is active, it has exclusive access to the underlying database connection (as represented by its
191
+ * token).
192
+ */
193
+ class ClientSqlExecutor implements SqlExecutor {
194
+ readonly #connection: ConnectionState;
195
+ readonly #token: string;
196
+
197
+ constructor(connection: ConnectionState, token: string) {
198
+ this.#connection = connection;
199
+ this.#token = token;
200
+ }
201
+
202
+ /**
203
+ * Requests an operation from the worker, potentially tracing it if that option has been enabled.
204
+ */
205
+ private async maybeTrace<T>(
206
+ fn: (connection: ClientConnectionView) => Promise<T>,
207
+ describeForTrace: () => string
208
+ ): Promise<T> {
209
+ if (this.#connection.traceQueries) {
210
+ const start = performance.now();
211
+ const description = describeForTrace();
212
+
213
+ try {
214
+ const r = await useConnectionState(this.#connection, fn);
215
+ performance.measure(`[SQL] ${description}`, { start });
216
+ return r;
217
+ } catch (e: any) {
218
+ performance.measure(`[SQL] [ERROR: ${e.message}] ${description}`, { start });
219
+ throw e;
220
+ }
221
+ } else {
222
+ return useConnectionState(this.#connection, fn);
223
+ }
224
+ }
225
+
226
+ async execute(query: string, params?: any[] | undefined): Promise<QueryResult> {
227
+ const rs = await this.#executeOnWorker(query, params);
228
+ let rows: QueryResult['rows'] | undefined;
229
+ if (rs.resultSet) {
230
+ const resultSet = rs.resultSet;
231
+
232
+ function rowToJavaScriptObject(row: any[]): Record<string, any> {
233
+ const obj: Record<string, any> = {};
234
+ resultSet.columns.forEach((key, idx) => (obj[key] = row[idx]));
235
+ return obj;
236
+ }
237
+
238
+ const mapped = resultSet.rows.map(rowToJavaScriptObject);
239
+
240
+ rows = {
241
+ _array: mapped,
242
+ length: mapped.length,
243
+ item: (idx: number) => mapped[idx]
244
+ };
245
+ }
246
+
247
+ return {
248
+ rowsAffected: rs.changes,
249
+ insertId: rs.lastInsertRowId,
250
+ rows
251
+ };
252
+ }
253
+
254
+ async executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
255
+ const rs = await this.#executeOnWorker(query, params);
256
+ return rs.resultSet?.rows ?? [];
257
+ }
258
+
259
+ async #executeOnWorker(query: string, params: any[] | undefined): Promise<RawQueryResult> {
260
+ return this.maybeTrace(
261
+ (c) => c.execute(this.#token, query, params),
262
+ () => query
263
+ );
264
+ }
265
+
266
+ async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
267
+ const results = await this.maybeTrace(
268
+ (c) => c.executeBatch(this.#token, query, params),
269
+ () => `${query} (batch of ${params.length})`
270
+ );
271
+ const result: QueryResult = { insertId: undefined, rowsAffected: 0 };
272
+ for (const source of results) {
273
+ result.insertId = source.lastInsertRowId;
274
+ result.rowsAffected += source.changes;
275
+ }
276
+
277
+ return result;
278
+ }
279
+ }
280
+
281
+ class ClientLockContext extends DBGetUtilsDefaultMixin(ClientSqlExecutor) implements LockContext {}
282
+
283
+ interface ConnectionState {
284
+ connection: ClientConnectionView;
285
+ notifyRemoteClosed: AbortController | undefined;
286
+ traceQueries: boolean;
287
+ }
288
+
289
+ async function useConnectionState<T>(
290
+ state: ConnectionState,
291
+ workerPromise: (connection: ClientConnectionView) => Promise<T>,
292
+ fireActionOnAbort = false
293
+ ): Promise<T> {
294
+ const controller = state.notifyRemoteClosed;
295
+ if (controller) {
296
+ return new Promise((resolve, reject) => {
297
+ if (controller.signal.aborted) {
298
+ reject(new ConnectionClosedError('Called operation on closed remote'));
299
+ if (!fireActionOnAbort) {
300
+ // Don't run the operation if we're going to reject
301
+ // We might want to fire-and-forget the operation in some cases (like a close operation)
302
+ return;
303
+ }
304
+ }
305
+
306
+ function handleAbort() {
307
+ reject(new ConnectionClosedError('Remote peer closed with request in flight'));
308
+ }
309
+
310
+ function completePromise(action: () => void) {
311
+ controller!.signal.removeEventListener('abort', handleAbort);
312
+ action();
313
+ }
314
+
315
+ controller.signal.addEventListener('abort', handleAbort);
316
+
317
+ workerPromise(state.connection)
318
+ .then((data) => completePromise(() => resolve(data)))
319
+ .catch((e) => completePromise(() => reject(e)));
320
+ });
321
+ } else {
322
+ // Can't close, so just return the inner worker promise unguarded.
323
+ return workerPromise(state.connection);
324
+ }
325
+ }