@powersync/web 1.27.1 → 1.28.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 +64 -26
- package/dist/index.umd.js.map +1 -1
- package/dist/worker/SharedSyncImplementation.umd.js +13640 -440
- package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
- package/dist/worker/WASQLiteDB.umd.js +13467 -161
- package/dist/worker/WASQLiteDB.umd.js.map +1 -1
- package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js +66 -38
- package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +1 -1
- package/lib/package.json +6 -5
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
- package/src/db/PowerSyncDatabase.ts +224 -0
- package/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts +47 -0
- package/src/db/adapters/AbstractWebSQLOpenFactory.ts +48 -0
- package/src/db/adapters/AsyncDatabaseConnection.ts +40 -0
- package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +358 -0
- package/src/db/adapters/SSRDBAdapter.ts +94 -0
- package/src/db/adapters/WebDBAdapter.ts +20 -0
- package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +175 -0
- package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +444 -0
- package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +86 -0
- package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +134 -0
- package/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts +24 -0
- package/src/db/adapters/web-sql-flags.ts +135 -0
- package/src/db/sync/SSRWebStreamingSyncImplementation.ts +89 -0
- package/src/db/sync/SharedWebStreamingSyncImplementation.ts +274 -0
- package/src/db/sync/WebRemote.ts +59 -0
- package/src/db/sync/WebStreamingSyncImplementation.ts +34 -0
- package/src/db/sync/userAgent.ts +78 -0
- package/src/index.ts +13 -0
- package/src/shared/navigator.ts +9 -0
- package/src/worker/db/WASQLiteDB.worker.ts +112 -0
- package/src/worker/db/open-worker-database.ts +62 -0
- package/src/worker/sync/AbstractSharedSyncClientProvider.ts +21 -0
- package/src/worker/sync/BroadcastLogger.ts +142 -0
- package/src/worker/sync/SharedSyncImplementation.ts +520 -0
- package/src/worker/sync/SharedSyncImplementation.worker.ts +14 -0
- package/src/worker/sync/WorkerClient.ts +106 -0
- package/dist/worker/node_modules_crypto-browserify_index_js.umd.js +0 -33734
- package/dist/worker/node_modules_crypto-browserify_index_js.umd.js.map +0 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ILogger,
|
|
3
|
+
BaseObserver,
|
|
4
|
+
createLogger,
|
|
5
|
+
DBAdapter,
|
|
6
|
+
DBAdapterListener,
|
|
7
|
+
DBGetUtils,
|
|
8
|
+
DBLockOptions,
|
|
9
|
+
LockContext,
|
|
10
|
+
QueryResult,
|
|
11
|
+
Transaction
|
|
12
|
+
} from '@powersync/common';
|
|
13
|
+
import { getNavigatorLocks } from '../..//shared/navigator';
|
|
14
|
+
import { AsyncDatabaseConnection } from './AsyncDatabaseConnection';
|
|
15
|
+
import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter';
|
|
16
|
+
import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection';
|
|
17
|
+
import { ResolvedWebSQLOpenOptions } from './web-sql-flags';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export interface LockedAsyncDatabaseAdapterOptions {
|
|
23
|
+
name: string;
|
|
24
|
+
openConnection: () => Promise<AsyncDatabaseConnection>;
|
|
25
|
+
debugMode?: boolean;
|
|
26
|
+
logger?: ILogger;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type LockedAsyncDatabaseAdapterListener = DBAdapterListener & {
|
|
30
|
+
initialized?: () => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
* Wraps a {@link AsyncDatabaseConnection} and provides exclusive locking functions in
|
|
36
|
+
* order to implement {@link DBAdapter}.
|
|
37
|
+
*/
|
|
38
|
+
export class LockedAsyncDatabaseAdapter
|
|
39
|
+
extends BaseObserver<LockedAsyncDatabaseAdapterListener>
|
|
40
|
+
implements WebDBAdapter
|
|
41
|
+
{
|
|
42
|
+
private logger: ILogger;
|
|
43
|
+
private dbGetHelpers: DBGetUtils | null;
|
|
44
|
+
private debugMode: boolean;
|
|
45
|
+
private _dbIdentifier: string;
|
|
46
|
+
protected initPromise: Promise<void>;
|
|
47
|
+
private _db: AsyncDatabaseConnection | null = null;
|
|
48
|
+
protected _disposeTableChangeListener: (() => void) | null = null;
|
|
49
|
+
private _config: ResolvedWebSQLOpenOptions | null = null;
|
|
50
|
+
protected pendingAbortControllers: Set<AbortController>;
|
|
51
|
+
|
|
52
|
+
closing: boolean;
|
|
53
|
+
closed: boolean;
|
|
54
|
+
|
|
55
|
+
constructor(protected options: LockedAsyncDatabaseAdapterOptions) {
|
|
56
|
+
super();
|
|
57
|
+
this._dbIdentifier = options.name;
|
|
58
|
+
this.logger = options.logger ?? createLogger(`LockedAsyncDatabaseAdapter - ${this._dbIdentifier}`);
|
|
59
|
+
this.pendingAbortControllers = new Set<AbortController>();
|
|
60
|
+
this.closed = false;
|
|
61
|
+
this.closing = false;
|
|
62
|
+
// Set the name if provided. We can query for the name if not available yet
|
|
63
|
+
this.debugMode = options.debugMode ?? false;
|
|
64
|
+
if (this.debugMode) {
|
|
65
|
+
const originalExecute = this._execute.bind(this);
|
|
66
|
+
this._execute = async (sql, bindings) => {
|
|
67
|
+
const start = performance.now();
|
|
68
|
+
try {
|
|
69
|
+
const r = await originalExecute(sql, bindings);
|
|
70
|
+
performance.measure(`[SQL] ${sql}`, { start });
|
|
71
|
+
return r;
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
performance.measure(`[SQL] [ERROR: ${e.message}] ${sql}`, { start });
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.dbGetHelpers = this.generateDBHelpers({
|
|
80
|
+
execute: (query, params) => this.acquireLock(() => this._execute(query, params)),
|
|
81
|
+
executeRaw: (query, params) => this.acquireLock(() => this._executeRaw(query, params))
|
|
82
|
+
});
|
|
83
|
+
this.initPromise = this._init();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected get baseDB() {
|
|
87
|
+
if (!this._db) {
|
|
88
|
+
throw new Error(`Initialization has not completed yet. Cannot access base db`);
|
|
89
|
+
}
|
|
90
|
+
return this._db;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get name() {
|
|
94
|
+
return this._dbIdentifier;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Init is automatic, this helps catch errors or explicitly await initialization
|
|
99
|
+
*/
|
|
100
|
+
async init() {
|
|
101
|
+
return this.initPromise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected async _init() {
|
|
105
|
+
this._db = await this.options.openConnection();
|
|
106
|
+
await this._db.init();
|
|
107
|
+
this._config = await this._db.getConfig();
|
|
108
|
+
await this.registerOnChangeListener(this._db);
|
|
109
|
+
this.iterateListeners((cb) => cb.initialized?.());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getConfiguration(): ResolvedWebSQLOpenOptions {
|
|
113
|
+
if (!this._config) {
|
|
114
|
+
throw new Error(`Cannot get config before initialization is completed`);
|
|
115
|
+
}
|
|
116
|
+
return this._config;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
protected async waitForInitialized() {
|
|
120
|
+
// Awaiting this will expose errors on function calls like .execute etc
|
|
121
|
+
await this.initPromise;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async shareConnection(): Promise<SharedConnectionWorker> {
|
|
125
|
+
if (false == this._db instanceof WorkerWrappedAsyncDatabaseConnection) {
|
|
126
|
+
throw new Error(`Only worker connections can be shared`);
|
|
127
|
+
}
|
|
128
|
+
return this._db.shareConnection();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Registers a table change notification callback with the base database.
|
|
133
|
+
* This can be extended by custom implementations in order to handle proxy events.
|
|
134
|
+
*/
|
|
135
|
+
protected async registerOnChangeListener(db: AsyncDatabaseConnection) {
|
|
136
|
+
this._disposeTableChangeListener = await db.registerOnTableChange((event) => {
|
|
137
|
+
this.iterateListeners((cb) => cb.tablesUpdated?.(event));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* This is currently a no-op on web
|
|
143
|
+
*/
|
|
144
|
+
async refreshSchema(): Promise<void> {}
|
|
145
|
+
|
|
146
|
+
async execute(query: string, params?: any[] | undefined): Promise<QueryResult> {
|
|
147
|
+
return this.writeLock((ctx) => ctx.execute(query, params));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
|
|
151
|
+
return this.writeLock((ctx) => ctx.executeRaw(query, params));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async executeBatch(query: string, params?: any[][]): Promise<QueryResult> {
|
|
155
|
+
return this.writeLock((ctx) => this._executeBatch(query, params));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Attempts to close the connection.
|
|
160
|
+
* Shared workers might not actually close the connection if other
|
|
161
|
+
* tabs are still using it.
|
|
162
|
+
*/
|
|
163
|
+
async close() {
|
|
164
|
+
this.closing = true;
|
|
165
|
+
this._disposeTableChangeListener?.();
|
|
166
|
+
this.pendingAbortControllers.forEach((controller) => controller.abort('Closed'));
|
|
167
|
+
await this.baseDB?.close?.();
|
|
168
|
+
this.closed = true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async getAll<T>(sql: string, parameters?: any[] | undefined): Promise<T[]> {
|
|
172
|
+
await this.waitForInitialized();
|
|
173
|
+
return this.dbGetHelpers!.getAll(sql, parameters);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async getOptional<T>(sql: string, parameters?: any[] | undefined): Promise<T | null> {
|
|
177
|
+
await this.waitForInitialized();
|
|
178
|
+
return this.dbGetHelpers!.getOptional(sql, parameters);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async get<T>(sql: string, parameters?: any[] | undefined): Promise<T> {
|
|
182
|
+
await this.waitForInitialized();
|
|
183
|
+
return this.dbGetHelpers!.get(sql, parameters);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
|
|
187
|
+
await this.waitForInitialized();
|
|
188
|
+
return this.acquireLock(
|
|
189
|
+
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
|
|
190
|
+
{
|
|
191
|
+
timeoutMs: options?.timeoutMs
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
|
|
197
|
+
await this.waitForInitialized();
|
|
198
|
+
return this.acquireLock(
|
|
199
|
+
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
|
|
200
|
+
{
|
|
201
|
+
timeoutMs: options?.timeoutMs
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
|
|
207
|
+
await this.waitForInitialized();
|
|
208
|
+
|
|
209
|
+
if (this.closing) {
|
|
210
|
+
throw new Error(`Cannot acquire lock, the database is closing`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const abortController = new AbortController();
|
|
214
|
+
this.pendingAbortControllers.add(abortController);
|
|
215
|
+
const { timeoutMs } = options ?? {};
|
|
216
|
+
|
|
217
|
+
const timoutId = timeoutMs
|
|
218
|
+
? setTimeout(() => {
|
|
219
|
+
abortController.abort(`Timeout after ${timeoutMs}ms`);
|
|
220
|
+
this.pendingAbortControllers.delete(abortController);
|
|
221
|
+
}, timeoutMs)
|
|
222
|
+
: null;
|
|
223
|
+
|
|
224
|
+
return getNavigatorLocks().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, () => {
|
|
225
|
+
this.pendingAbortControllers.delete(abortController);
|
|
226
|
+
if (timoutId) {
|
|
227
|
+
clearTimeout(timoutId);
|
|
228
|
+
}
|
|
229
|
+
return callback();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
|
|
234
|
+
return this.readLock(this.wrapTransaction(fn));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
writeTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
|
|
238
|
+
return this.writeLock(this.wrapTransaction(fn, true));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private generateDBHelpers<
|
|
242
|
+
T extends {
|
|
243
|
+
execute: (sql: string, params?: any[]) => Promise<QueryResult>;
|
|
244
|
+
executeRaw: (sql: string, params?: any[]) => Promise<any[][]>;
|
|
245
|
+
}
|
|
246
|
+
>(tx: T): T & DBGetUtils {
|
|
247
|
+
return {
|
|
248
|
+
...tx,
|
|
249
|
+
/**
|
|
250
|
+
* Execute a read-only query and return results
|
|
251
|
+
*/
|
|
252
|
+
async getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
|
|
253
|
+
const res = await tx.execute(sql, parameters);
|
|
254
|
+
return res.rows?._array ?? [];
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Execute a read-only query and return the first result, or null if the ResultSet is empty.
|
|
259
|
+
*/
|
|
260
|
+
async getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
|
|
261
|
+
const res = await tx.execute(sql, parameters);
|
|
262
|
+
return res.rows?.item(0) ?? null;
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Execute a read-only query and return the first result, error if the ResultSet is empty.
|
|
267
|
+
*/
|
|
268
|
+
async get<T>(sql: string, parameters?: any[]): Promise<T> {
|
|
269
|
+
const res = await tx.execute(sql, parameters);
|
|
270
|
+
const first = res.rows?.item(0);
|
|
271
|
+
if (!first) {
|
|
272
|
+
throw new Error('Result set is empty');
|
|
273
|
+
}
|
|
274
|
+
return first;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Wraps a lock context into a transaction context
|
|
281
|
+
*/
|
|
282
|
+
private wrapTransaction<T>(cb: (tx: Transaction) => Promise<T>, write = false) {
|
|
283
|
+
return async (tx: LockContext): Promise<T> => {
|
|
284
|
+
await this._execute(write ? 'BEGIN EXCLUSIVE' : 'BEGIN');
|
|
285
|
+
let finalized = false;
|
|
286
|
+
const commit = async (): Promise<QueryResult> => {
|
|
287
|
+
if (finalized) {
|
|
288
|
+
return { rowsAffected: 0 };
|
|
289
|
+
}
|
|
290
|
+
finalized = true;
|
|
291
|
+
return this._execute('COMMIT');
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const rollback = () => {
|
|
295
|
+
finalized = true;
|
|
296
|
+
return this._execute('ROLLBACK');
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const result = await cb({
|
|
301
|
+
...tx,
|
|
302
|
+
commit,
|
|
303
|
+
rollback
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!finalized) {
|
|
307
|
+
await commit();
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
} catch (ex) {
|
|
311
|
+
this.logger.debug('Caught ex in transaction', ex);
|
|
312
|
+
try {
|
|
313
|
+
await rollback();
|
|
314
|
+
} catch (ex2) {
|
|
315
|
+
// In rare cases, a rollback may fail.
|
|
316
|
+
// Safe to ignore.
|
|
317
|
+
}
|
|
318
|
+
throw ex;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Wraps the worker execute function, awaiting for it to be available
|
|
325
|
+
*/
|
|
326
|
+
private _execute = async (sql: string, bindings?: any[]): Promise<QueryResult> => {
|
|
327
|
+
await this.waitForInitialized();
|
|
328
|
+
|
|
329
|
+
const result = await this.baseDB.execute(sql, bindings);
|
|
330
|
+
return {
|
|
331
|
+
...result,
|
|
332
|
+
rows: {
|
|
333
|
+
...result.rows,
|
|
334
|
+
item: (idx: number) => result.rows._array[idx]
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Wraps the worker executeRaw function, awaiting for it to be available
|
|
341
|
+
*/
|
|
342
|
+
private _executeRaw = async (sql: string, bindings?: any[]): Promise<any[][]> => {
|
|
343
|
+
await this.waitForInitialized();
|
|
344
|
+
return await this.baseDB.executeRaw(sql, bindings);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Wraps the worker executeBatch function, awaiting for it to be available
|
|
349
|
+
*/
|
|
350
|
+
private _executeBatch = async (query: string, params?: any[]): Promise<QueryResult> => {
|
|
351
|
+
await this.waitForInitialized();
|
|
352
|
+
const result = await this.baseDB.executeBatch(query, params);
|
|
353
|
+
return {
|
|
354
|
+
...result,
|
|
355
|
+
rows: undefined
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseObserver,
|
|
3
|
+
DBAdapterListener,
|
|
4
|
+
DBAdapter,
|
|
5
|
+
DBLockOptions,
|
|
6
|
+
LockContext,
|
|
7
|
+
QueryResult,
|
|
8
|
+
Transaction
|
|
9
|
+
} from '@powersync/common';
|
|
10
|
+
|
|
11
|
+
import { Mutex } from 'async-mutex';
|
|
12
|
+
|
|
13
|
+
const MOCK_QUERY_RESPONSE: QueryResult = {
|
|
14
|
+
rowsAffected: 0
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Implements a Mock DB adapter for use in Server Side Rendering (SSR).
|
|
19
|
+
* This adapter will return empty results for queries, which will allow
|
|
20
|
+
* server rendered views to initially generate scaffolding components
|
|
21
|
+
*/
|
|
22
|
+
export class SSRDBAdapter extends BaseObserver<DBAdapterListener> implements DBAdapter {
|
|
23
|
+
name: string;
|
|
24
|
+
readMutex: Mutex;
|
|
25
|
+
writeMutex: Mutex;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.name = 'SSR DB';
|
|
30
|
+
this.readMutex = new Mutex();
|
|
31
|
+
this.writeMutex = new Mutex();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
close() {}
|
|
35
|
+
|
|
36
|
+
async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) {
|
|
37
|
+
return this.readMutex.runExclusive(() => fn(this));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) {
|
|
41
|
+
return this.readLock(() => fn(this.generateMockTransactionContext()));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) {
|
|
45
|
+
return this.writeMutex.runExclusive(() => fn(this));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async writeTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) {
|
|
49
|
+
return this.writeLock(() => fn(this.generateMockTransactionContext()));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async execute(query: string, params?: any[]): Promise<QueryResult> {
|
|
53
|
+
return this.writeMutex.runExclusive(async () => MOCK_QUERY_RESPONSE);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async executeRaw(query: string, params?: any[]): Promise<any[][]> {
|
|
57
|
+
return this.writeMutex.runExclusive(async () => []);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async executeBatch(query: string, params?: any[][]): Promise<QueryResult> {
|
|
61
|
+
return this.writeMutex.runExclusive(async () => MOCK_QUERY_RESPONSE);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getOptional<T>(sql: string, parameters?: any[] | undefined): Promise<T | null> {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async get<T>(sql: string, parameters?: any[] | undefined): Promise<T> {
|
|
73
|
+
throw new Error(`No values are returned in SSR mode`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generates a mock context for use in read/write transactions.
|
|
78
|
+
* `this` already mocks most of the API, commit and rollback mocks
|
|
79
|
+
* are added here
|
|
80
|
+
*/
|
|
81
|
+
private generateMockTransactionContext(): Transaction {
|
|
82
|
+
return {
|
|
83
|
+
...this,
|
|
84
|
+
commit: async () => {
|
|
85
|
+
return MOCK_QUERY_RESPONSE;
|
|
86
|
+
},
|
|
87
|
+
rollback: async () => {
|
|
88
|
+
return MOCK_QUERY_RESPONSE;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async refreshSchema(): Promise<void> {}
|
|
94
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DBAdapter } from '@powersync/common';
|
|
2
|
+
import { ResolvedWebSQLOpenOptions } from './web-sql-flags';
|
|
3
|
+
|
|
4
|
+
export type SharedConnectionWorker = {
|
|
5
|
+
identifier: string;
|
|
6
|
+
port: MessagePort;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface WebDBAdapter extends DBAdapter {
|
|
10
|
+
/**
|
|
11
|
+
* Get a MessagePort which can be used to share the internals of this connection.
|
|
12
|
+
*/
|
|
13
|
+
shareConnection(): Promise<SharedConnectionWorker>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the config options used to open this connection.
|
|
17
|
+
* This is useful for sharing connections.
|
|
18
|
+
*/
|
|
19
|
+
getConfiguration(): ResolvedWebSQLOpenOptions;
|
|
20
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as Comlink from 'comlink';
|
|
2
|
+
import {
|
|
3
|
+
AsyncDatabaseConnection,
|
|
4
|
+
OnTableChangeCallback,
|
|
5
|
+
OpenAsyncDatabaseConnection,
|
|
6
|
+
ProxiedQueryResult
|
|
7
|
+
} from './AsyncDatabaseConnection';
|
|
8
|
+
import { ResolvedWebSQLOpenOptions } from './web-sql-flags';
|
|
9
|
+
|
|
10
|
+
export type SharedConnectionWorker = {
|
|
11
|
+
identifier: string;
|
|
12
|
+
port: MessagePort;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type WrappedWorkerConnectionOptions<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions> = {
|
|
16
|
+
baseConnection: AsyncDatabaseConnection;
|
|
17
|
+
identifier: string;
|
|
18
|
+
remoteCanCloseUnexpectedly: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Need a remote in order to keep a reference to the Proxied worker
|
|
21
|
+
*/
|
|
22
|
+
remote: Comlink.Remote<OpenAsyncDatabaseConnection<Config>>;
|
|
23
|
+
onClose?: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy
|
|
28
|
+
* functions for worker listeners.
|
|
29
|
+
*/
|
|
30
|
+
export class WorkerWrappedAsyncDatabaseConnection<Config extends ResolvedWebSQLOpenOptions = ResolvedWebSQLOpenOptions>
|
|
31
|
+
implements AsyncDatabaseConnection
|
|
32
|
+
{
|
|
33
|
+
protected lockAbortController = new AbortController();
|
|
34
|
+
protected notifyRemoteClosed: AbortController | undefined;
|
|
35
|
+
|
|
36
|
+
constructor(protected options: WrappedWorkerConnectionOptions<Config>) {
|
|
37
|
+
if (options.remoteCanCloseUnexpectedly) {
|
|
38
|
+
this.notifyRemoteClosed = new AbortController();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected get baseConnection() {
|
|
43
|
+
return this.options.baseConnection;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
init(): Promise<void> {
|
|
47
|
+
return this.baseConnection.init();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Marks the remote as closed.
|
|
52
|
+
*
|
|
53
|
+
* This can sometimes happen outside of our control, e.g. when a shared worker requests a connection from a tab. When
|
|
54
|
+
* it happens, all methods on the {@link baseConnection} would never resolve. To avoid livelocks in this scenario, we
|
|
55
|
+
* throw on all outstanding promises and forbid new calls.
|
|
56
|
+
*/
|
|
57
|
+
markRemoteClosed() {
|
|
58
|
+
// Can non-null assert here because this function is only supposed to be called when remoteCanCloseUnexpectedly was
|
|
59
|
+
// set.
|
|
60
|
+
this.notifyRemoteClosed!.abort();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private withRemote<T>(workerPromise: () => Promise<T>): Promise<T> {
|
|
64
|
+
const controller = this.notifyRemoteClosed;
|
|
65
|
+
if (controller) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
if (controller.signal.aborted) {
|
|
68
|
+
reject(new Error('Called operation on closed remote'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleAbort() {
|
|
72
|
+
reject(new Error('Remote peer closed with request in flight'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function completePromise(action: () => void) {
|
|
76
|
+
controller!.signal.removeEventListener('abort', handleAbort);
|
|
77
|
+
action();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
controller.signal.addEventListener('abort', handleAbort);
|
|
81
|
+
|
|
82
|
+
workerPromise()
|
|
83
|
+
.then((data) => completePromise(() => resolve(data)))
|
|
84
|
+
.catch((e) => completePromise(() => reject(e)));
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
// Can't close, so just return the inner worker promise unguarded.
|
|
88
|
+
return workerPromise();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get a MessagePort which can be used to share the internals of this connection.
|
|
94
|
+
*/
|
|
95
|
+
async shareConnection(): Promise<SharedConnectionWorker> {
|
|
96
|
+
const { identifier, remote } = this.options;
|
|
97
|
+
/**
|
|
98
|
+
* Hold a navigator lock in order to avoid features such as Chrome's frozen tabs,
|
|
99
|
+
* or Edge's sleeping tabs from pausing the thread for this connection.
|
|
100
|
+
* This promise resolves once a lock is obtained.
|
|
101
|
+
* This lock will be held as long as this connection is open.
|
|
102
|
+
* The `shareConnection` method should not be called on multiple tabs concurrently.
|
|
103
|
+
*/
|
|
104
|
+
await new Promise<void>((resolve, reject) =>
|
|
105
|
+
navigator.locks
|
|
106
|
+
.request(
|
|
107
|
+
`shared-connection-${this.options.identifier}-${Date.now()}-${Math.round(Math.random() * 10000)}`,
|
|
108
|
+
{
|
|
109
|
+
signal: this.lockAbortController.signal
|
|
110
|
+
},
|
|
111
|
+
async () => {
|
|
112
|
+
resolve();
|
|
113
|
+
|
|
114
|
+
// Free the lock when the connection is already closed.
|
|
115
|
+
if (this.lockAbortController.signal.aborted) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Hold the lock while the shared connection is in use.
|
|
120
|
+
await new Promise<void>((releaseLock) => {
|
|
121
|
+
this.lockAbortController.signal.addEventListener('abort', () => {
|
|
122
|
+
releaseLock();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
// We aren't concerned with abort errors here
|
|
128
|
+
.catch((ex) => {
|
|
129
|
+
if (ex.name == 'AbortError') {
|
|
130
|
+
resolve();
|
|
131
|
+
} else {
|
|
132
|
+
reject(ex);
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const newPort = await remote[Comlink.createEndpoint]();
|
|
138
|
+
return { port: newPort, identifier };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Registers a table change notification callback with the base database.
|
|
143
|
+
* This can be extended by custom implementations in order to handle proxy events.
|
|
144
|
+
*/
|
|
145
|
+
async registerOnTableChange(callback: OnTableChangeCallback) {
|
|
146
|
+
return this.baseConnection.registerOnTableChange(Comlink.proxy(callback));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async close(): Promise<void> {
|
|
150
|
+
// Abort any pending lock requests.
|
|
151
|
+
this.lockAbortController.abort();
|
|
152
|
+
try {
|
|
153
|
+
await this.withRemote(() => this.baseConnection.close());
|
|
154
|
+
} finally {
|
|
155
|
+
this.options.remote[Comlink.releaseProxy]();
|
|
156
|
+
this.options.onClose?.();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
execute(sql: string, params?: any[]): Promise<ProxiedQueryResult> {
|
|
161
|
+
return this.withRemote(() => this.baseConnection.execute(sql, params));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
executeRaw(sql: string, params?: any[]): Promise<any[][]> {
|
|
165
|
+
return this.withRemote(() => this.baseConnection.executeRaw(sql, params));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
executeBatch(sql: string, params?: any[]): Promise<ProxiedQueryResult> {
|
|
169
|
+
return this.withRemote(() => this.baseConnection.executeBatch(sql, params));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getConfig(): Promise<ResolvedWebSQLOpenOptions> {
|
|
173
|
+
return this.withRemote(() => this.baseConnection.getConfig());
|
|
174
|
+
}
|
|
175
|
+
}
|