@powersync/web 1.28.2 → 1.29.1
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 +71 -9
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +12400 -8435
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +12888 -10926
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/lib/package.json +2 -2
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +15 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +2 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +17 -2
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +3 -0
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +11 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +22 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +32 -0
- package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +42 -0
- package/lib/src/worker/db/SharedWASQLiteConnection.js +90 -0
- package/lib/src/worker/db/WASQLiteDB.worker.js +22 -39
- package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +9 -0
- package/lib/src/worker/db/WorkerWASQLiteConnection.js +12 -0
- package/lib/src/worker/sync/SharedSyncImplementation.d.ts +2 -2
- package/lib/src/worker/sync/SharedSyncImplementation.js +7 -4
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/db/adapters/AsyncDatabaseConnection.ts +15 -0
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +29 -10
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +14 -0
- package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +45 -0
- package/src/worker/db/SharedWASQLiteConnection.ts +131 -0
- package/src/worker/db/WASQLiteDB.worker.ts +25 -54
- package/src/worker/db/WorkerWASQLiteConnection.ts +14 -0
- package/src/worker/sync/SharedSyncImplementation.ts +15 -12
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-81d3460.index.umd.js +0 -355
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-81d3460.index.umd.js.map +0 -1
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-81d3461.index.umd.js +0 -355
- package/dist/_journeyapps_wa-sqlite-_journeyapps_wa-sqlite_src_examples_AccessHandlePoolVFS_js-_journeyapp-81d3461.index.umd.js.map +0 -1
package/lib/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.1",
|
|
4
4
|
"description": "PowerSync web SDK. Sync Postgres, MongoDB or MySQL with SQLite in your web app",
|
|
5
5
|
"main": "lib/src/index.js",
|
|
6
6
|
"types": "lib/src/index.d.ts",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"license": "Apache-2.0",
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@journeyapps/wa-sqlite": "^1.3.2",
|
|
65
|
-
"@powersync/common": "workspace:^1.
|
|
65
|
+
"@powersync/common": "workspace:^1.43.1"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@powersync/common": "workspace:*",
|
|
@@ -22,6 +22,21 @@ export type OnTableChangeCallback = (event: BatchedUpdateNotification) => void;
|
|
|
22
22
|
export interface AsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> {
|
|
23
23
|
init(): Promise<void>;
|
|
24
24
|
close(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Marks the connection as in-use by a certain actor.
|
|
27
|
+
* @returns A hold ID which can be used to release the hold.
|
|
28
|
+
*/
|
|
29
|
+
markHold(): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Releases a hold on the connection.
|
|
32
|
+
* @param holdId The hold ID to release.
|
|
33
|
+
*/
|
|
34
|
+
releaseHold(holdId: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the database connection is in autocommit mode.
|
|
37
|
+
* @returns true if in autocommit mode, false if in a transaction
|
|
38
|
+
*/
|
|
39
|
+
isAutoCommit(): Promise<boolean>;
|
|
25
40
|
execute(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
|
|
26
41
|
executeRaw(sql: string, params?: any[]): Promise<any[][]>;
|
|
27
42
|
executeBatch(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseObserver, DBAdapterListener, DBLockOptions, LockContext, QueryResult, Transaction, type ILogger } from '@powersync/common';
|
|
2
2
|
import { AsyncDatabaseConnection } from './AsyncDatabaseConnection';
|
|
3
3
|
import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
|
|
4
4
|
import { ResolvedWebSQLOpenOptions } from './web-sql-flags';
|
|
@@ -30,6 +30,7 @@ export declare class LockedAsyncDatabaseAdapter extends BaseObserver<LockedAsync
|
|
|
30
30
|
protected _disposeTableChangeListener: (() => void) | null;
|
|
31
31
|
private _config;
|
|
32
32
|
protected pendingAbortControllers: Set<AbortController>;
|
|
33
|
+
protected requiresHolds: boolean | null;
|
|
33
34
|
closing: boolean;
|
|
34
35
|
closed: boolean;
|
|
35
36
|
constructor(options: LockedAsyncDatabaseAdapterOptions);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseObserver, createLogger } from '@powersync/common';
|
|
2
2
|
import { getNavigatorLocks } from '../..//shared/navigator';
|
|
3
3
|
import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
|
|
4
|
+
import { WASQLiteVFS } from './wa-sqlite/WASQLiteConnection';
|
|
4
5
|
/**
|
|
5
6
|
* @internal
|
|
6
7
|
* Wraps a {@link AsyncDatabaseConnection} and provides exclusive locking functions in
|
|
@@ -17,6 +18,7 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
|
|
|
17
18
|
_disposeTableChangeListener = null;
|
|
18
19
|
_config = null;
|
|
19
20
|
pendingAbortControllers;
|
|
21
|
+
requiresHolds;
|
|
20
22
|
closing;
|
|
21
23
|
closed;
|
|
22
24
|
constructor(options) {
|
|
@@ -27,6 +29,7 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
|
|
|
27
29
|
this.pendingAbortControllers = new Set();
|
|
28
30
|
this.closed = false;
|
|
29
31
|
this.closing = false;
|
|
32
|
+
this.requiresHolds = null;
|
|
30
33
|
// Set the name if provided. We can query for the name if not available yet
|
|
31
34
|
this.debugMode = options.debugMode ?? false;
|
|
32
35
|
if (this.debugMode) {
|
|
@@ -71,6 +74,10 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
|
|
|
71
74
|
this._config = await this._db.getConfig();
|
|
72
75
|
await this.registerOnChangeListener(this._db);
|
|
73
76
|
this.iterateListeners((cb) => cb.initialized?.());
|
|
77
|
+
/**
|
|
78
|
+
* This is only required for the long-lived shared IndexedDB connections.
|
|
79
|
+
*/
|
|
80
|
+
this.requiresHolds = this._config.vfs == WASQLiteVFS.IDBBatchAtomicVFS;
|
|
74
81
|
}
|
|
75
82
|
getConfiguration() {
|
|
76
83
|
if (!this._config) {
|
|
@@ -160,12 +167,20 @@ export class LockedAsyncDatabaseAdapter extends BaseObserver {
|
|
|
160
167
|
this.pendingAbortControllers.delete(abortController);
|
|
161
168
|
}, timeoutMs)
|
|
162
169
|
: null;
|
|
163
|
-
return getNavigatorLocks().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, () => {
|
|
170
|
+
return getNavigatorLocks().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, async () => {
|
|
164
171
|
this.pendingAbortControllers.delete(abortController);
|
|
165
172
|
if (timoutId) {
|
|
166
173
|
clearTimeout(timoutId);
|
|
167
174
|
}
|
|
168
|
-
|
|
175
|
+
const holdId = this.requiresHolds ? await this.baseDB.markHold() : null;
|
|
176
|
+
try {
|
|
177
|
+
return await callback();
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
if (holdId) {
|
|
181
|
+
await this.baseDB.releaseHold(holdId);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
169
184
|
});
|
|
170
185
|
}
|
|
171
186
|
async readTransaction(fn, options) {
|
|
@@ -34,6 +34,9 @@ export declare class WorkerWrappedAsyncDatabaseConnection<Config extends Resolve
|
|
|
34
34
|
* throw on all outstanding promises and forbid new calls.
|
|
35
35
|
*/
|
|
36
36
|
markRemoteClosed(): void;
|
|
37
|
+
markHold(): Promise<string>;
|
|
38
|
+
releaseHold(holdId: string): Promise<void>;
|
|
39
|
+
isAutoCommit(): Promise<boolean>;
|
|
37
40
|
private withRemote;
|
|
38
41
|
/**
|
|
39
42
|
* Get a MessagePort which can be used to share the internals of this connection.
|
|
@@ -31,12 +31,23 @@ export class WorkerWrappedAsyncDatabaseConnection {
|
|
|
31
31
|
// set.
|
|
32
32
|
this.notifyRemoteClosed.abort();
|
|
33
33
|
}
|
|
34
|
+
markHold() {
|
|
35
|
+
return this.withRemote(() => this.baseConnection.markHold());
|
|
36
|
+
}
|
|
37
|
+
releaseHold(holdId) {
|
|
38
|
+
return this.withRemote(() => this.baseConnection.releaseHold(holdId));
|
|
39
|
+
}
|
|
40
|
+
isAutoCommit() {
|
|
41
|
+
return this.withRemote(() => this.baseConnection.isAutoCommit());
|
|
42
|
+
}
|
|
34
43
|
withRemote(workerPromise) {
|
|
35
44
|
const controller = this.notifyRemoteClosed;
|
|
36
45
|
if (controller) {
|
|
37
46
|
return new Promise((resolve, reject) => {
|
|
38
47
|
if (controller.signal.aborted) {
|
|
39
48
|
reject(new Error('Called operation on closed remote'));
|
|
49
|
+
// Don't run the operation if we're going to reject
|
|
50
|
+
return;
|
|
40
51
|
}
|
|
41
52
|
function handleAbort() {
|
|
42
53
|
reject(new Error('Remote peer closed with request in flight'));
|
|
@@ -23,6 +23,14 @@ export type WASQLiteBroadCastTableUpdateEvent = {
|
|
|
23
23
|
*/
|
|
24
24
|
export type WASQLiteConnectionListener = {
|
|
25
25
|
tablesUpdated: (event: BatchedUpdateNotification) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Triggered when an active hold is overwritten by a new hold.
|
|
28
|
+
* This is most likely to happen when a shared connection has been closed
|
|
29
|
+
* without releasing the hold.
|
|
30
|
+
* This listener can be used to cleanup any resources associated with the previous hold.
|
|
31
|
+
* @param holdId - The id of the hold that has been overwritten.
|
|
32
|
+
*/
|
|
33
|
+
holdOverwritten: (holdId: string) => Promise<void>;
|
|
26
34
|
};
|
|
27
35
|
/**
|
|
28
36
|
* @internal
|
|
@@ -94,9 +102,23 @@ export declare class WASqliteConnection extends BaseObserver<WASQLiteConnectionL
|
|
|
94
102
|
* notification loops.
|
|
95
103
|
*/
|
|
96
104
|
protected connectionId: number;
|
|
105
|
+
protected _holdCounter: number;
|
|
106
|
+
protected _holdId: string | null;
|
|
97
107
|
constructor(options: ResolvedWASQLiteOpenFactoryOptions);
|
|
108
|
+
/**
|
|
109
|
+
* Gets the id for the current hold.
|
|
110
|
+
* This can be used to check for invalid states.
|
|
111
|
+
*/
|
|
112
|
+
get currentHoldId(): string | null;
|
|
98
113
|
protected get sqliteAPI(): SQLiteAPI;
|
|
99
114
|
protected get dbP(): number;
|
|
115
|
+
/**
|
|
116
|
+
* Checks if the database connection is in autocommit mode.
|
|
117
|
+
* @returns true if in autocommit mode, false if in a transaction
|
|
118
|
+
*/
|
|
119
|
+
isAutoCommit(): Promise<boolean>;
|
|
120
|
+
markHold(): Promise<string>;
|
|
121
|
+
releaseHold(holdId: string): Promise<void>;
|
|
100
122
|
protected openDB(): Promise<number>;
|
|
101
123
|
protected executeEncryptionPragma(): Promise<void>;
|
|
102
124
|
protected openSQLiteAPI(): Promise<SQLiteAPI>;
|
|
@@ -107,6 +107,8 @@ export class WASqliteConnection extends BaseObserver {
|
|
|
107
107
|
* notification loops.
|
|
108
108
|
*/
|
|
109
109
|
connectionId;
|
|
110
|
+
_holdCounter;
|
|
111
|
+
_holdId;
|
|
110
112
|
constructor(options) {
|
|
111
113
|
super();
|
|
112
114
|
this.options = options;
|
|
@@ -116,6 +118,15 @@ export class WASqliteConnection extends BaseObserver {
|
|
|
116
118
|
this.connectionId = new Date().valueOf() + Math.random();
|
|
117
119
|
this.statementMutex = new Mutex();
|
|
118
120
|
this._moduleFactory = DEFAULT_MODULE_FACTORIES[this.options.vfs];
|
|
121
|
+
this._holdCounter = 0;
|
|
122
|
+
this._holdId = null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Gets the id for the current hold.
|
|
126
|
+
* This can be used to check for invalid states.
|
|
127
|
+
*/
|
|
128
|
+
get currentHoldId() {
|
|
129
|
+
return this._holdId;
|
|
119
130
|
}
|
|
120
131
|
get sqliteAPI() {
|
|
121
132
|
if (!this._sqliteAPI) {
|
|
@@ -129,6 +140,27 @@ export class WASqliteConnection extends BaseObserver {
|
|
|
129
140
|
}
|
|
130
141
|
return this._dbP;
|
|
131
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Checks if the database connection is in autocommit mode.
|
|
145
|
+
* @returns true if in autocommit mode, false if in a transaction
|
|
146
|
+
*/
|
|
147
|
+
async isAutoCommit() {
|
|
148
|
+
return this.sqliteAPI.get_autocommit(this.dbP) != 0;
|
|
149
|
+
}
|
|
150
|
+
async markHold() {
|
|
151
|
+
const previousHoldId = this._holdId;
|
|
152
|
+
this._holdId = `${++this._holdCounter}`;
|
|
153
|
+
if (previousHoldId) {
|
|
154
|
+
await this.iterateAsyncListeners(async (cb) => cb.holdOverwritten?.(previousHoldId));
|
|
155
|
+
}
|
|
156
|
+
return this._holdId;
|
|
157
|
+
}
|
|
158
|
+
async releaseHold(holdId) {
|
|
159
|
+
if (holdId != this._holdId) {
|
|
160
|
+
throw new Error(`Invalid hold state, expected ${this._holdId} but got ${holdId}`);
|
|
161
|
+
}
|
|
162
|
+
this._holdId = null;
|
|
163
|
+
}
|
|
132
164
|
async openDB() {
|
|
133
165
|
this._dbP = await this.sqliteAPI.open_v2(this.options.dbFilename);
|
|
134
166
|
return this._dbP;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ILogger } from '@powersync/common';
|
|
2
|
+
import { AsyncDatabaseConnection, OnTableChangeCallback, ProxiedQueryResult } from '../../db/adapters/AsyncDatabaseConnection';
|
|
3
|
+
import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
|
|
4
|
+
/**
|
|
5
|
+
* Keeps track of open DB connections and the clients which
|
|
6
|
+
* are using it.
|
|
7
|
+
*/
|
|
8
|
+
export type SharedDBWorkerConnection = {
|
|
9
|
+
clientIds: Set<number>;
|
|
10
|
+
db: AsyncDatabaseConnection;
|
|
11
|
+
};
|
|
12
|
+
export type SharedWASQLiteConnectionOptions = {
|
|
13
|
+
dbMap: Map<string, SharedDBWorkerConnection>;
|
|
14
|
+
dbFilename: string;
|
|
15
|
+
clientId: number;
|
|
16
|
+
logger: ILogger;
|
|
17
|
+
};
|
|
18
|
+
export declare class SharedWASQLiteConnection implements AsyncDatabaseConnection {
|
|
19
|
+
protected options: SharedWASQLiteConnectionOptions;
|
|
20
|
+
protected isClosing: boolean;
|
|
21
|
+
protected activeHoldId: string | null;
|
|
22
|
+
constructor(options: SharedWASQLiteConnectionOptions);
|
|
23
|
+
protected get logger(): ILogger;
|
|
24
|
+
protected get dbEntry(): SharedDBWorkerConnection;
|
|
25
|
+
protected get connection(): AsyncDatabaseConnection<ResolvedWebSQLOpenOptions>;
|
|
26
|
+
protected get clientIds(): Set<number>;
|
|
27
|
+
init(): Promise<void>;
|
|
28
|
+
markHold(): Promise<string>;
|
|
29
|
+
releaseHold(id: string): Promise<void>;
|
|
30
|
+
isAutoCommit(): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Handles closing of a shared connection.
|
|
33
|
+
* The connection is only closed if there are no active clients using it.
|
|
34
|
+
*/
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
protected withClosing<T>(action: () => Promise<T>): Promise<T>;
|
|
37
|
+
execute(sql: string, params?: any[]): Promise<ProxiedQueryResult>;
|
|
38
|
+
executeRaw(sql: string, params?: any[]): Promise<any[][]>;
|
|
39
|
+
executeBatch(sql: string, params?: any[] | undefined): Promise<ProxiedQueryResult>;
|
|
40
|
+
registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void>;
|
|
41
|
+
getConfig(): Promise<ResolvedWebSQLOpenOptions>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export class SharedWASQLiteConnection {
|
|
2
|
+
options;
|
|
3
|
+
isClosing;
|
|
4
|
+
// Keeps track if this current hold if the shared connection has a hold
|
|
5
|
+
activeHoldId;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
// Add this client ID to the set of known clients
|
|
9
|
+
this.clientIds.add(options.clientId);
|
|
10
|
+
this.isClosing = false;
|
|
11
|
+
this.activeHoldId = null;
|
|
12
|
+
}
|
|
13
|
+
get logger() {
|
|
14
|
+
return this.options.logger;
|
|
15
|
+
}
|
|
16
|
+
get dbEntry() {
|
|
17
|
+
return this.options.dbMap.get(this.options.dbFilename);
|
|
18
|
+
}
|
|
19
|
+
get connection() {
|
|
20
|
+
return this.dbEntry.db;
|
|
21
|
+
}
|
|
22
|
+
get clientIds() {
|
|
23
|
+
return this.dbEntry.clientIds;
|
|
24
|
+
}
|
|
25
|
+
async init() {
|
|
26
|
+
// No-op since the connection is already initialized when it was created
|
|
27
|
+
}
|
|
28
|
+
async markHold() {
|
|
29
|
+
this.activeHoldId = await this.connection.markHold();
|
|
30
|
+
return this.activeHoldId;
|
|
31
|
+
}
|
|
32
|
+
async releaseHold(id) {
|
|
33
|
+
try {
|
|
34
|
+
await this.connection.releaseHold(id);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
this.activeHoldId = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async isAutoCommit() {
|
|
41
|
+
return this.connection.isAutoCommit();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Handles closing of a shared connection.
|
|
45
|
+
* The connection is only closed if there are no active clients using it.
|
|
46
|
+
*/
|
|
47
|
+
async close() {
|
|
48
|
+
// This prevents further statements on this connection from being executed
|
|
49
|
+
this.isClosing = true;
|
|
50
|
+
const { clientIds, logger } = this;
|
|
51
|
+
const { clientId, dbFilename, dbMap } = this.options;
|
|
52
|
+
logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
|
|
53
|
+
clientIds.delete(clientId);
|
|
54
|
+
if (this.activeHoldId) {
|
|
55
|
+
// We can't cleanup here since we're not in a lock context.
|
|
56
|
+
// The cleanup will occur once a new hold is acquired.
|
|
57
|
+
this.logger.info(`Hold ${this.activeHoldId} was still active when the connection was closed. Cleanup will occur once a new hold is acquired.`);
|
|
58
|
+
}
|
|
59
|
+
if (clientIds.size == 0) {
|
|
60
|
+
logger.debug(`Closing connection to ${this.options}.`);
|
|
61
|
+
const connection = this.connection;
|
|
62
|
+
dbMap.delete(dbFilename);
|
|
63
|
+
await connection.close();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
logger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
async withClosing(action) {
|
|
70
|
+
if (this.isClosing) {
|
|
71
|
+
throw new Error('Connection is closing');
|
|
72
|
+
}
|
|
73
|
+
return action();
|
|
74
|
+
}
|
|
75
|
+
async execute(sql, params) {
|
|
76
|
+
return this.withClosing(() => this.connection.execute(sql, params));
|
|
77
|
+
}
|
|
78
|
+
async executeRaw(sql, params) {
|
|
79
|
+
return this.withClosing(() => this.connection.executeRaw(sql, params));
|
|
80
|
+
}
|
|
81
|
+
executeBatch(sql, params) {
|
|
82
|
+
return this.withClosing(() => this.connection.executeBatch(sql, params));
|
|
83
|
+
}
|
|
84
|
+
registerOnTableChange(callback) {
|
|
85
|
+
return this.connection.registerOnTableChange(callback);
|
|
86
|
+
}
|
|
87
|
+
getConfig() {
|
|
88
|
+
return this.connection.getConfig();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -4,29 +4,15 @@
|
|
|
4
4
|
import '@journeyapps/wa-sqlite';
|
|
5
5
|
import { createBaseLogger, createLogger } from '@powersync/common';
|
|
6
6
|
import * as Comlink from 'comlink';
|
|
7
|
-
import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
|
|
8
7
|
import { getNavigatorLocks } from '../../shared/navigator';
|
|
8
|
+
import { SharedWASQLiteConnection } from './SharedWASQLiteConnection';
|
|
9
|
+
import { WorkerWASQLiteConnection } from './WorkerWASQLiteConnection';
|
|
9
10
|
const baseLogger = createBaseLogger();
|
|
10
11
|
baseLogger.useDefaults();
|
|
11
12
|
const logger = createLogger('db-worker');
|
|
12
13
|
const DBMap = new Map();
|
|
13
14
|
const OPEN_DB_LOCK = 'open-wasqlite-db';
|
|
14
15
|
let nextClientId = 1;
|
|
15
|
-
const openWorkerConnection = async (options) => {
|
|
16
|
-
const connection = new WASqliteConnection(options);
|
|
17
|
-
return {
|
|
18
|
-
init: Comlink.proxy(() => connection.init()),
|
|
19
|
-
getConfig: Comlink.proxy(() => connection.getConfig()),
|
|
20
|
-
close: Comlink.proxy(() => connection.close()),
|
|
21
|
-
execute: Comlink.proxy(async (sql, params) => connection.execute(sql, params)),
|
|
22
|
-
executeRaw: Comlink.proxy(async (sql, params) => connection.executeRaw(sql, params)),
|
|
23
|
-
executeBatch: Comlink.proxy(async (sql, params) => connection.executeBatch(sql, params)),
|
|
24
|
-
registerOnTableChange: Comlink.proxy(async (callback) => {
|
|
25
|
-
// Proxy the callback remove function
|
|
26
|
-
return Comlink.proxy(await connection.registerOnTableChange(callback));
|
|
27
|
-
})
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
16
|
const openDBShared = async (options) => {
|
|
31
17
|
// Prevent multiple simultaneous opens from causing race conditions
|
|
32
18
|
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
|
|
@@ -35,35 +21,32 @@ const openDBShared = async (options) => {
|
|
|
35
21
|
logger.setLevel(logLevel);
|
|
36
22
|
if (!DBMap.has(dbFilename)) {
|
|
37
23
|
const clientIds = new Set();
|
|
38
|
-
|
|
24
|
+
// This format returns proxy objects for function callbacks
|
|
25
|
+
const connection = new WorkerWASQLiteConnection(options);
|
|
39
26
|
await connection.init();
|
|
27
|
+
connection.registerListener({
|
|
28
|
+
holdOverwritten: async () => {
|
|
29
|
+
/**
|
|
30
|
+
* The previous hold has been overwritten, without being released.
|
|
31
|
+
* we need to cleanup any resources associated with it.
|
|
32
|
+
* We can perform a rollback to release any potential transactions that were started.
|
|
33
|
+
*/
|
|
34
|
+
await connection.execute('ROLLBACK').catch(() => { });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
40
37
|
DBMap.set(dbFilename, {
|
|
41
38
|
clientIds,
|
|
42
39
|
db: connection
|
|
43
40
|
});
|
|
44
41
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
close: Comlink.proxy(async () => {
|
|
54
|
-
const { clientIds } = dbEntry;
|
|
55
|
-
logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
|
|
56
|
-
clientIds.delete(clientId);
|
|
57
|
-
if (clientIds.size == 0) {
|
|
58
|
-
logger.debug(`Closing connection to ${dbFilename}.`);
|
|
59
|
-
DBMap.delete(dbFilename);
|
|
60
|
-
return db.close?.();
|
|
61
|
-
}
|
|
62
|
-
logger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`);
|
|
63
|
-
return;
|
|
64
|
-
})
|
|
65
|
-
};
|
|
66
|
-
return Comlink.proxy(wrappedConnection);
|
|
42
|
+
// Associates this clientId with the shared connection entry
|
|
43
|
+
const sharedConnection = new SharedWASQLiteConnection({
|
|
44
|
+
dbMap: DBMap,
|
|
45
|
+
dbFilename,
|
|
46
|
+
clientId,
|
|
47
|
+
logger
|
|
48
|
+
});
|
|
49
|
+
return Comlink.proxy(sharedConnection);
|
|
67
50
|
});
|
|
68
51
|
};
|
|
69
52
|
// Check if we're in a SharedWorker context
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { OnTableChangeCallback } from '../../db/adapters/AsyncDatabaseConnection';
|
|
2
|
+
import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
|
|
3
|
+
/**
|
|
4
|
+
* A Small proxy wrapper around the WASqliteConnection.
|
|
5
|
+
* This ensures that certain return types are properly proxied.
|
|
6
|
+
*/
|
|
7
|
+
export declare class WorkerWASQLiteConnection extends WASqliteConnection {
|
|
8
|
+
registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as Comlink from 'comlink';
|
|
2
|
+
import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
|
|
3
|
+
/**
|
|
4
|
+
* A Small proxy wrapper around the WASqliteConnection.
|
|
5
|
+
* This ensures that certain return types are properly proxied.
|
|
6
|
+
*/
|
|
7
|
+
export class WorkerWASQLiteConnection extends WASqliteConnection {
|
|
8
|
+
async registerOnTableChange(callback) {
|
|
9
|
+
// Proxy the callback remove function
|
|
10
|
+
return Comlink.proxy(await super.registerOnTableChange(callback));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions
|
|
1
|
+
import { BaseObserver, ConnectionManager, DBAdapter, SubscribedStream, SyncStatus, type ILogger, type ILogLevel, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, type SyncStatusOptions } from '@powersync/common';
|
|
2
2
|
import { Mutex } from 'async-mutex';
|
|
3
3
|
import * as Comlink from 'comlink';
|
|
4
4
|
import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from '../../db/sync/WebStreamingSyncImplementation';
|
|
@@ -44,7 +44,7 @@ export type WrappedSyncPort = {
|
|
|
44
44
|
clientProvider: Comlink.Remote<AbstractSharedSyncClientProvider>;
|
|
45
45
|
db?: DBAdapter;
|
|
46
46
|
currentSubscriptions: SubscribedStream[];
|
|
47
|
-
closeListeners: (() => void)[];
|
|
47
|
+
closeListeners: (() => void | Promise<void>)[];
|
|
48
48
|
};
|
|
49
49
|
/**
|
|
50
50
|
* @internal
|
|
@@ -228,11 +228,14 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
228
228
|
return () => { };
|
|
229
229
|
}
|
|
230
230
|
for (const closeListener of trackedPort.closeListeners) {
|
|
231
|
-
closeListener();
|
|
231
|
+
await closeListener();
|
|
232
232
|
}
|
|
233
233
|
if (this.dbAdapter && this.dbAdapter == trackedPort.db) {
|
|
234
234
|
// Unconditionally close the connection because the database it's writing to has just been closed.
|
|
235
|
-
|
|
235
|
+
// The connection has been closed previously, this might throw. We should be able to ignore it.
|
|
236
|
+
await this.connectionManager
|
|
237
|
+
.disconnect()
|
|
238
|
+
.catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex));
|
|
236
239
|
// Clearing the adapter will result in a new one being opened in connect
|
|
237
240
|
this.dbAdapter = null;
|
|
238
241
|
if (shouldReconnect) {
|
|
@@ -361,9 +364,9 @@ export class SharedSyncImplementation extends BaseObserver {
|
|
|
361
364
|
// that and ensure pending requests are aborted when the tab is closed.
|
|
362
365
|
remoteCanCloseUnexpectedly: true
|
|
363
366
|
});
|
|
364
|
-
lastClient.closeListeners.push(() => {
|
|
367
|
+
lastClient.closeListeners.push(async () => {
|
|
365
368
|
this.logger.info('Aborting open connection because associated tab closed.');
|
|
366
|
-
wrapped.close();
|
|
369
|
+
await wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex));
|
|
367
370
|
wrapped.markRemoteClosed();
|
|
368
371
|
});
|
|
369
372
|
return wrapped;
|