@powersync/web 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.react_native_web.js +212 -92
- package/dist/index.react_native_web.js.map +1 -1
- package/dist/worker/AccessHandlePoolVFS-BPUHfZME.js.map +1 -1
- package/dist/worker/FacadeVFS-d1ZDvud7.js.map +1 -1
- package/dist/worker/IDBBatchAtomicVFS-DbkDb777.js.map +1 -1
- package/dist/worker/MemoryVFS-DVJL5F8j.js.map +1 -1
- package/dist/worker/OPFSCoopSyncVFS-BgTiWPfa.js.map +1 -1
- package/dist/worker/OPFSWriteAheadVFS-Rt5CGCZP.js +2 -0
- package/dist/worker/OPFSWriteAheadVFS-Rt5CGCZP.js.map +1 -0
- package/dist/worker/mc-wa-sqlite-DDFgWP93.js.map +1 -1
- package/dist/worker/mc-wa-sqlite-async-lGclTjKJ.js.map +1 -1
- package/dist/worker/wa-sqlite-B0tZMM0j.js.map +1 -1
- package/dist/worker/wa-sqlite-async-CM6BmfRh.js.map +1 -1
- package/dist/worker/worker.js +1 -1
- package/dist/worker/worker.js.map +1 -1
- package/lib/db/PowerSyncDatabase.js +16 -11
- package/lib/db/PowerSyncDatabase.js.map +1 -1
- package/lib/db/adapters/AsyncWebAdapter.js +4 -55
- package/lib/db/adapters/AsyncWebAdapter.js.map +1 -1
- package/lib/db/adapters/acquireFromPool.d.ts +7 -0
- package/lib/db/adapters/acquireFromPool.js +60 -0
- package/lib/db/adapters/acquireFromPool.js.map +1 -0
- package/lib/db/adapters/memory-pool/client.d.ts +70 -0
- package/lib/db/adapters/memory-pool/client.js +269 -0
- package/lib/db/adapters/memory-pool/client.js.map +1 -0
- package/lib/db/adapters/memory-pool/shared.d.ts +51 -0
- package/lib/db/adapters/memory-pool/shared.js +16 -0
- package/lib/db/adapters/memory-pool/shared.js.map +1 -0
- package/lib/db/adapters/memory-pool/vfs.d.ts +19 -0
- package/lib/db/adapters/memory-pool/vfs.js +247 -0
- package/lib/db/adapters/memory-pool/vfs.js.map +1 -0
- package/lib/db/adapters/memory-pool/worker.d.ts +1 -0
- package/lib/db/adapters/memory-pool/worker.js +45 -0
- package/lib/db/adapters/memory-pool/worker.js.map +1 -0
- package/lib/db/adapters/options.d.ts +7 -0
- package/lib/db/adapters/options.js.map +1 -1
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +8 -0
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js +56 -5
- package/lib/db/adapters/wa-sqlite/RawSqliteConnection.js.map +1 -1
- package/lib/db/adapters/wa-sqlite/StatementCache.d.ts +16 -0
- package/lib/db/adapters/wa-sqlite/StatementCache.js +44 -0
- package/lib/db/adapters/wa-sqlite/StatementCache.js.map +1 -0
- package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +7 -4
- package/lib/db/adapters/wa-sqlite/WASQLiteOpenFactory.js.map +1 -1
- package/lib/worker/db/MultiDatabaseServer.d.ts +1 -2
- package/lib/worker/db/MultiDatabaseServer.js +28 -17
- package/lib/worker/db/MultiDatabaseServer.js.map +1 -1
- package/package.json +13 -6
- package/src/db/PowerSyncDatabase.ts +18 -11
- package/src/db/adapters/AsyncWebAdapter.ts +13 -61
- package/src/db/adapters/acquireFromPool.ts +73 -0
- package/src/db/adapters/memory-pool/client.ts +367 -0
- package/src/db/adapters/memory-pool/shared.ts +79 -0
- package/src/db/adapters/memory-pool/vfs.ts +293 -0
- package/src/db/adapters/memory-pool/worker.ts +53 -0
- package/src/db/adapters/options.ts +8 -0
- package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +71 -6
- package/src/db/adapters/wa-sqlite/StatementCache.ts +50 -0
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +17 -5
- package/src/worker/db/MultiDatabaseServer.ts +29 -21
- package/dist/worker/OPFSWriteAheadVFS-BzodSqNq.js +0 -2
- package/dist/worker/OPFSWriteAheadVFS-BzodSqNq.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WASQLiteOpenFactory.js","sourceRoot":"","sources":["../../../../src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAmC,MAAM,mBAAmB,CAAC;AAC1F,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAEnC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,2BAA2B,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAChF,OAAO,EAAE,cAAc,EAAwB,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAkB,MAAM,uBAAuB,CAAC;AAEvE,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AAC/F,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAoB,MAAM,2BAA2B,CAAC;AAOvG;;GAEG;AACH,MAAM,OAAO,mBAAmB;IACtB,OAAO,CAA4B;IACnC,MAAM,CAAkB;IAEhC,YAAY,OAAmC;QAC7C,IAAI,CAAC,OAAO,GAAG,yBAAyB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAEvD,kEAAkE;QAClE,MAAM,SAAS,GAAG,iBAAiB,GAAG,EAAE,CAAC;QACzC,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CAAC,sCAAsC,SAAS,GAAG,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC/B,CAAC;IAES,WAAW;QACnB,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM;QACJ,MAAM,EAAE,iBAAiB,EAAE,eAAe,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QAErE,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACvB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;oBACd,KAAK,EAAE,SAAS,CAAC,IAAI;oBACrB,OAAO,EAAE;;;4EAGyD;iBACnE,CAAC,CAAC;YACL,CAAC;YAED,OAAO,IAAI,YAAY,EAAE,CAAC;QAC5B,CAAC;QAED,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;gBACd,KAAK,EAAE,SAAS,CAAC,IAAI;gBACrB,OAAO,EAAE,uGAAuG;aACjH,CAAC,CAAC;QACL,CAAC;QAED,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,cAAc;QAClB,MAAM,
|
|
1
|
+
{"version":3,"file":"WASQLiteOpenFactory.js","sourceRoot":"","sources":["../../../../src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,13 +10,12 @@ export interface ConnectToMultiDatabaseServerOptions {
|
|
|
10
10
|
* Shared state to manage multiple database connections hosted by a worker.
|
|
11
11
|
*/
|
|
12
12
|
export declare class MultiDatabaseServer {
|
|
13
|
+
#private;
|
|
13
14
|
readonly logger: PowerSyncLogger;
|
|
14
|
-
private activeDatabases;
|
|
15
15
|
constructor(logger: PowerSyncLogger);
|
|
16
16
|
handleConnection({ logLevel, database, lockName }: ConnectToMultiDatabaseServerOptions): Promise<ClientConnectionView>;
|
|
17
17
|
connectToExisting(name: string, lockName: string): Promise<ClientConnectionView>;
|
|
18
18
|
openConnectionLocally(logger: PowerSyncLogger, options: RawWaSqliteDatabaseOptions, lockName?: string): Promise<ClientConnectionView>;
|
|
19
|
-
private databaseOpenAttempt;
|
|
20
19
|
closeAll(): Promise<void[]>;
|
|
21
20
|
}
|
|
22
21
|
export declare const isSharedWorker: boolean;
|
|
@@ -5,13 +5,15 @@ import { getNavigatorLocks } from '../../shared/navigator.js';
|
|
|
5
5
|
import { RawSqliteConnection } from '../../db/adapters/wa-sqlite/RawSqliteConnection.js';
|
|
6
6
|
import { ConcurrentSqliteConnection } from '../../db/adapters/wa-sqlite/ConcurrentConnection.js';
|
|
7
7
|
import { WASQLiteVFS } from '../../db/adapters/wa-sqlite/vfs.js';
|
|
8
|
+
import { Mutex } from '@powersync/shared-internals';
|
|
8
9
|
const OPEN_DB_LOCK = 'open-wasqlite-db';
|
|
9
10
|
/**
|
|
10
11
|
* Shared state to manage multiple database connections hosted by a worker.
|
|
11
12
|
*/
|
|
12
13
|
export class MultiDatabaseServer {
|
|
13
14
|
logger;
|
|
14
|
-
activeDatabases = new Map();
|
|
15
|
+
#activeDatabases = new Map();
|
|
16
|
+
#localOpenLock = new Mutex();
|
|
15
17
|
constructor(logger) {
|
|
16
18
|
this.logger = logger;
|
|
17
19
|
}
|
|
@@ -26,7 +28,7 @@ export class MultiDatabaseServer {
|
|
|
26
28
|
}
|
|
27
29
|
async connectToExisting(name, lockName) {
|
|
28
30
|
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
|
|
29
|
-
const server = this
|
|
31
|
+
const server = this.#activeDatabases.get(name);
|
|
30
32
|
if (server == null) {
|
|
31
33
|
throw new Error(`connectToExisting(${name}) failed because the worker doesn't own a database with that name.`);
|
|
32
34
|
}
|
|
@@ -40,7 +42,7 @@ export class MultiDatabaseServer {
|
|
|
40
42
|
let server;
|
|
41
43
|
for (let count = 0; count < maxAttempts - 1; count++) {
|
|
42
44
|
try {
|
|
43
|
-
server = await this
|
|
45
|
+
server = await this.#databaseOpenAttempt(logger, options);
|
|
44
46
|
}
|
|
45
47
|
catch (error) {
|
|
46
48
|
this.logger.log({
|
|
@@ -52,19 +54,20 @@ export class MultiDatabaseServer {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
// Final attempt if we haven't been able to open the server - rethrow errors if we still can't open.
|
|
55
|
-
server ??= await this
|
|
57
|
+
server ??= await this.#databaseOpenAttempt(logger, options);
|
|
56
58
|
return server.connect(lockName);
|
|
57
59
|
}
|
|
58
|
-
async databaseOpenAttempt(logger, options) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
async #databaseOpenAttempt(logger, options) {
|
|
61
|
+
const { filename, readonly, vfs } = options;
|
|
62
|
+
// We don't need navigator locks for shared workers because all queries run in this shared worker exclusively.
|
|
63
|
+
// For read-only connections, we use a VFS that supports concurrent reads (so a single lock on the connection is
|
|
64
|
+
// fine). In-memory databases either run in a shared worker or aren't shared across tabs at all, so the internal
|
|
65
|
+
// lock is enough.
|
|
66
|
+
const needsNavigatorLocks = !(isSharedWorker || readonly || vfs == WASQLiteVFS.InMemoryVfs);
|
|
67
|
+
const activeDatabases = this.#activeDatabases;
|
|
68
|
+
async function openDatabase() {
|
|
69
|
+
let server = activeDatabases.get(filename);
|
|
62
70
|
if (server == null) {
|
|
63
|
-
// We don't need navigator locks for shared workers because all queries run in this shared worker exclusively.
|
|
64
|
-
// For read-only connections, we use a VFS that supports concurrent reads (so a single lock on the connection is
|
|
65
|
-
// fine). In-memory databases either run in a shared worker or aren't shared across tabs at all, so the internal
|
|
66
|
-
// lock is enough.
|
|
67
|
-
const needsNavigatorLocks = !(isSharedWorker || readonly || vfs == WASQLiteVFS.InMemoryVfs);
|
|
68
71
|
const connection = new RawSqliteConnection(options);
|
|
69
72
|
const withSafeConcurrency = new ConcurrentSqliteConnection(connection, needsNavigatorLocks);
|
|
70
73
|
// Initializing the RawSqliteConnection will run some pragmas that might write to the database file, so we want
|
|
@@ -80,19 +83,27 @@ export class MultiDatabaseServer {
|
|
|
80
83
|
throw e;
|
|
81
84
|
}
|
|
82
85
|
returnLease();
|
|
83
|
-
const onClose = () =>
|
|
86
|
+
const onClose = () => activeDatabases.delete(filename);
|
|
84
87
|
server = new DatabaseServer({
|
|
85
88
|
inner: withSafeConcurrency,
|
|
86
89
|
logger,
|
|
87
90
|
onClose
|
|
88
91
|
});
|
|
89
|
-
|
|
92
|
+
activeDatabases.set(filename, server);
|
|
90
93
|
}
|
|
91
94
|
return server;
|
|
92
|
-
}
|
|
95
|
+
}
|
|
96
|
+
if (needsNavigatorLocks) {
|
|
97
|
+
return getNavigatorLocks().request(OPEN_DB_LOCK, openDatabase);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Even if we don't need navigator locks, this avoids a race between the activeDatabases.get() call, the async
|
|
101
|
+
// open logic and the final activeDatabases.set() step.
|
|
102
|
+
return this.#localOpenLock.runExclusive(openDatabase);
|
|
103
|
+
}
|
|
93
104
|
}
|
|
94
105
|
closeAll() {
|
|
95
|
-
const existingDatabases = [...this
|
|
106
|
+
const existingDatabases = [...this.#activeDatabases.values()];
|
|
96
107
|
return Promise.all(existingDatabases.map((db) => {
|
|
97
108
|
db.forceClose();
|
|
98
109
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiDatabaseServer.js","sourceRoot":"","sources":["../../../src/worker/db/MultiDatabaseServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAmB,MAAM,mBAAmB,CAAC;AAC/D,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,EAAwB,cAAc,EAAE,MAAM,+CAA+C,CAAC;AACrG,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAA8B,MAAM,oDAAoD,CAAC;AACrH,OAAO,EAAE,0BAA0B,EAAE,MAAM,qDAAqD,CAAC;AACjG,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"MultiDatabaseServer.js","sourceRoot":"","sources":["../../../src/worker/db/MultiDatabaseServer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/web",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "PowerSync Web SDK",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -23,12 +23,19 @@
|
|
|
23
23
|
"react-native": "./dist/index.react_native_web.js",
|
|
24
24
|
"default": "./lib/index.js"
|
|
25
25
|
},
|
|
26
|
+
"./extra/shared-memory-pool": {
|
|
27
|
+
"types": "./lib/db/adapters/memory-pool/client.d.ts",
|
|
28
|
+
"default": "./lib/db/adapters/memory-pool/client.js"
|
|
29
|
+
},
|
|
26
30
|
"./bundled_worker": {
|
|
27
31
|
"types": "./lib/worker/worker.d.ts",
|
|
28
32
|
"default": "./dist/worker/worker.js"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
|
-
"repository":
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/powersync-ja/powersync-js"
|
|
38
|
+
},
|
|
32
39
|
"bugs": {
|
|
33
40
|
"url": "https://github.com/powersync-ja/powersync-js/issues"
|
|
34
41
|
},
|
|
@@ -47,14 +54,14 @@
|
|
|
47
54
|
"author": "PowerSync",
|
|
48
55
|
"license": "Apache-2.0",
|
|
49
56
|
"peerDependencies": {
|
|
50
|
-
"@powersync/common": "^2.
|
|
57
|
+
"@powersync/common": "^2.1.0",
|
|
51
58
|
"@powersync/shared-internals": "^1.1.1"
|
|
52
59
|
},
|
|
53
60
|
"dependencies": {
|
|
54
|
-
"@journeyapps/wa-sqlite": "2.0.
|
|
61
|
+
"@journeyapps/wa-sqlite": "2.0.3",
|
|
55
62
|
"comlink": "^4.4.2",
|
|
56
63
|
"commander": "^12.1.0",
|
|
57
|
-
"@powersync/common": "2.
|
|
64
|
+
"@powersync/common": "2.1.0",
|
|
58
65
|
"@powersync/shared-internals": "1.1.1"
|
|
59
66
|
},
|
|
60
67
|
"devDependencies": {
|
|
@@ -78,6 +85,6 @@
|
|
|
78
85
|
"clean": "rm -rf lib dist tsconfig.tsbuildinfo",
|
|
79
86
|
"watch": "tsc --build -w",
|
|
80
87
|
"test": "pnpm build && vitest",
|
|
81
|
-
"test:exports": "attw --pack . --entrypoints . --profile=esm-only "
|
|
88
|
+
"test:exports": "attw --pack . --entrypoints . extra/shared-memory-pool --profile=esm-only "
|
|
82
89
|
}
|
|
83
90
|
}
|
|
@@ -172,27 +172,34 @@ export class WebPowerSyncDatabase extends BasePowerSyncDatabase<WebPowerSyncData
|
|
|
172
172
|
logger: this.logger
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const warning = `
|
|
175
|
+
if (this.resolvedOpenOptions.ssrMode) {
|
|
176
|
+
return new SSRStreamingSyncImplementation();
|
|
177
|
+
} else if (this.resolvedOpenOptions.enableMultiTabs) {
|
|
178
|
+
if (!this.enableBroadcastLogs) {
|
|
179
|
+
const warning = `
|
|
181
180
|
Multiple tabs are enabled, but broadcasting of logs is disabled.
|
|
182
181
|
Logs for shared sync worker will only be available in the shared worker context
|
|
183
182
|
`;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
const logger = this.options.logger;
|
|
184
|
+
logger ? logger.log({ level: LogLevels.warn, message: warning }) : console.warn(warning);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if ('shareConnection' in this.database) {
|
|
187
188
|
return new SharedWebStreamingSyncImplementation({
|
|
188
189
|
...syncOptions,
|
|
189
190
|
db: this.database as WebDBAdapter, // This should always be the case
|
|
190
191
|
logLevel: this.options.sync?.logLevel ?? LogLevels.info,
|
|
191
192
|
enableBroadcastLogs: this.enableBroadcastLogs
|
|
192
193
|
});
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.logger.log({
|
|
197
|
+
level: LogLevels.warn,
|
|
198
|
+
message: "Not using a shared sync worker because the database adapter doesn't support it."
|
|
199
|
+
});
|
|
195
200
|
}
|
|
201
|
+
|
|
202
|
+
return new TabLocalStreamingSyncImplementation(syncOptions);
|
|
196
203
|
}
|
|
197
204
|
}
|
|
198
205
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DBAdapter, DBAdapterListener, DBLockOptions, LockContext } from '@powersync/common';
|
|
2
|
-
import { Mutex, Semaphore
|
|
3
|
-
import { SharedConnectionWorker,
|
|
2
|
+
import { Mutex, Semaphore } from '@powersync/shared-internals';
|
|
3
|
+
import { SharedConnectionWorker, WebDBAdapterConfiguration } from './WebDBAdapter.js';
|
|
4
4
|
import { DatabaseClient } from './wa-sqlite/DatabaseClient.js';
|
|
5
|
+
import { acquireFromPool } from './acquireFromPool.js';
|
|
5
6
|
|
|
6
7
|
type PendingListener = { listener: Partial<DBAdapterListener>; closeAfterRegisteredOnResolvedPool?: () => void };
|
|
7
8
|
|
|
@@ -126,65 +127,16 @@ function readWritePoolState(writer: DatabaseClient, readers: DatabaseClient[]):
|
|
|
126
127
|
return {
|
|
127
128
|
writer,
|
|
128
129
|
async withConnection(allowReadOnly, fn, options) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (allowReadOnly) {
|
|
140
|
-
let connection: DatabaseClient;
|
|
141
|
-
|
|
142
|
-
// Even if we have a pool of read connections, it's typically very small and we assume that most queries are
|
|
143
|
-
// reads. So, we want to request any connection from the read pool and the dedicated write connection (which
|
|
144
|
-
// can also serve reads). We race for the first connection we can obtain this way, and then abort the other
|
|
145
|
-
// request.
|
|
146
|
-
[connection, release] = await new Promise<[DatabaseClient, UnlockFn]>((resolve, reject) => {
|
|
147
|
-
let didComplete = false;
|
|
148
|
-
function complete() {
|
|
149
|
-
didComplete = true;
|
|
150
|
-
abortController.abort();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function completeSuccess(connection: DatabaseClient, returnFn: UnlockFn) {
|
|
154
|
-
if (didComplete) {
|
|
155
|
-
// We're not going to use this connection, so return it immediately.
|
|
156
|
-
returnFn();
|
|
157
|
-
} else {
|
|
158
|
-
complete();
|
|
159
|
-
resolve([connection, returnFn]);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function completeError(error: unknown) {
|
|
164
|
-
// We either have a working connection already, or we've rejected the promise. Either way, we don't need
|
|
165
|
-
// to do either thing again.
|
|
166
|
-
if (didComplete) return;
|
|
167
|
-
|
|
168
|
-
complete();
|
|
169
|
-
reject(error);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
writerMutex.acquire(abortSignal).then((unlock) => completeSuccess(writer, unlock), completeError);
|
|
173
|
-
readerSemaphore
|
|
174
|
-
.requestOne(abortSignal)
|
|
175
|
-
.then(({ item, release }) => completeSuccess(item, release), completeError);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return await connection.readLock(fn);
|
|
179
|
-
} else {
|
|
180
|
-
return await writerMutex.runExclusive(() => writer.writeLock(fn), abortSignal);
|
|
181
|
-
}
|
|
182
|
-
} finally {
|
|
183
|
-
if (timeout != null) {
|
|
184
|
-
clearTimeout(timeout);
|
|
185
|
-
}
|
|
186
|
-
release?.();
|
|
187
|
-
}
|
|
130
|
+
return acquireFromPool(
|
|
131
|
+
writerMutex,
|
|
132
|
+
writer,
|
|
133
|
+
readerSemaphore,
|
|
134
|
+
(connection) => {
|
|
135
|
+
return allowReadOnly ? connection.readLock(fn) : connection.writeLock(fn);
|
|
136
|
+
},
|
|
137
|
+
options,
|
|
138
|
+
allowReadOnly
|
|
139
|
+
);
|
|
188
140
|
},
|
|
189
141
|
async close() {
|
|
190
142
|
await writer.close();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { DBLockOptions } from '@powersync/common';
|
|
2
|
+
import type { Mutex, Semaphore, UnlockFn } from '@powersync/shared-internals';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal helper function to acquire a connection from a pool that has a designated writer, additional readers, and
|
|
6
|
+
* also allows dispatching reads to the writer.
|
|
7
|
+
*/
|
|
8
|
+
export async function acquireFromPool<Connection, Res>(
|
|
9
|
+
writerMutex: Mutex,
|
|
10
|
+
writer: Connection,
|
|
11
|
+
readers: Semaphore<Connection> | undefined,
|
|
12
|
+
callback: (connection: Connection) => Promise<Res>,
|
|
13
|
+
options: DBLockOptions | undefined,
|
|
14
|
+
allowReadOnly: boolean
|
|
15
|
+
): Promise<Res> {
|
|
16
|
+
const abortController = new AbortController();
|
|
17
|
+
const abortSignal = abortController.signal;
|
|
18
|
+
|
|
19
|
+
let timeout: any = null;
|
|
20
|
+
let release: UnlockFn | undefined;
|
|
21
|
+
if (options?.timeoutMs) {
|
|
22
|
+
timeout = setTimeout(() => abortController.abort('requesting database timed out'), options.timeoutMs);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (allowReadOnly) {
|
|
27
|
+
let connection: Connection;
|
|
28
|
+
|
|
29
|
+
// Even if we have a pool of read connections, it's typically very small and we assume that most queries are
|
|
30
|
+
// reads. So, we want to request any connection from the read pool and the dedicated write connection (which
|
|
31
|
+
// can also serve reads). We race for the first connection we can obtain this way, and then abort the other
|
|
32
|
+
// request.
|
|
33
|
+
[connection, release] = await new Promise<[Connection, UnlockFn]>((resolve, reject) => {
|
|
34
|
+
let didComplete = false;
|
|
35
|
+
function complete() {
|
|
36
|
+
didComplete = true;
|
|
37
|
+
abortController.abort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function completeSuccess(connection: Connection, returnFn: UnlockFn) {
|
|
41
|
+
if (didComplete) {
|
|
42
|
+
// We're not going to use this connection, so return it immediately.
|
|
43
|
+
returnFn();
|
|
44
|
+
} else {
|
|
45
|
+
complete();
|
|
46
|
+
resolve([connection, returnFn]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function completeError(error: unknown) {
|
|
51
|
+
// We either have a working connection already, or we've rejected the promise. Either way, we don't need
|
|
52
|
+
// to do either thing again.
|
|
53
|
+
if (didComplete) return;
|
|
54
|
+
|
|
55
|
+
complete();
|
|
56
|
+
reject(error);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
writerMutex.acquire(abortSignal).then((unlock) => completeSuccess(writer, unlock), completeError);
|
|
60
|
+
readers?.requestOne(abortSignal).then(({ item, release }) => completeSuccess(item, release), completeError);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return await callback(connection);
|
|
64
|
+
} else {
|
|
65
|
+
return await writerMutex.runExclusive(() => callback(writer), abortSignal);
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
if (timeout != null) {
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
}
|
|
71
|
+
release?.();
|
|
72
|
+
}
|
|
73
|
+
}
|