@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.
Files changed (28) hide show
  1. package/dist/index.umd.js +39 -17
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +35 -27
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +220 -54
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/lib/package.json +2 -2
  8. package/lib/src/db/adapters/AsyncDatabaseConnection.d.ts +2 -0
  9. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.d.ts +1 -1
  10. package/lib/src/db/adapters/LockedAsyncDatabaseAdapter.js +8 -17
  11. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.d.ts +2 -0
  12. package/lib/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.js +6 -0
  13. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.d.ts +17 -0
  14. package/lib/src/db/adapters/wa-sqlite/WASQLiteConnection.js +25 -0
  15. package/lib/src/worker/db/SharedWASQLiteConnection.d.ts +41 -0
  16. package/lib/src/worker/db/SharedWASQLiteConnection.js +89 -0
  17. package/lib/src/worker/db/WASQLiteDB.worker.js +22 -39
  18. package/lib/src/worker/db/WorkerWASQLiteConnection.d.ts +9 -0
  19. package/lib/src/worker/db/WorkerWASQLiteConnection.js +24 -0
  20. package/lib/tsconfig.tsbuildinfo +1 -1
  21. package/package.json +3 -3
  22. package/src/db/adapters/AsyncDatabaseConnection.ts +2 -0
  23. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +19 -40
  24. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +8 -0
  25. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +37 -0
  26. package/src/worker/db/SharedWASQLiteConnection.ts +127 -0
  27. package/src/worker/db/WASQLiteDB.worker.ts +25 -54
  28. 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 { WASqliteConnection } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
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
- const connection = await openWorkerConnection(options);
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
- const dbEntry = DBMap.get(dbFilename)!;
69
- dbEntry.clientIds.add(clientId);
70
- const { db } = dbEntry;
71
-
72
- const wrappedConnection = {
73
- ...db,
74
- init: Comlink.proxy(async () => {
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 Comlink.proxy(wrappedConnection);
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
+ }