@powersync/web 0.0.0-dev-20260311103504 → 0.0.0-dev-20260503073249

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 (121) hide show
  1. package/dist/2075a31bb151adbb9767.wasm +0 -0
  2. package/dist/3322bc84de986b63c2cd.wasm +0 -0
  3. package/dist/8e97452e297be23b5e50.wasm +0 -0
  4. package/dist/fbc178b70d530e8ce02b.wasm +0 -0
  5. package/dist/index.umd.js +5341 -1279
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/worker/SharedSyncImplementation.umd.js +1113 -3526
  8. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  9. package/dist/worker/WASQLiteDB.umd.js +1397 -1332
  10. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  11. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-9af0a7.umd.js +31 -0
  12. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-9af0a7.umd.js.map +1 -0
  13. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-bbf5a9.umd.js +31 -0
  14. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-bbf5a9.umd.js.map +1 -0
  15. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-c26e0f.umd.js +31 -0
  16. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-cc5fcc.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-c26e0f.umd.js.map} +1 -1
  17. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +31 -0
  18. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js.map} +1 -1
  19. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-2fb422.umd.js +3562 -0
  20. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-2fb422.umd.js.map +1 -0
  21. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-0df390.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-96fb23.umd.js} +16 -16
  22. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-0df390.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-96fb23.umd.js.map} +1 -1
  23. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-151024.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-c89911.umd.js} +12 -12
  24. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-151024.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-c89911.umd.js.map} +1 -1
  25. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-c01ef0.umd.js → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-ec4eb1.umd.js} +14 -14
  26. package/dist/worker/{node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_src_examples-c01ef0.umd.js.map → node_modules_pnpm_journeyapps_wa-sqlite_1_7_0_node_modules_journeyapps_wa-sqlite_src_examples-ec4eb1.umd.js.map} +1 -1
  27. package/lib/package.json +4 -5
  28. package/lib/src/db/PowerSyncDatabase.d.ts +2 -3
  29. package/lib/src/db/PowerSyncDatabase.js +3 -12
  30. package/lib/src/db/adapters/AsyncWebAdapter.d.ts +50 -0
  31. package/lib/src/db/adapters/AsyncWebAdapter.js +163 -0
  32. package/lib/src/db/adapters/SSRDBAdapter.d.ts +1 -2
  33. package/lib/src/db/adapters/SSRDBAdapter.js +5 -6
  34. package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.d.ts +56 -0
  35. package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.js +121 -0
  36. package/lib/src/db/adapters/wa-sqlite/DatabaseClient.d.ts +54 -0
  37. package/lib/src/db/adapters/wa-sqlite/DatabaseClient.js +227 -0
  38. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.d.ts +47 -0
  39. package/lib/src/db/adapters/wa-sqlite/DatabaseServer.js +145 -0
  40. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +46 -0
  41. package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.js +147 -0
  42. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +28 -6
  43. package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +104 -55
  44. package/lib/src/db/adapters/wa-sqlite/vfs.d.ts +50 -0
  45. package/lib/src/db/adapters/wa-sqlite/vfs.js +76 -0
  46. package/lib/src/db/adapters/web-sql-flags.d.ts +5 -0
  47. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +5 -2
  48. package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +6 -3
  49. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +4 -19
  50. package/lib/src/index.d.ts +1 -4
  51. package/lib/src/index.js +1 -4
  52. package/lib/src/shared/tab_close_signal.d.ts +11 -0
  53. package/lib/src/shared/tab_close_signal.js +26 -0
  54. package/lib/src/worker/db/MultiDatabaseServer.d.ts +17 -0
  55. package/lib/src/worker/db/MultiDatabaseServer.js +89 -0
  56. package/lib/src/worker/db/WASQLiteDB.worker.js +9 -48
  57. package/lib/src/worker/db/open-worker-database.d.ts +3 -3
  58. package/lib/src/worker/db/open-worker-database.js +2 -2
  59. package/lib/src/worker/sync/SharedSyncImplementation.d.ts +5 -6
  60. package/lib/src/worker/sync/SharedSyncImplementation.js +88 -54
  61. package/lib/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +5 -6
  63. package/src/db/PowerSyncDatabase.ts +4 -12
  64. package/src/db/adapters/AsyncWebAdapter.ts +207 -0
  65. package/src/db/adapters/SSRDBAdapter.ts +7 -7
  66. package/src/db/adapters/wa-sqlite/ConcurrentConnection.ts +137 -0
  67. package/src/db/adapters/wa-sqlite/DatabaseClient.ts +325 -0
  68. package/src/db/adapters/wa-sqlite/DatabaseServer.ts +203 -0
  69. package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +194 -0
  70. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +152 -63
  71. package/src/db/adapters/wa-sqlite/vfs.ts +96 -0
  72. package/src/db/adapters/web-sql-flags.ts +6 -0
  73. package/src/db/sync/SSRWebStreamingSyncImplementation.ts +7 -3
  74. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +4 -20
  75. package/src/index.ts +1 -4
  76. package/src/shared/tab_close_signal.ts +28 -0
  77. package/src/worker/db/MultiDatabaseServer.ts +107 -0
  78. package/src/worker/db/WASQLiteDB.worker.ts +10 -57
  79. package/src/worker/db/open-worker-database.ts +4 -4
  80. package/src/worker/sync/SharedSyncImplementation.ts +114 -58
  81. package/dist/26d61ca9f5694d064635.wasm +0 -0
  82. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +0 -1878
  83. package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +0 -1
  84. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js +0 -555
  85. package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js.map +0 -1
  86. package/dist/b4c6283dc473b6b3fd24.wasm +0 -0
  87. package/dist/c78985091a0b22aaef03.wasm +0 -0
  88. package/dist/ca59e199e1138b553fad.wasm +0 -0
  89. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-b9c070.umd.js +0 -31
  90. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-b9c070.umd.js.map +0 -1
  91. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-c99c07.umd.js +0 -31
  92. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_mc-wa-s-c99c07.umd.js.map +0 -1
  93. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqli-cc5fcc.umd.js +0 -31
  94. package/dist/worker/node_modules_pnpm_journeyapps_wa-sqlite_1_5_0_node_modules_journeyapps_wa-sqlite_dist_wa-sqlite_mjs.umd.js +0 -31
  95. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +0 -17
  96. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +0 -33
  97. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +0 -49
  98. package/lib/src/db/adapters/AsyncDatabaseConnection.js +0 -1
  99. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +0 -109
  100. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +0 -401
  101. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +0 -59
  102. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +0 -147
  103. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +0 -12
  104. package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +0 -19
  105. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +0 -155
  106. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +0 -401
  107. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +0 -32
  108. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +0 -49
  109. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +0 -42
  110. package/lib/src/worker/db/SharedWASQLiteConnection.js +0 -90
  111. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +0 -9
  112. package/lib/src/worker/db/WorkerWASQLiteConnection.js +0 -12
  113. package/src/db/adapters/AbstractWebSQLOpenFactory.ts +0 -48
  114. package/src/db/adapters/AsyncDatabaseConnection.ts +0 -55
  115. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +0 -490
  116. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +0 -201
  117. package/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.ts +0 -23
  118. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +0 -497
  119. package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +0 -86
  120. package/src/worker/db/SharedWASQLiteConnection.ts +0 -131
  121. 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": "0.0.0-dev-20260311103504",
3
+ "version": "0.0.0-dev-20260503073249",
4
4
  "description": "PowerSync Web SDK",
5
5
  "main": "lib/src/index.js",
6
6
  "module": "lib/src/index.js",
@@ -52,21 +52,20 @@
52
52
  "real-time data stream",
53
53
  "live data"
54
54
  ],
55
- "author": "JOURNEYAPPS",
55
+ "author": "PowerSync",
56
56
  "license": "Apache-2.0",
57
57
  "peerDependencies": {
58
58
  "@journeyapps/wa-sqlite": "^1.5.0",
59
- "@powersync/common": "0.0.0-dev-20260311103504"
59
+ "@powersync/common": "0.0.0-dev-20260503073249"
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": "0.0.0-dev-20260311103504"
65
+ "@powersync/common": "0.0.0-dev-20260503073249"
67
66
  },
68
67
  "devDependencies": {
69
- "@journeyapps/wa-sqlite": "^1.5.0",
68
+ "@journeyapps/wa-sqlite": "^1.7.0",
70
69
  "@types/uuid": "^9.0.6",
71
70
  "crypto-browserify": "^3.12.0",
72
71
  "glob": "^11.0.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,9 +34,11 @@ 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
  /**
41
+ * @deprecated This flag is no longer used. Navigator locks now handle tab detection automatically.
41
42
  * Externally unload open PowerSync database instances when the window closes.
42
43
  * Setting this to `true` requires calling `close` on all open PowerSyncDatabase
43
44
  * instances before the window unloads
@@ -127,7 +128,6 @@ function assertValidDatabaseOptions(options: WebPowerSyncDatabaseOptions): void
127
128
  export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
128
129
  static SHARED_MUTEX = new Mutex();
129
130
 
130
- protected unloadListener?: () => Promise<void>;
131
131
  protected resolvedFlags: WebPowerSyncFlags;
132
132
 
133
133
  constructor(options: WebPowerSyncDatabaseOptionsWithAdapter);
@@ -140,15 +140,10 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
140
140
  assertValidDatabaseOptions(options);
141
141
 
142
142
  this.resolvedFlags = resolveWebPowerSyncFlags(options.flags);
143
-
144
- if (this.resolvedFlags.enableMultiTabs && !this.resolvedFlags.externallyUnload) {
145
- this.unloadListener = () => this.close({ disconnect: false });
146
- window.addEventListener('unload', this.unloadListener);
147
- }
148
143
  }
149
144
 
150
145
  async _initialize(): Promise<void> {
151
- if (this.database instanceof LockedAsyncDatabaseAdapter) {
146
+ if (this.database instanceof AsyncDbAdapter) {
152
147
  /**
153
148
  * While init is done automatically,
154
149
  * LockedAsyncDatabaseAdapter only exposes config after init.
@@ -190,9 +185,6 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
190
185
  * multiple tabs are not enabled.
191
186
  */
192
187
  close(options?: PowerSyncCloseOptions): Promise<void> {
193
- if (this.unloadListener) {
194
- window.removeEventListener('unload', this.unloadListener);
195
- }
196
188
  return super.close({
197
189
  // Don't disconnect by default if multiple tabs are enabled
198
190
  disconnect: options?.disconnect ?? !this.resolvedFlags.enableMultiTabs
@@ -0,0 +1,207 @@
1
+ import {
2
+ ConnectionPool,
3
+ DBAdapterDefaultMixin,
4
+ DBAdapterListener,
5
+ DBLockOptions,
6
+ LockContext,
7
+ Mutex,
8
+ Semaphore,
9
+ UnlockFn
10
+ } from '@powersync/common';
11
+ import { SharedConnectionWorker, WebDBAdapter, WebDBAdapterConfiguration } from './WebDBAdapter.js';
12
+ import { DatabaseClient } from './wa-sqlite/DatabaseClient.js';
13
+
14
+ type PendingListener = { listener: Partial<DBAdapterListener>; closeAfterRegisteredOnResolvedPool?: () => void };
15
+
16
+ /**
17
+ * A connection pool implementation delegating to another pool opened asynchronnously.
18
+ */
19
+ class AsyncConnectionPool implements ConnectionPool {
20
+ protected readonly state: Promise<PoolState>;
21
+ protected resolvedWriter?: DatabaseClient;
22
+
23
+ private readonly pendingListeners = new Set<PendingListener>();
24
+
25
+ constructor(
26
+ inner: Promise<PoolConnection>,
27
+ readonly name: string
28
+ ) {
29
+ this.state = inner.then((client) => {
30
+ for (const pending of this.pendingListeners) {
31
+ pending.closeAfterRegisteredOnResolvedPool = client.writer.registerListener(pending.listener);
32
+ }
33
+ this.pendingListeners.clear();
34
+
35
+ this.resolvedWriter = client.writer;
36
+ if (client.additionalReaders.length) {
37
+ return readWritePoolState(client.writer, client.additionalReaders);
38
+ }
39
+
40
+ return singleConnectionPoolState(client.writer);
41
+ });
42
+ }
43
+
44
+ async init() {
45
+ await this.state;
46
+ }
47
+
48
+ async close() {
49
+ const state = await this.state;
50
+ await state.close();
51
+ }
52
+
53
+ async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
54
+ const state = await this.state;
55
+ return state.withConnection(true, fn, options);
56
+ }
57
+
58
+ async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
59
+ const state = await this.state;
60
+ return state.withConnection(false, fn, options);
61
+ }
62
+
63
+ async refreshSchema(): Promise<void> {
64
+ const state = await this.state;
65
+ await state.refreshSchema();
66
+ }
67
+
68
+ registerListener(listener: Partial<DBAdapterListener>): () => void {
69
+ if (this.resolvedWriter) {
70
+ return this.resolvedWriter.registerListener(listener);
71
+ } else {
72
+ const pending: PendingListener = { listener };
73
+ this.pendingListeners.add(pending);
74
+ return () => {
75
+ if (pending.closeAfterRegisteredOnResolvedPool) {
76
+ return pending.closeAfterRegisteredOnResolvedPool();
77
+ } else {
78
+ // Has not been registered yet, we can just remove the pending listener.
79
+ this.pendingListeners.delete(pending);
80
+ }
81
+ };
82
+ }
83
+ }
84
+ }
85
+
86
+ export interface PoolConnection {
87
+ writer: DatabaseClient;
88
+ additionalReaders: DatabaseClient[];
89
+ }
90
+
91
+ interface PoolState {
92
+ writer: DatabaseClient;
93
+ withConnection<T>(allowReadOnly: boolean, fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T>;
94
+ close(): Promise<void>;
95
+ refreshSchema(): Promise<void>;
96
+ }
97
+
98
+ function singleConnectionPoolState(connection: DatabaseClient): PoolState {
99
+ return {
100
+ writer: connection,
101
+ withConnection: (allowReadOnly, fn, options) => {
102
+ if (allowReadOnly) {
103
+ return connection.readLock(fn, options);
104
+ } else {
105
+ return connection.writeLock(fn, options);
106
+ }
107
+ },
108
+ close: () => connection.close(),
109
+ refreshSchema: () => connection.refreshSchema()
110
+ };
111
+ }
112
+
113
+ function readWritePoolState(writer: DatabaseClient, readers: DatabaseClient[]): PoolState {
114
+ // DatabaseClients have locks internally, so these aren't necessary for correctness. However, our mutex and semaphore
115
+ // implementations are very cheap to cancel, which we use to dispatch reads to the first available connection (by
116
+ // simply requesting all of them and sticking with the first connection we get).
117
+ const writerMutex = new Mutex();
118
+ const readerSemaphore = new Semaphore(readers);
119
+
120
+ return {
121
+ writer,
122
+ async withConnection(allowReadOnly, fn, options) {
123
+ const abortController = new AbortController();
124
+ const abortSignal = abortController.signal;
125
+
126
+ let timeout: any = null;
127
+ let release: UnlockFn | undefined;
128
+ if (options?.timeoutMs) {
129
+ timeout = setTimeout(() => abortController.abort, options.timeoutMs);
130
+ }
131
+
132
+ try {
133
+ if (allowReadOnly) {
134
+ let connection: DatabaseClient;
135
+
136
+ // Even if we have a pool of read connections, it's typically very small and we assume that most queries are
137
+ // reads. So, we want to request any connection from the read pool and the dedicated write connection (which
138
+ // can also serve reads). We race for the first connection we can obtain this way, and then abort the other
139
+ // request.
140
+ [connection, release] = await new Promise<[DatabaseClient, UnlockFn]>((resolve, reject) => {
141
+ let didComplete = false;
142
+ function complete() {
143
+ didComplete = true;
144
+ abortController.abort();
145
+ }
146
+
147
+ function completeSuccess(connection: DatabaseClient, returnFn: UnlockFn) {
148
+ if (didComplete) {
149
+ // We're not going to use this connection, so return it immediately.
150
+ returnFn();
151
+ } else {
152
+ complete();
153
+ resolve([connection, returnFn]);
154
+ }
155
+ }
156
+
157
+ function completeError(error: unknown) {
158
+ // We either have a working connection already, or we've rejected the promise. Either way, we don't need
159
+ // to do either thing again.
160
+ if (didComplete) return;
161
+
162
+ complete();
163
+ reject(error);
164
+ }
165
+
166
+ writerMutex.acquire(abortSignal).then((unlock) => completeSuccess(writer, unlock), completeError);
167
+ readerSemaphore
168
+ .requestOne(abortSignal)
169
+ .then(({ item, release }) => completeSuccess(item, release), completeError);
170
+ });
171
+
172
+ return await connection.readLock(fn);
173
+ } else {
174
+ return await writerMutex.runExclusive(() => writer.writeLock(fn), abortSignal);
175
+ }
176
+ } finally {
177
+ if (timeout != null) {
178
+ clearTimeout(timeout);
179
+ }
180
+ release?.();
181
+ }
182
+ },
183
+ async close() {
184
+ await writer.close();
185
+ await Promise.all(readers.map((r) => r.close()));
186
+ },
187
+ async refreshSchema() {
188
+ await writer.refreshSchema();
189
+ await Promise.all(readers.map((r) => r.refreshSchema()));
190
+ }
191
+ };
192
+ }
193
+
194
+ export class AsyncDbAdapter extends DBAdapterDefaultMixin(AsyncConnectionPool) implements WebDBAdapter {
195
+ async shareConnection(): Promise<SharedConnectionWorker> {
196
+ const state = await this.state;
197
+ return state.writer.shareConnection();
198
+ }
199
+
200
+ getConfiguration(): WebDBAdapterConfiguration {
201
+ if (this.resolvedWriter) {
202
+ return this.resolvedWriter.getConfiguration();
203
+ }
204
+
205
+ throw new Error('AsyncDbAdapter.getConfiguration() can only be called after initializing it.');
206
+ }
207
+ }
@@ -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
+ }