@powersync/web 0.0.0-dev-20251106124255 → 0.0.0-dev-20251119142638
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 +39 -17
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +35 -27
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +220 -54
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/lib/package.json +2 -2
- package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +2 -0
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +1 -1
- package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +8 -17
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +2 -0
- package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +6 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +17 -0
- package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +25 -0
- package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +41 -0
- package/lib/src/worker/db/SharedWASQLiteConnection.js +89 -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 +24 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/db/adapters/AsyncDatabaseConnection.ts +2 -0
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +19 -40
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +8 -0
- package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +37 -0
- package/src/worker/db/SharedWASQLiteConnection.ts +127 -0
- package/src/worker/db/WASQLiteDB.worker.ts +25 -54
- package/src/worker/db/WorkerWASQLiteConnection.ts +29 -0
|
@@ -60,6 +60,14 @@ export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLO
|
|
|
60
60
|
this.notifyRemoteClosed!.abort();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
markHold(): Promise<string> {
|
|
64
|
+
return this.baseConnection.markHold();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
releaseHold(holdId: string): Promise<void> {
|
|
68
|
+
return this.baseConnection.releaseHold(holdId);
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
private withRemote<T>(workerPromise: () => Promise<T>): Promise<T> {
|
|
64
72
|
const controller = this.notifyRemoteClosed;
|
|
65
73
|
if (controller) {
|
|
@@ -26,6 +26,14 @@ export type WASQLiteBroadCastTableUpdateEvent = {
|
|
|
26
26
|
*/
|
|
27
27
|
export type WASQLiteConnectionListener = {
|
|
28
28
|
tablesUpdated: (event: BatchedUpdateNotification) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Triggered when an active hold is overwritten by a new hold.
|
|
31
|
+
* This is most likely to happen when a shared connection has been closed
|
|
32
|
+
* without releasing the hold.
|
|
33
|
+
* This listener can be used to cleanup any resources associated with the previous hold.
|
|
34
|
+
* @param holdId - The id of the hold that has been overwritten.
|
|
35
|
+
*/
|
|
36
|
+
holdOverwritten: (holdId: string) => Promise<void>;
|
|
29
37
|
};
|
|
30
38
|
|
|
31
39
|
/**
|
|
@@ -148,6 +156,9 @@ export class WASqliteConnection
|
|
|
148
156
|
*/
|
|
149
157
|
protected connectionId: number;
|
|
150
158
|
|
|
159
|
+
protected _holdCounter: number;
|
|
160
|
+
protected _holdId: string | null;
|
|
161
|
+
|
|
151
162
|
constructor(protected options: ResolvedWASQLiteOpenFactoryOptions) {
|
|
152
163
|
super();
|
|
153
164
|
this.updatedTables = new Set();
|
|
@@ -156,6 +167,16 @@ export class WASqliteConnection
|
|
|
156
167
|
this.connectionId = new Date().valueOf() + Math.random();
|
|
157
168
|
this.statementMutex = new Mutex();
|
|
158
169
|
this._moduleFactory = DEFAULT_MODULE_FACTORIES[this.options.vfs];
|
|
170
|
+
this._holdCounter = 0;
|
|
171
|
+
this._holdId = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gets the id for the current hold.
|
|
176
|
+
* This can be used to check for invalid states.
|
|
177
|
+
*/
|
|
178
|
+
get currentHoldId() {
|
|
179
|
+
return this._holdId;
|
|
159
180
|
}
|
|
160
181
|
|
|
161
182
|
protected get sqliteAPI() {
|
|
@@ -172,6 +193,22 @@ export class WASqliteConnection
|
|
|
172
193
|
return this._dbP;
|
|
173
194
|
}
|
|
174
195
|
|
|
196
|
+
async markHold(): Promise<string> {
|
|
197
|
+
const previousHoldId = this._holdId;
|
|
198
|
+
this._holdId = `${++this._holdCounter}`;
|
|
199
|
+
if (previousHoldId) {
|
|
200
|
+
await this.iterateAsyncListeners(async (cb) => cb.holdOverwritten?.(previousHoldId));
|
|
201
|
+
}
|
|
202
|
+
return this._holdId;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async releaseHold(holdId: string): Promise<void> {
|
|
206
|
+
if (holdId != this._holdId) {
|
|
207
|
+
throw new Error(`Invalid hold state, expected ${this._holdId} but got ${holdId}`);
|
|
208
|
+
}
|
|
209
|
+
this._holdId = null;
|
|
210
|
+
}
|
|
211
|
+
|
|
175
212
|
protected async openDB() {
|
|
176
213
|
this._dbP = await this.sqliteAPI.open_v2(this.options.dbFilename);
|
|
177
214
|
return this._dbP;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ILogger } from '@powersync/common';
|
|
2
|
+
import {
|
|
3
|
+
AsyncDatabaseConnection,
|
|
4
|
+
OnTableChangeCallback,
|
|
5
|
+
ProxiedQueryResult
|
|
6
|
+
} from '../../db/adapters/AsyncDatabaseConnection';
|
|
7
|
+
import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Keeps track of open DB connections and the clients which
|
|
11
|
+
* are using it.
|
|
12
|
+
*/
|
|
13
|
+
export type SharedDBWorkerConnection = {
|
|
14
|
+
clientIds: Set<number>;
|
|
15
|
+
db: AsyncDatabaseConnection;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SharedWASQLiteConnectionOptions = {
|
|
19
|
+
dbMap: Map<string, SharedDBWorkerConnection>;
|
|
20
|
+
dbFilename: string;
|
|
21
|
+
clientId: number;
|
|
22
|
+
logger: ILogger;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class SharedWASQLiteConnection implements AsyncDatabaseConnection {
|
|
26
|
+
protected isClosing: boolean;
|
|
27
|
+
// Keeps track if this current hold if the shared connection has a hold
|
|
28
|
+
protected activeHoldId: string | null;
|
|
29
|
+
|
|
30
|
+
constructor(protected options: SharedWASQLiteConnectionOptions) {
|
|
31
|
+
// Add this client ID to the set of known clients
|
|
32
|
+
this.clientIds.add(options.clientId);
|
|
33
|
+
this.isClosing = false;
|
|
34
|
+
this.activeHoldId = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async init(): Promise<void> {
|
|
38
|
+
// No-op since the connection is already initialized when it was created
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async markHold(): Promise<string> {
|
|
42
|
+
this.activeHoldId = await this.connection.markHold();
|
|
43
|
+
return this.activeHoldId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async releaseHold(id: string): Promise<void> {
|
|
47
|
+
try {
|
|
48
|
+
await this.connection.releaseHold(id);
|
|
49
|
+
} finally {
|
|
50
|
+
this.activeHoldId = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected get logger() {
|
|
55
|
+
return this.options.logger;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected get dbEntry() {
|
|
59
|
+
return this.options.dbMap.get(this.options.dbFilename)!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected get connection() {
|
|
63
|
+
return this.dbEntry.db;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected get clientIds() {
|
|
67
|
+
return this.dbEntry.clientIds;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handles closing of a shared connection.
|
|
72
|
+
* The connection is only closed if there are no active clients using it.
|
|
73
|
+
*/
|
|
74
|
+
async close(): Promise<void> {
|
|
75
|
+
// This prevents further statements on this connection from being executed
|
|
76
|
+
this.isClosing = true;
|
|
77
|
+
const { clientIds, logger } = this;
|
|
78
|
+
const { clientId, dbFilename, dbMap } = this.options;
|
|
79
|
+
logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
|
|
80
|
+
clientIds.delete(clientId);
|
|
81
|
+
|
|
82
|
+
if (this.activeHoldId) {
|
|
83
|
+
/**
|
|
84
|
+
* The hold hasn't been released, but we're closing now.
|
|
85
|
+
* We can proactively cleanup and release the hold.
|
|
86
|
+
*/
|
|
87
|
+
await this.connection.execute('ROLLBACK').catch(() => {});
|
|
88
|
+
await this.connection.releaseHold(this.activeHoldId).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (clientIds.size == 0) {
|
|
92
|
+
logger.debug(`Closing connection to ${this.options}.`);
|
|
93
|
+
const connection = this.connection;
|
|
94
|
+
dbMap.delete(dbFilename);
|
|
95
|
+
await connection.close();
|
|
96
|
+
}
|
|
97
|
+
logger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected async withClosing<T>(action: () => Promise<T>) {
|
|
102
|
+
if (this.isClosing) {
|
|
103
|
+
throw new Error('Connection is closing');
|
|
104
|
+
}
|
|
105
|
+
return action();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async execute(sql: string, params?: any[]): Promise<ProxiedQueryResult> {
|
|
109
|
+
return this.withClosing(() => this.connection.execute(sql, params));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async executeRaw(sql: string, params?: any[]): Promise<any[][]> {
|
|
113
|
+
return this.withClosing(() => this.connection.executeRaw(sql, params));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
executeBatch(sql: string, params?: any[] | undefined): Promise<ProxiedQueryResult> {
|
|
117
|
+
return this.withClosing(() => this.connection.executeBatch(sql, params));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void> {
|
|
121
|
+
return this.connection.registerOnTableChange(callback);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getConfig(): Promise<ResolvedWebSQLOpenOptions> {
|
|
125
|
+
return this.connection.getConfig();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -6,47 +6,20 @@ import '@journeyapps/wa-sqlite';
|
|
|
6
6
|
import { createBaseLogger, createLogger } from '@powersync/common';
|
|
7
7
|
import * as Comlink from 'comlink';
|
|
8
8
|
import { AsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
ResolvedWASQLiteOpenFactoryOptions,
|
|
12
|
-
WorkerDBOpenerOptions
|
|
13
|
-
} from '../../db/adapters/wa-sqlite/WASQLiteOpenFactory';
|
|
9
|
+
import { WorkerDBOpenerOptions } from '../../db/adapters/wa-sqlite/WASQLiteOpenFactory';
|
|
14
10
|
import { getNavigatorLocks } from '../../shared/navigator';
|
|
11
|
+
import { SharedDBWorkerConnection, SharedWASQLiteConnection } from './SharedWASQLiteConnection';
|
|
12
|
+
import { WorkerWASQLiteConnection, proxyWASQLiteConnection } from './WorkerWASQLiteConnection';
|
|
15
13
|
|
|
16
14
|
const baseLogger = createBaseLogger();
|
|
17
15
|
baseLogger.useDefaults();
|
|
18
16
|
const logger = createLogger('db-worker');
|
|
19
17
|
|
|
20
|
-
/**
|
|
21
|
-
* Keeps track of open DB connections and the clients which
|
|
22
|
-
* are using it.
|
|
23
|
-
*/
|
|
24
|
-
type SharedDBWorkerConnection = {
|
|
25
|
-
clientIds: Set<number>;
|
|
26
|
-
db: AsyncDatabaseConnection;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
18
|
const DBMap = new Map<string, SharedDBWorkerConnection>();
|
|
30
19
|
const OPEN_DB_LOCK = 'open-wasqlite-db';
|
|
31
20
|
|
|
32
21
|
let nextClientId = 1;
|
|
33
22
|
|
|
34
|
-
const openWorkerConnection = async (options: ResolvedWASQLiteOpenFactoryOptions): Promise<AsyncDatabaseConnection> => {
|
|
35
|
-
const connection = new WASqliteConnection(options);
|
|
36
|
-
return {
|
|
37
|
-
init: Comlink.proxy(() => connection.init()),
|
|
38
|
-
getConfig: Comlink.proxy(() => connection.getConfig()),
|
|
39
|
-
close: Comlink.proxy(() => connection.close()),
|
|
40
|
-
execute: Comlink.proxy(async (sql: string, params?: any[]) => connection.execute(sql, params)),
|
|
41
|
-
executeRaw: Comlink.proxy(async (sql: string, params?: any[]) => connection.executeRaw(sql, params)),
|
|
42
|
-
executeBatch: Comlink.proxy(async (sql: string, params?: any[]) => connection.executeBatch(sql, params)),
|
|
43
|
-
registerOnTableChange: Comlink.proxy(async (callback) => {
|
|
44
|
-
// Proxy the callback remove function
|
|
45
|
-
return Comlink.proxy(await connection.registerOnTableChange(callback));
|
|
46
|
-
})
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
|
|
50
23
|
const openDBShared = async (options: WorkerDBOpenerOptions): Promise<AsyncDatabaseConnection> => {
|
|
51
24
|
// Prevent multiple simultaneous opens from causing race conditions
|
|
52
25
|
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
|
|
@@ -57,38 +30,36 @@ const openDBShared = async (options: WorkerDBOpenerOptions): Promise<AsyncDataba
|
|
|
57
30
|
|
|
58
31
|
if (!DBMap.has(dbFilename)) {
|
|
59
32
|
const clientIds = new Set<number>();
|
|
60
|
-
|
|
33
|
+
// This format returns proxy objects for function callbacks
|
|
34
|
+
const connection = new WorkerWASQLiteConnection(options);
|
|
61
35
|
await connection.init();
|
|
36
|
+
|
|
37
|
+
connection.registerListener({
|
|
38
|
+
holdOverwritten: async () => {
|
|
39
|
+
/**
|
|
40
|
+
* The previous hold has been overwritten, without being released.
|
|
41
|
+
* we need to cleanup any resources associated with it.
|
|
42
|
+
* We can perform a rollback to release any potential transactions that were started.
|
|
43
|
+
*/
|
|
44
|
+
await connection.execute('ROLLBACK').catch(() => {});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
62
48
|
DBMap.set(dbFilename, {
|
|
63
49
|
clientIds,
|
|
64
50
|
db: connection
|
|
65
51
|
});
|
|
66
52
|
}
|
|
67
53
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// the init has been done automatically
|
|
76
|
-
}),
|
|
77
|
-
close: Comlink.proxy(async () => {
|
|
78
|
-
const { clientIds } = dbEntry;
|
|
79
|
-
logger.debug(`Close requested from client ${clientId} of ${[...clientIds]}`);
|
|
80
|
-
clientIds.delete(clientId);
|
|
81
|
-
if (clientIds.size == 0) {
|
|
82
|
-
logger.debug(`Closing connection to ${dbFilename}.`);
|
|
83
|
-
DBMap.delete(dbFilename);
|
|
84
|
-
return db.close?.();
|
|
85
|
-
}
|
|
86
|
-
logger.debug(`Connection to ${dbFilename} not closed yet due to active clients.`);
|
|
87
|
-
return;
|
|
88
|
-
})
|
|
89
|
-
};
|
|
54
|
+
// Associates this clientId with the shared connection entry
|
|
55
|
+
const sharedConnection = new SharedWASQLiteConnection({
|
|
56
|
+
dbMap: DBMap,
|
|
57
|
+
dbFilename,
|
|
58
|
+
clientId,
|
|
59
|
+
logger
|
|
60
|
+
});
|
|
90
61
|
|
|
91
|
-
return
|
|
62
|
+
return proxyWASQLiteConnection(sharedConnection);
|
|
92
63
|
});
|
|
93
64
|
};
|
|
94
65
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Comlink from 'comlink';
|
|
2
|
+
import { AsyncDatabaseConnection, OnTableChangeCallback } from '../../db/adapters/AsyncDatabaseConnection';
|
|
3
|
+
import { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fully proxies a WASQLiteConnection to be used as an AsyncDatabaseConnection.
|
|
7
|
+
*/
|
|
8
|
+
export function proxyWASQLiteConnection(connection: AsyncDatabaseConnection): AsyncDatabaseConnection {
|
|
9
|
+
return Comlink.proxy({
|
|
10
|
+
init: Comlink.proxy(() => connection.init()),
|
|
11
|
+
close: Comlink.proxy(() => connection.close()),
|
|
12
|
+
markHold: Comlink.proxy(() => connection.markHold()),
|
|
13
|
+
releaseHold: Comlink.proxy((holdId: string) => connection.releaseHold(holdId)),
|
|
14
|
+
execute: Comlink.proxy((sql: string, params?: any[]) => connection.execute(sql, params)),
|
|
15
|
+
executeRaw: Comlink.proxy((sql: string, params?: any[]) => connection.executeRaw(sql, params)),
|
|
16
|
+
executeBatch: Comlink.proxy((sql: string, params?: any[]) => connection.executeBatch(sql, params)),
|
|
17
|
+
registerOnTableChange: Comlink.proxy((callback: OnTableChangeCallback) =>
|
|
18
|
+
connection.registerOnTableChange(callback)
|
|
19
|
+
),
|
|
20
|
+
getConfig: Comlink.proxy(() => connection.getConfig())
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class WorkerWASQLiteConnection extends WASqliteConnection {
|
|
25
|
+
async registerOnTableChange(callback: OnTableChangeCallback): Promise<() => void> {
|
|
26
|
+
// Proxy the callback remove function
|
|
27
|
+
return Comlink.proxy(await super.registerOnTableChange(callback));
|
|
28
|
+
}
|
|
29
|
+
}
|