@powersync/web 1.36.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.
- package/dist/index.umd.js +1127 -1235
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +481 -3111
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +688 -836
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/lib/package.json +2 -3
- package/lib/src/db/PowerSyncDatabase.d.ts +1 -2
- package/lib/src/db/PowerSyncDatabase.js +3 -4
- package/lib/src/db/adapters/AsyncWebAdapter.d.ts +40 -0
- package/lib/src/db/adapters/AsyncWebAdapter.js +69 -0
- package/lib/src/db/adapters/SSRDBAdapter.d.ts +1 -2
- package/lib/src/db/adapters/SSRDBAdapter.js +5 -6
- package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.d.ts +56 -0
- package/lib/src/db/adapters/wa-sqlite/ConcurrentConnection.js +121 -0
- package/lib/src/db/adapters/wa-sqlite/DatabaseClient.d.ts +54 -0
- package/lib/src/db/adapters/wa-sqlite/DatabaseClient.js +227 -0
- package/lib/src/db/adapters/wa-sqlite/DatabaseServer.d.ts +47 -0
- package/lib/src/db/adapters/wa-sqlite/DatabaseServer.js +146 -0
- package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.d.ts +46 -0
- package/lib/src/db/adapters/wa-sqlite/RawSqliteConnection.js +147 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.d.ts +14 -6
- package/lib/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.js +66 -39
- package/lib/src/db/adapters/wa-sqlite/vfs.d.ts +61 -0
- package/lib/src/db/adapters/wa-sqlite/vfs.js +91 -0
- package/lib/src/db/adapters/web-sql-flags.d.ts +5 -0
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.d.ts +1 -2
- package/lib/src/db/sync/SSRWebStreamingSyncImplementation.js +2 -3
- package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +4 -19
- package/lib/src/index.d.ts +1 -4
- package/lib/src/index.js +1 -4
- package/lib/src/shared/tab_close_signal.d.ts +11 -0
- package/lib/src/shared/tab_close_signal.js +26 -0
- package/lib/src/worker/db/MultiDatabaseServer.d.ts +17 -0
- package/lib/src/worker/db/MultiDatabaseServer.js +86 -0
- package/lib/src/worker/db/WASQLiteDB.worker.js +9 -48
- package/lib/src/worker/db/open-worker-database.d.ts +3 -3
- package/lib/src/worker/db/open-worker-database.js +1 -1
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +5 -6
- package/lib/src/worker/sync/SharedSyncImplementation.js +92 -54
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/src/db/PowerSyncDatabase.ts +3 -3
- package/src/db/adapters/AsyncWebAdapter.ts +91 -0
- package/src/db/adapters/SSRDBAdapter.ts +7 -7
- package/src/db/adapters/wa-sqlite/ConcurrentConnection.ts +137 -0
- package/src/db/adapters/wa-sqlite/DatabaseClient.ts +325 -0
- package/src/db/adapters/wa-sqlite/DatabaseServer.ts +201 -0
- package/src/db/adapters/wa-sqlite/RawSqliteConnection.ts +191 -0
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +87 -43
- package/src/db/adapters/wa-sqlite/vfs.ts +112 -0
- package/src/db/adapters/web-sql-flags.ts +6 -0
- package/src/db/sync/SSRWebStreamingSyncImplementation.ts +2 -3
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +4 -20
- package/src/index.ts +1 -4
- package/src/shared/tab_close_signal.ts +28 -0
- package/src/worker/db/MultiDatabaseServer.ts +104 -0
- package/src/worker/db/WASQLiteDB.worker.ts +10 -57
- package/src/worker/db/open-worker-database.ts +3 -3
- package/src/worker/sync/SharedSyncImplementation.ts +118 -58
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js +0 -1881
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-89f0ba.index.umd.js.map +0 -1
- package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js +0 -555
- package/dist/_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapps_wa-sqlite_src_example-97ebe9.index.umd.js.map +0 -1
- package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +0 -17
- package/lib/src/db/adapters/AbstractWebSQLOpenFactory.js +0 -33
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +0 -49
- package/lib/src/db/adapters/AsyncDatabaseConnection.js +0 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +0 -109
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +0 -404
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +0 -59
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +0 -147
- package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.d.ts +0 -12
- package/lib/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.js +0 -19
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +0 -155
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +0 -401
- package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +0 -32
- package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +0 -49
- package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +0 -42
- package/lib/src/worker/db/SharedWASQLiteConnection.js +0 -90
- package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +0 -9
- package/lib/src/worker/db/WorkerWASQLiteConnection.js +0 -12
- package/src/db/adapters/AbstractWebSQLOpenFactory.ts +0 -48
- package/src/db/adapters/AsyncDatabaseConnection.ts +0 -55
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +0 -489
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +0 -201
- package/src/db/adapters/wa-sqlite/InternalWASQLiteDBAdapter.ts +0 -23
- package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +0 -497
- package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +0 -86
- package/src/worker/db/SharedWASQLiteConnection.ts +0 -131
- package/src/worker/db/WorkerWASQLiteConnection.ts +0 -14
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ILogger } from '@powersync/common';
|
|
2
|
+
import { ConcurrentSqliteConnection } from './ConcurrentConnection.js';
|
|
3
|
+
import { RawQueryResult } from './RawSqliteConnection.js';
|
|
4
|
+
export interface DatabaseServerOptions {
|
|
5
|
+
inner: ConcurrentSqliteConnection;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
logger: ILogger;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Access to a WA-sqlite connection that can be shared with multiple clients sending queries over an RPC protocol built
|
|
11
|
+
* with the Comlink package.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DatabaseServer {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(options: DatabaseServerOptions);
|
|
16
|
+
/**
|
|
17
|
+
* Called by clients when they wish to connect to this database.
|
|
18
|
+
*
|
|
19
|
+
* @param lockName A lock that is currently held by the client. When the lock is returned, we know the client is gone
|
|
20
|
+
* and that we need to clean up resources.
|
|
21
|
+
*/
|
|
22
|
+
connect(lockName?: string): Promise<ClientConnectionView>;
|
|
23
|
+
forceClose(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface ClientConnectionView {
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Only used for testing purposes.
|
|
29
|
+
*/
|
|
30
|
+
debugIsAutoCommit(): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Requests exclusive access to this database connection.
|
|
33
|
+
*
|
|
34
|
+
* Returns a token that can be used with the query methods. It must be returned with {@link completeAccess} to
|
|
35
|
+
* give other clients access to the database afterwards.
|
|
36
|
+
*/
|
|
37
|
+
requestAccess(write: boolean, timeoutMs?: number): Promise<string>;
|
|
38
|
+
execute(token: string, sql: string, params: any[] | undefined): Promise<RawQueryResult>;
|
|
39
|
+
executeBatch(token: string, sql: string, params: any[][]): Promise<RawQueryResult[]>;
|
|
40
|
+
completeAccess(token: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Sends update notifications to the given message port.
|
|
43
|
+
*
|
|
44
|
+
* Update notifications are posted as a `string[]` message.
|
|
45
|
+
*/
|
|
46
|
+
setUpdateListener(listener: MessagePort): Promise<void>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access to a WA-sqlite connection that can be shared with multiple clients sending queries over an RPC protocol built
|
|
3
|
+
* with the Comlink package.
|
|
4
|
+
*/
|
|
5
|
+
export class DatabaseServer {
|
|
6
|
+
#options;
|
|
7
|
+
#nextClientId = 0;
|
|
8
|
+
#activeClients = new Set();
|
|
9
|
+
// TODO: Don't use a broadcast channel for connections managed by a shared worker.
|
|
10
|
+
#updateBroadcastChannel;
|
|
11
|
+
#clientTableListeners = new Set();
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.#options = options;
|
|
14
|
+
const inner = options.inner;
|
|
15
|
+
this.#updateBroadcastChannel = new BroadcastChannel(`${inner.options.dbFilename}-table-updates`);
|
|
16
|
+
this.#updateBroadcastChannel.onmessage = ({ data }) => {
|
|
17
|
+
this.#pushTableUpdateToClients(data);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
#pushTableUpdateToClients(changedTables) {
|
|
21
|
+
for (const listener of this.#clientTableListeners) {
|
|
22
|
+
listener.postMessage(changedTables);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
get #inner() {
|
|
26
|
+
return this.#options.inner;
|
|
27
|
+
}
|
|
28
|
+
get #logger() {
|
|
29
|
+
return this.#options.logger;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Called by clients when they wish to connect to this database.
|
|
33
|
+
*
|
|
34
|
+
* @param lockName A lock that is currently held by the client. When the lock is returned, we know the client is gone
|
|
35
|
+
* and that we need to clean up resources.
|
|
36
|
+
*/
|
|
37
|
+
async connect(lockName) {
|
|
38
|
+
let isOpen = true;
|
|
39
|
+
const clientId = this.#nextClientId++;
|
|
40
|
+
this.#activeClients.add(clientId);
|
|
41
|
+
let connectionLeases = new Map();
|
|
42
|
+
let currentTableListener;
|
|
43
|
+
function requireOpen() {
|
|
44
|
+
if (!isOpen) {
|
|
45
|
+
throw new Error('Client has already been closed');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function requireOpenAndLease(lease) {
|
|
49
|
+
requireOpen();
|
|
50
|
+
const token = connectionLeases.get(lease);
|
|
51
|
+
if (!token) {
|
|
52
|
+
throw new Error('Attempted to use a connection lease that has already been returned.');
|
|
53
|
+
}
|
|
54
|
+
return token;
|
|
55
|
+
}
|
|
56
|
+
const close = async () => {
|
|
57
|
+
if (isOpen) {
|
|
58
|
+
isOpen = false;
|
|
59
|
+
if (currentTableListener) {
|
|
60
|
+
this.#clientTableListeners.delete(currentTableListener);
|
|
61
|
+
}
|
|
62
|
+
// If the client holds a connection lease it hasn't returned, return that now.
|
|
63
|
+
for (const { lease } of connectionLeases.values()) {
|
|
64
|
+
this.#logger.debug(`Closing connection lease that hasn't been returned.`);
|
|
65
|
+
await lease.returnLease();
|
|
66
|
+
}
|
|
67
|
+
this.#activeClients.delete(clientId);
|
|
68
|
+
if (this.#activeClients.size == 0) {
|
|
69
|
+
await this.forceClose();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.#logger.debug('Keeping underlying connection active since its used by other clients.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
if (lockName) {
|
|
77
|
+
navigator.locks.request(lockName, {}, () => {
|
|
78
|
+
close();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
close,
|
|
83
|
+
debugIsAutoCommit: async () => {
|
|
84
|
+
return this.#inner.unsafeUseInner().isAutoCommit();
|
|
85
|
+
},
|
|
86
|
+
requestAccess: async (write, timeoutMs) => {
|
|
87
|
+
requireOpen();
|
|
88
|
+
// TODO: Support timeouts, they don't seem to be supported by the async-mutex package.
|
|
89
|
+
const lease = await this.#inner.acquireConnection();
|
|
90
|
+
if (!isOpen) {
|
|
91
|
+
// Race between requestAccess and close(), the connection was closed while we tried to acquire a lease.
|
|
92
|
+
await lease.returnLease();
|
|
93
|
+
return requireOpen();
|
|
94
|
+
}
|
|
95
|
+
const token = crypto.randomUUID();
|
|
96
|
+
connectionLeases.set(token, { lease, write });
|
|
97
|
+
return token;
|
|
98
|
+
},
|
|
99
|
+
completeAccess: async (token) => {
|
|
100
|
+
const lease = requireOpenAndLease(token);
|
|
101
|
+
connectionLeases.delete(token);
|
|
102
|
+
try {
|
|
103
|
+
if (lease.write) {
|
|
104
|
+
// Collect update hooks invoked while the client had the write connection.
|
|
105
|
+
const { resultSet } = await lease.lease.use((conn) => conn.execute(`SELECT powersync_update_hooks('get')`));
|
|
106
|
+
if (resultSet) {
|
|
107
|
+
const updatedTables = JSON.parse(resultSet.rows[0][0]);
|
|
108
|
+
if (updatedTables.length) {
|
|
109
|
+
this.#updateBroadcastChannel.postMessage(updatedTables);
|
|
110
|
+
this.#pushTableUpdateToClients(updatedTables);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await lease.lease.returnLease();
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
execute: async (token, sql, params) => {
|
|
120
|
+
const { lease } = requireOpenAndLease(token);
|
|
121
|
+
return await lease.use((db) => db.execute(sql, params));
|
|
122
|
+
},
|
|
123
|
+
executeBatch: async (token, sql, params) => {
|
|
124
|
+
const { lease } = requireOpenAndLease(token);
|
|
125
|
+
return await lease.use((db) => db.executeBatch(sql, params));
|
|
126
|
+
},
|
|
127
|
+
setUpdateListener: async (listener) => {
|
|
128
|
+
requireOpen();
|
|
129
|
+
if (currentTableListener) {
|
|
130
|
+
this.#clientTableListeners.delete(currentTableListener);
|
|
131
|
+
}
|
|
132
|
+
currentTableListener = listener;
|
|
133
|
+
if (listener) {
|
|
134
|
+
this.#clientTableListeners.add(listener);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async forceClose() {
|
|
140
|
+
this.#logger.debug(`Closing connection to ${this.#inner.options}.`);
|
|
141
|
+
const connection = this.#inner;
|
|
142
|
+
this.#options.onClose();
|
|
143
|
+
this.#updateBroadcastChannel.close();
|
|
144
|
+
await connection.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory.js';
|
|
2
|
+
export interface RawResultSet {
|
|
3
|
+
columns: string[];
|
|
4
|
+
rows: SQLiteCompatibleType[][];
|
|
5
|
+
}
|
|
6
|
+
export interface RawQueryResult {
|
|
7
|
+
changes: number;
|
|
8
|
+
lastInsertRowId: number;
|
|
9
|
+
autocommit: boolean;
|
|
10
|
+
resultSet: RawResultSet | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A small wrapper around WA-sqlite to help with opening databases and running statements by preparing them internally.
|
|
14
|
+
*
|
|
15
|
+
* This is an internal class, and it must never be used directly. Wrappers are required to ensure raw connections aren't
|
|
16
|
+
* used concurrently across tabs.
|
|
17
|
+
*/
|
|
18
|
+
export declare class RawSqliteConnection {
|
|
19
|
+
readonly options: ResolvedWASQLiteOpenFactoryOptions;
|
|
20
|
+
private _sqliteAPI;
|
|
21
|
+
/**
|
|
22
|
+
* The `sqlite3*` connection pointer.
|
|
23
|
+
*/
|
|
24
|
+
private db;
|
|
25
|
+
private _moduleFactory;
|
|
26
|
+
constructor(options: ResolvedWASQLiteOpenFactoryOptions);
|
|
27
|
+
get isOpen(): boolean;
|
|
28
|
+
init(): Promise<void>;
|
|
29
|
+
private openSQLiteAPI;
|
|
30
|
+
requireSqlite(): SQLiteAPI;
|
|
31
|
+
/**
|
|
32
|
+
* Checks if the database connection is in autocommit mode.
|
|
33
|
+
* @returns true if in autocommit mode, false if in a transaction
|
|
34
|
+
*/
|
|
35
|
+
isAutoCommit(): boolean;
|
|
36
|
+
execute(sql: string, bindings?: any[]): Promise<RawQueryResult>;
|
|
37
|
+
executeBatch(sql: string, bindings: any[][]): Promise<RawQueryResult[]>;
|
|
38
|
+
private wrapQueryResults;
|
|
39
|
+
/**
|
|
40
|
+
* This executes a single statement using SQLite3 and returns the results as a {@link RawResultSet}.
|
|
41
|
+
*/
|
|
42
|
+
private executeSingleStatementRaw;
|
|
43
|
+
executeRaw(sql: string, bindings?: any[]): Promise<RawResultSet[]>;
|
|
44
|
+
private stepThroughStatement;
|
|
45
|
+
close(): Promise<void>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Factory as WaSqliteFactory, SQLITE_ROW } from '@journeyapps/wa-sqlite';
|
|
2
|
+
import { DEFAULT_MODULE_FACTORIES } from './vfs.js';
|
|
3
|
+
/**
|
|
4
|
+
* A small wrapper around WA-sqlite to help with opening databases and running statements by preparing them internally.
|
|
5
|
+
*
|
|
6
|
+
* This is an internal class, and it must never be used directly. Wrappers are required to ensure raw connections aren't
|
|
7
|
+
* used concurrently across tabs.
|
|
8
|
+
*/
|
|
9
|
+
export class RawSqliteConnection {
|
|
10
|
+
options;
|
|
11
|
+
_sqliteAPI = null;
|
|
12
|
+
/**
|
|
13
|
+
* The `sqlite3*` connection pointer.
|
|
14
|
+
*/
|
|
15
|
+
db = 0;
|
|
16
|
+
_moduleFactory;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
this._moduleFactory = DEFAULT_MODULE_FACTORIES[this.options.vfs];
|
|
20
|
+
}
|
|
21
|
+
get isOpen() {
|
|
22
|
+
return this.db != 0;
|
|
23
|
+
}
|
|
24
|
+
async init() {
|
|
25
|
+
const api = (this._sqliteAPI = await this.openSQLiteAPI());
|
|
26
|
+
this.db = await api.open_v2(this.options.dbFilename);
|
|
27
|
+
await this.executeRaw(`PRAGMA temp_store = ${this.options.temporaryStorage};`);
|
|
28
|
+
if (this.options.encryptionKey) {
|
|
29
|
+
const escapedKey = this.options.encryptionKey.replace("'", "''");
|
|
30
|
+
await this.executeRaw(`PRAGMA key = '${escapedKey}'`);
|
|
31
|
+
}
|
|
32
|
+
await this.executeRaw(`PRAGMA cache_size = -${this.options.cacheSizeKb};`);
|
|
33
|
+
await this.executeRaw(`SELECT powersync_update_hooks('install');`);
|
|
34
|
+
}
|
|
35
|
+
async openSQLiteAPI() {
|
|
36
|
+
const { module, vfs } = await this._moduleFactory({
|
|
37
|
+
dbFileName: this.options.dbFilename,
|
|
38
|
+
encryptionKey: this.options.encryptionKey
|
|
39
|
+
});
|
|
40
|
+
const sqlite3 = WaSqliteFactory(module);
|
|
41
|
+
sqlite3.vfs_register(vfs, true);
|
|
42
|
+
/**
|
|
43
|
+
* Register the PowerSync core SQLite extension
|
|
44
|
+
*/
|
|
45
|
+
module.ccall('powersync_init_static', 'int', []);
|
|
46
|
+
/**
|
|
47
|
+
* Create the multiple cipher vfs if an encryption key is provided
|
|
48
|
+
*/
|
|
49
|
+
if (this.options.encryptionKey) {
|
|
50
|
+
const createResult = module.ccall('sqlite3mc_vfs_create', 'int', ['string', 'int'], [this.options.dbFilename, 1]);
|
|
51
|
+
if (createResult !== 0) {
|
|
52
|
+
throw new Error('Failed to create multiple cipher vfs, Database encryption will not work');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return sqlite3;
|
|
56
|
+
}
|
|
57
|
+
requireSqlite() {
|
|
58
|
+
if (!this._sqliteAPI) {
|
|
59
|
+
throw new Error(`Initialization has not completed`);
|
|
60
|
+
}
|
|
61
|
+
return this._sqliteAPI;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Checks if the database connection is in autocommit mode.
|
|
65
|
+
* @returns true if in autocommit mode, false if in a transaction
|
|
66
|
+
*/
|
|
67
|
+
isAutoCommit() {
|
|
68
|
+
return this.requireSqlite().get_autocommit(this.db) != 0;
|
|
69
|
+
}
|
|
70
|
+
async execute(sql, bindings) {
|
|
71
|
+
const resultSet = await this.executeSingleStatementRaw(sql, bindings);
|
|
72
|
+
return this.wrapQueryResults(this.requireSqlite(), resultSet);
|
|
73
|
+
}
|
|
74
|
+
async executeBatch(sql, bindings) {
|
|
75
|
+
const results = [];
|
|
76
|
+
const api = this.requireSqlite();
|
|
77
|
+
for await (const stmt of api.statements(this.db, sql)) {
|
|
78
|
+
let columns;
|
|
79
|
+
for (const parameterSet of bindings) {
|
|
80
|
+
const rs = await this.stepThroughStatement(api, stmt, parameterSet, columns, false);
|
|
81
|
+
results.push(this.wrapQueryResults(api, rs));
|
|
82
|
+
}
|
|
83
|
+
// executeBatch can only use a single statement
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
wrapQueryResults(api, rs) {
|
|
89
|
+
return {
|
|
90
|
+
changes: api.changes(this.db),
|
|
91
|
+
lastInsertRowId: api.last_insert_id(this.db),
|
|
92
|
+
autocommit: api.get_autocommit(this.db) != 0,
|
|
93
|
+
resultSet: rs
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* This executes a single statement using SQLite3 and returns the results as a {@link RawResultSet}.
|
|
98
|
+
*/
|
|
99
|
+
async executeSingleStatementRaw(sql, bindings) {
|
|
100
|
+
const results = await this.executeRaw(sql, bindings);
|
|
101
|
+
return results.length ? results[0] : undefined;
|
|
102
|
+
}
|
|
103
|
+
async executeRaw(sql, bindings) {
|
|
104
|
+
const results = [];
|
|
105
|
+
const api = this.requireSqlite();
|
|
106
|
+
for await (const stmt of api.statements(this.db, sql)) {
|
|
107
|
+
let columns;
|
|
108
|
+
const rs = await this.stepThroughStatement(api, stmt, bindings ?? [], columns);
|
|
109
|
+
columns = rs.columns;
|
|
110
|
+
if (columns.length) {
|
|
111
|
+
results.push(rs);
|
|
112
|
+
}
|
|
113
|
+
// When binding parameters, only a single statement is executed.
|
|
114
|
+
if (bindings) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
async stepThroughStatement(api, stmt, bindings, knownColumns, includeResults = true) {
|
|
121
|
+
// TODO not sure why this is needed currently, but booleans break
|
|
122
|
+
bindings.forEach((b, index, arr) => {
|
|
123
|
+
if (typeof b == 'boolean') {
|
|
124
|
+
arr[index] = b ? 1 : 0;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
api.reset(stmt);
|
|
128
|
+
if (bindings) {
|
|
129
|
+
api.bind_collection(stmt, bindings);
|
|
130
|
+
}
|
|
131
|
+
const rows = [];
|
|
132
|
+
while ((await api.step(stmt)) === SQLITE_ROW) {
|
|
133
|
+
if (includeResults) {
|
|
134
|
+
const row = api.row(stmt);
|
|
135
|
+
rows.push(row);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
knownColumns ??= api.column_names(stmt);
|
|
139
|
+
return { columns: knownColumns, rows };
|
|
140
|
+
}
|
|
141
|
+
async close() {
|
|
142
|
+
if (this.isOpen) {
|
|
143
|
+
await this.requireSqlite().close(this.db);
|
|
144
|
+
this.db = 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { DBAdapter, type ILogLevel } from '@powersync/common';
|
|
2
|
-
import { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory.js';
|
|
3
|
-
import { AsyncDatabaseConnection } from '../AsyncDatabaseConnection.js';
|
|
1
|
+
import { DBAdapter, SQLOpenFactory, type ILogLevel } from '@powersync/common';
|
|
4
2
|
import { ResolvedWebSQLOpenOptions, WebSQLOpenFactoryOptions } from '../web-sql-flags.js';
|
|
5
|
-
import { WASQLiteVFS } from './
|
|
3
|
+
import { WASQLiteVFS } from './vfs.js';
|
|
4
|
+
import { DatabaseClient } from './DatabaseClient.js';
|
|
6
5
|
export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions {
|
|
7
6
|
vfs?: WASQLiteVFS;
|
|
8
7
|
}
|
|
@@ -11,13 +10,22 @@ export interface ResolvedWASQLiteOpenFactoryOptions extends ResolvedWebSQLOpenOp
|
|
|
11
10
|
}
|
|
12
11
|
export interface WorkerDBOpenerOptions extends ResolvedWASQLiteOpenFactoryOptions {
|
|
13
12
|
logLevel: ILogLevel;
|
|
13
|
+
/**
|
|
14
|
+
* A lock that is currently held by the client. When the lock is returned, we know the client is gone and that we need
|
|
15
|
+
* to clean up resources.
|
|
16
|
+
*/
|
|
17
|
+
lockName: string;
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
20
|
* Opens a SQLite connection using WA-SQLite.
|
|
17
21
|
*/
|
|
18
|
-
export declare class WASQLiteOpenFactory
|
|
22
|
+
export declare class WASQLiteOpenFactory implements SQLOpenFactory {
|
|
23
|
+
private options;
|
|
24
|
+
private resolvedFlags;
|
|
25
|
+
private logger;
|
|
19
26
|
constructor(options: WASQLiteOpenFactoryOptions);
|
|
20
27
|
get waOptions(): WASQLiteOpenFactoryOptions;
|
|
21
28
|
protected openAdapter(): DBAdapter;
|
|
22
|
-
|
|
29
|
+
openDB(): DBAdapter;
|
|
30
|
+
openConnection(): Promise<DatabaseClient>;
|
|
23
31
|
}
|
|
@@ -1,29 +1,48 @@
|
|
|
1
|
+
import { createLogger } from '@powersync/common';
|
|
1
2
|
import * as Comlink from 'comlink';
|
|
2
3
|
import { openWorkerDatabasePort, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { DEFAULT_CACHE_SIZE_KB, isServerSide, resolveWebSQLFlags, TemporaryStorageOption } from '../web-sql-flags.js';
|
|
5
|
+
import { SSRDBAdapter } from '../SSRDBAdapter.js';
|
|
6
|
+
import { vfsRequiresDedicatedWorkers, WASQLiteVFS } from './vfs.js';
|
|
7
|
+
import { MultiDatabaseServer } from '../../../worker/db/MultiDatabaseServer.js';
|
|
8
|
+
import { DatabaseClient } from './DatabaseClient.js';
|
|
9
|
+
import { generateTabCloseSignal } from '../../../shared/tab_close_signal.js';
|
|
10
|
+
import { AsyncDbAdapter } from '../AsyncWebAdapter.js';
|
|
8
11
|
/**
|
|
9
12
|
* Opens a SQLite connection using WA-SQLite.
|
|
10
13
|
*/
|
|
11
|
-
export class WASQLiteOpenFactory
|
|
14
|
+
export class WASQLiteOpenFactory {
|
|
15
|
+
options;
|
|
16
|
+
resolvedFlags;
|
|
17
|
+
logger;
|
|
12
18
|
constructor(options) {
|
|
13
|
-
|
|
19
|
+
this.options = options;
|
|
14
20
|
assertValidWASQLiteOpenFactoryOptions(options);
|
|
21
|
+
this.resolvedFlags = resolveWebSQLFlags(options.flags);
|
|
22
|
+
this.logger = options.logger ?? createLogger(`WASQLiteOpenFactory - ${this.options.dbFilename}`);
|
|
15
23
|
}
|
|
16
24
|
get waOptions() {
|
|
17
25
|
// Cast to extended type
|
|
18
26
|
return this.options;
|
|
19
27
|
}
|
|
20
28
|
openAdapter() {
|
|
21
|
-
return new
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
return new AsyncDbAdapter(this.openConnection(), this.options.dbFilename);
|
|
30
|
+
}
|
|
31
|
+
openDB() {
|
|
32
|
+
const { resolvedFlags: { disableSSRWarning, enableMultiTabs, ssrMode = isServerSide() } } = this;
|
|
33
|
+
if (ssrMode) {
|
|
34
|
+
if (!disableSSRWarning) {
|
|
35
|
+
this.logger.warn(`
|
|
36
|
+
Running PowerSync in SSR mode.
|
|
37
|
+
Only empty query results will be returned.
|
|
38
|
+
Disable this warning by setting 'disableSSRWarning: true' in options.`);
|
|
39
|
+
}
|
|
40
|
+
return new SSRDBAdapter();
|
|
41
|
+
}
|
|
42
|
+
if (!enableMultiTabs) {
|
|
43
|
+
this.logger.warn('Multiple tab support is not enabled. Using this site across multiple tabs may not function correctly.');
|
|
44
|
+
}
|
|
45
|
+
return this.openAdapter();
|
|
27
46
|
}
|
|
28
47
|
async openConnection() {
|
|
29
48
|
const { enableMultiTabs, useWebWorker } = this.resolvedFlags;
|
|
@@ -31,6 +50,18 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
|
|
|
31
50
|
if (!enableMultiTabs) {
|
|
32
51
|
this.logger.warn('Multiple tabs are not enabled in this browser');
|
|
33
52
|
}
|
|
53
|
+
const resolvedOptions = {
|
|
54
|
+
dbFilename: this.options.dbFilename,
|
|
55
|
+
dbLocation: this.options.dbLocation,
|
|
56
|
+
debugMode: this.options.debugMode,
|
|
57
|
+
vfs,
|
|
58
|
+
temporaryStorage,
|
|
59
|
+
cacheSizeKb,
|
|
60
|
+
flags: this.resolvedFlags,
|
|
61
|
+
encryptionKey: encryptionKey
|
|
62
|
+
};
|
|
63
|
+
let clientOptions;
|
|
64
|
+
let requiresPersistentTriggers = vfsRequiresDedicatedWorkers(vfs);
|
|
34
65
|
if (useWebWorker) {
|
|
35
66
|
const optionsDbWorker = this.options.worker;
|
|
36
67
|
const workerPort = typeof optionsDbWorker == 'function'
|
|
@@ -42,22 +73,20 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
|
|
|
42
73
|
encryptionKey
|
|
43
74
|
}))
|
|
44
75
|
: openWorkerDatabasePort(this.options.dbFilename, enableMultiTabs, optionsDbWorker, this.waOptions.vfs);
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
const source = Comlink.wrap(workerPort);
|
|
77
|
+
const closeSignal = new AbortController();
|
|
78
|
+
const connection = await source.connect({
|
|
79
|
+
...resolvedOptions,
|
|
80
|
+
logLevel: this.logger.getLevel(),
|
|
81
|
+
lockName: await generateTabCloseSignal(closeSignal.signal)
|
|
82
|
+
});
|
|
83
|
+
clientOptions = {
|
|
84
|
+
connection,
|
|
85
|
+
source,
|
|
48
86
|
// This tab owns the worker, so we're guaranteed to outlive it.
|
|
49
87
|
remoteCanCloseUnexpectedly: false,
|
|
50
|
-
baseConnection: await workerDBOpener({
|
|
51
|
-
dbFilename: this.options.dbFilename,
|
|
52
|
-
vfs,
|
|
53
|
-
temporaryStorage,
|
|
54
|
-
cacheSizeKb,
|
|
55
|
-
flags: this.resolvedFlags,
|
|
56
|
-
encryptionKey: encryptionKey,
|
|
57
|
-
logLevel: this.logger.getLevel()
|
|
58
|
-
}),
|
|
59
|
-
identifier: this.options.dbFilename,
|
|
60
88
|
onClose: () => {
|
|
89
|
+
closeSignal.abort();
|
|
61
90
|
if (workerPort instanceof Worker) {
|
|
62
91
|
workerPort.terminate();
|
|
63
92
|
}
|
|
@@ -65,21 +94,19 @@ export class WASQLiteOpenFactory extends AbstractWebSQLOpenFactory {
|
|
|
65
94
|
workerPort.close();
|
|
66
95
|
}
|
|
67
96
|
}
|
|
68
|
-
}
|
|
97
|
+
};
|
|
69
98
|
}
|
|
70
99
|
else {
|
|
71
|
-
// Don't use a web worker
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
vfs,
|
|
77
|
-
temporaryStorage,
|
|
78
|
-
cacheSizeKb,
|
|
79
|
-
flags: this.resolvedFlags,
|
|
80
|
-
encryptionKey: encryptionKey
|
|
81
|
-
});
|
|
100
|
+
// Don't use a web worker. Instead, open the MultiDatabaseServer a worker would use locally.
|
|
101
|
+
const localServer = new MultiDatabaseServer(this.logger);
|
|
102
|
+
requiresPersistentTriggers = true;
|
|
103
|
+
const connection = await localServer.openConnectionLocally(resolvedOptions);
|
|
104
|
+
clientOptions = { connection, source: null, remoteCanCloseUnexpectedly: false };
|
|
82
105
|
}
|
|
106
|
+
return new DatabaseClient(clientOptions, {
|
|
107
|
+
...resolvedOptions,
|
|
108
|
+
requiresPersistentTriggers
|
|
109
|
+
});
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
112
|
/**
|
|
@@ -89,7 +116,7 @@ function assertValidWASQLiteOpenFactoryOptions(options) {
|
|
|
89
116
|
// The OPFS VFS only works in dedicated web workers.
|
|
90
117
|
if ('vfs' in options && 'flags' in options) {
|
|
91
118
|
const { vfs, flags = {} } = options;
|
|
92
|
-
if (vfs
|
|
119
|
+
if (vfs && vfsRequiresDedicatedWorkers(vfs) && 'useWebWorker' in flags && !flags.useWebWorker) {
|
|
93
120
|
throw new Error(`Invalid configuration: The 'useWebWorker' flag must be true when using an OPFS-based VFS (${vfs}).`);
|
|
94
121
|
}
|
|
95
122
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type * as SQLite from '@journeyapps/wa-sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* List of currently tested virtual filesystems
|
|
4
|
+
*/
|
|
5
|
+
export declare enum WASQLiteVFS {
|
|
6
|
+
IDBBatchAtomicVFS = "IDBBatchAtomicVFS",
|
|
7
|
+
OPFSCoopSyncVFS = "OPFSCoopSyncVFS",
|
|
8
|
+
AccessHandlePoolVFS = "AccessHandlePoolVFS"
|
|
9
|
+
}
|
|
10
|
+
export declare function vfsRequiresDedicatedWorkers(vfs: WASQLiteVFS): vfs is WASQLiteVFS.OPFSCoopSyncVFS | WASQLiteVFS.AccessHandlePoolVFS;
|
|
11
|
+
/**
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export type WASQLiteModuleFactoryOptions = {
|
|
15
|
+
dbFileName: string;
|
|
16
|
+
encryptionKey?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export type SQLiteModule = Parameters<typeof SQLite.Factory>[0];
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export type WASQLiteModuleFactory = (options: WASQLiteModuleFactoryOptions) => Promise<{
|
|
26
|
+
module: SQLiteModule;
|
|
27
|
+
vfs: SQLiteVFS;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export declare const AsyncWASQLiteModuleFactory: () => Promise<any>;
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export declare const MultiCipherAsyncWASQLiteModuleFactory: () => Promise<any>;
|
|
37
|
+
/**
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare const SyncWASQLiteModuleFactory: () => Promise<any>;
|
|
41
|
+
/**
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export declare const MultiCipherSyncWASQLiteModuleFactory: () => Promise<any>;
|
|
45
|
+
/**
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export declare const DEFAULT_MODULE_FACTORIES: {
|
|
49
|
+
IDBBatchAtomicVFS: (options: WASQLiteModuleFactoryOptions) => Promise<{
|
|
50
|
+
module: any;
|
|
51
|
+
vfs: any;
|
|
52
|
+
}>;
|
|
53
|
+
AccessHandlePoolVFS: (options: WASQLiteModuleFactoryOptions) => Promise<{
|
|
54
|
+
module: any;
|
|
55
|
+
vfs: any;
|
|
56
|
+
}>;
|
|
57
|
+
OPFSCoopSyncVFS: (options: WASQLiteModuleFactoryOptions) => Promise<{
|
|
58
|
+
module: any;
|
|
59
|
+
vfs: any;
|
|
60
|
+
}>;
|
|
61
|
+
};
|