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