@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.
Files changed (91) hide show
  1. package/dist/index.umd.js +1127 -1235
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +481 -3111
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +688 -836
  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 -1881
  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 -404
  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 -489
  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,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 './WASQLiteConnection.js';
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 extends AbstractWebSQLOpenFactory {
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
- openConnection(): Promise<AsyncDatabaseConnection>;
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 { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory.js';
4
- import { WorkerWrappedAsyncDatabaseConnection } from '../WorkerWrappedAsyncDatabaseConnection.js';
5
- import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption } from '../web-sql-flags.js';
6
- import { InternalWASQLiteDBAdapter } from './InternalWASQLiteDBAdapter.js';
7
- import { WASQLiteVFS, WASqliteConnection } from './WASQLiteConnection.js';
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 extends AbstractWebSQLOpenFactory {
14
+ export class WASQLiteOpenFactory {
15
+ options;
16
+ resolvedFlags;
17
+ logger;
12
18
  constructor(options) {
13
- super(options);
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 InternalWASQLiteDBAdapter({
22
- name: this.options.dbFilename,
23
- openConnection: () => this.openConnection(),
24
- debugMode: this.options.debugMode,
25
- logger: this.logger
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 workerDBOpener = Comlink.wrap(workerPort);
46
- return new WorkerWrappedAsyncDatabaseConnection({
47
- remote: workerDBOpener,
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
- return new WASqliteConnection({
73
- dbFilename: this.options.dbFilename,
74
- dbLocation: this.options.dbLocation,
75
- debugMode: this.options.debugMode,
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 !== WASQLiteVFS.IDBBatchAtomicVFS && 'useWebWorker' in flags && !flags.useWebWorker) {
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
+ };