@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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supports both shared and dedicated workers, based on how the worker is constructed (new SharedWorker vs new Worker()).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import '@journeyapps/wa-sqlite';
|
|
6
|
+
import { createBaseLogger, createLogger } from '@powersync/common';
|
|
7
|
+
import * as Comlink from 'comlink';
|
|
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';
|
|
14
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
15
|
+
|
|
16
|
+
const baseLogger = createBaseLogger();
|
|
17
|
+
baseLogger.useDefaults();
|
|
18
|
+
const logger = createLogger('db-worker');
|
|
19
|
+
|
|
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
|
+
const DBMap = new Map<string, SharedDBWorkerConnection>();
|
|
30
|
+
const OPEN_DB_LOCK = 'open-wasqlite-db';
|
|
31
|
+
|
|
32
|
+
let nextClientId = 1;
|
|
33
|
+
|
|
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
|
+
const openDBShared = async (options: WorkerDBOpenerOptions): Promise<AsyncDatabaseConnection> => {
|
|
51
|
+
// Prevent multiple simultaneous opens from causing race conditions
|
|
52
|
+
return getNavigatorLocks().request(OPEN_DB_LOCK, async () => {
|
|
53
|
+
const clientId = nextClientId++;
|
|
54
|
+
const { dbFilename, logLevel } = options;
|
|
55
|
+
|
|
56
|
+
logger.setLevel(logLevel);
|
|
57
|
+
|
|
58
|
+
if (!DBMap.has(dbFilename)) {
|
|
59
|
+
const clientIds = new Set<number>();
|
|
60
|
+
const connection = await openWorkerConnection(options);
|
|
61
|
+
await connection.init();
|
|
62
|
+
DBMap.set(dbFilename, {
|
|
63
|
+
clientIds,
|
|
64
|
+
db: connection
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
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
|
+
};
|
|
90
|
+
|
|
91
|
+
return Comlink.proxy(wrappedConnection);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Check if we're in a SharedWorker context
|
|
96
|
+
if (typeof SharedWorkerGlobalScope !== 'undefined') {
|
|
97
|
+
const _self: SharedWorkerGlobalScope = self as any;
|
|
98
|
+
_self.onconnect = function (event: MessageEvent<string>) {
|
|
99
|
+
const port = event.ports[0];
|
|
100
|
+
Comlink.expose(openDBShared, port);
|
|
101
|
+
};
|
|
102
|
+
} else {
|
|
103
|
+
// A dedicated worker can be shared externally
|
|
104
|
+
Comlink.expose(openDBShared);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
addEventListener('unload', () => {
|
|
108
|
+
Array.from(DBMap.values()).forEach(async (dbConnection) => {
|
|
109
|
+
const { db } = dbConnection;
|
|
110
|
+
db.close?.();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as Comlink from 'comlink';
|
|
2
|
+
import { OpenAsyncDatabaseConnection } from '../..//db/adapters/AsyncDatabaseConnection';
|
|
3
|
+
import { WASQLiteVFS } from '../../db/adapters/wa-sqlite/WASQLiteConnection';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opens a shared or dedicated worker which exposes opening of database connections
|
|
7
|
+
*/
|
|
8
|
+
export function openWorkerDatabasePort(
|
|
9
|
+
workerIdentifier: string,
|
|
10
|
+
multipleTabs = true,
|
|
11
|
+
worker: string | URL = '',
|
|
12
|
+
vfs?: WASQLiteVFS
|
|
13
|
+
) {
|
|
14
|
+
const needsDedicated = vfs == WASQLiteVFS.AccessHandlePoolVFS || vfs == WASQLiteVFS.OPFSCoopSyncVFS;
|
|
15
|
+
|
|
16
|
+
if (worker) {
|
|
17
|
+
return !needsDedicated && multipleTabs
|
|
18
|
+
? new SharedWorker(`${worker}`, {
|
|
19
|
+
/* @vite-ignore */
|
|
20
|
+
name: `shared-DB-worker-${workerIdentifier}`
|
|
21
|
+
}).port
|
|
22
|
+
: new Worker(`${worker}`, {
|
|
23
|
+
/* @vite-ignore */
|
|
24
|
+
name: `DB-worker-${workerIdentifier}`
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
/**
|
|
28
|
+
* Webpack V5 can bundle the worker automatically if the full Worker constructor syntax is used
|
|
29
|
+
* https://webpack.js.org/guides/web-workers/
|
|
30
|
+
* This enables multi tab support by default, but falls back if SharedWorker is not available
|
|
31
|
+
* (in the case of Android)
|
|
32
|
+
*/
|
|
33
|
+
return !needsDedicated && multipleTabs
|
|
34
|
+
? new SharedWorker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
|
|
35
|
+
/* @vite-ignore */
|
|
36
|
+
name: `shared-DB-worker-${workerIdentifier}`,
|
|
37
|
+
type: 'module'
|
|
38
|
+
}).port
|
|
39
|
+
: new Worker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
|
|
40
|
+
/* @vite-ignore */
|
|
41
|
+
name: `DB-worker-${workerIdentifier}`,
|
|
42
|
+
type: 'module'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @returns A function which allows for opening database connections inside
|
|
49
|
+
* a worker.
|
|
50
|
+
*/
|
|
51
|
+
export function getWorkerDatabaseOpener(workerIdentifier: string, multipleTabs = true, worker: string | URL = '') {
|
|
52
|
+
return Comlink.wrap<OpenAsyncDatabaseConnection>(openWorkerDatabasePort(workerIdentifier, multipleTabs, worker));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveWorkerDatabasePortFactory(worker: () => Worker | SharedWorker) {
|
|
56
|
+
const workerInstance = worker();
|
|
57
|
+
return isSharedWorker(workerInstance) ? workerInstance.port : workerInstance;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isSharedWorker(worker: Worker | SharedWorker): worker is SharedWorker {
|
|
61
|
+
return 'port' in worker;
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PowerSyncCredentials, SyncStatusOptions } from '@powersync/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The client side port should provide these methods.
|
|
5
|
+
*/
|
|
6
|
+
export abstract class AbstractSharedSyncClientProvider {
|
|
7
|
+
abstract fetchCredentials(): Promise<PowerSyncCredentials | null>;
|
|
8
|
+
abstract invalidateCredentials(): void;
|
|
9
|
+
abstract uploadCrud(): Promise<void>;
|
|
10
|
+
abstract statusChanged(status: SyncStatusOptions): void;
|
|
11
|
+
abstract getDBWorkerPort(): Promise<MessagePort>;
|
|
12
|
+
|
|
13
|
+
abstract trace(...x: any[]): void;
|
|
14
|
+
abstract debug(...x: any[]): void;
|
|
15
|
+
abstract info(...x: any[]): void;
|
|
16
|
+
abstract log(...x: any[]): void;
|
|
17
|
+
abstract warn(...x: any[]): void;
|
|
18
|
+
abstract error(...x: any[]): void;
|
|
19
|
+
abstract time(label: string): void;
|
|
20
|
+
abstract timeEnd(label: string): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { type ILogger, type ILogLevel, LogLevel } from '@powersync/common';
|
|
2
|
+
import { type WrappedSyncPort } from './SharedSyncImplementation';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Broadcasts logs to all clients
|
|
6
|
+
*/
|
|
7
|
+
export class BroadcastLogger implements ILogger {
|
|
8
|
+
TRACE: ILogLevel;
|
|
9
|
+
DEBUG: ILogLevel;
|
|
10
|
+
INFO: ILogLevel;
|
|
11
|
+
TIME: ILogLevel;
|
|
12
|
+
WARN: ILogLevel;
|
|
13
|
+
ERROR: ILogLevel;
|
|
14
|
+
OFF: ILogLevel;
|
|
15
|
+
|
|
16
|
+
private currentLevel: ILogLevel = LogLevel.INFO;
|
|
17
|
+
|
|
18
|
+
constructor(protected clients: WrappedSyncPort[]) {
|
|
19
|
+
this.TRACE = LogLevel.TRACE;
|
|
20
|
+
this.DEBUG = LogLevel.DEBUG;
|
|
21
|
+
this.INFO = LogLevel.INFO;
|
|
22
|
+
this.TIME = LogLevel.TIME;
|
|
23
|
+
this.WARN = LogLevel.WARN;
|
|
24
|
+
this.ERROR = LogLevel.ERROR;
|
|
25
|
+
this.OFF = LogLevel.OFF;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
trace(...x: any[]): void {
|
|
29
|
+
if (!this.enabledFor(this.TRACE)) return;
|
|
30
|
+
|
|
31
|
+
console.trace(...x);
|
|
32
|
+
const sanitized = this.sanitizeArgs(x);
|
|
33
|
+
this.iterateClients((client) => client.clientProvider.trace(...sanitized));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
debug(...x: any[]): void {
|
|
37
|
+
if (!this.enabledFor(this.DEBUG)) return;
|
|
38
|
+
|
|
39
|
+
console.debug(...x);
|
|
40
|
+
const sanitized = this.sanitizeArgs(x);
|
|
41
|
+
this.iterateClients((client) => client.clientProvider.debug(...sanitized));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
info(...x: any[]): void {
|
|
45
|
+
if (!this.enabledFor(this.INFO)) return;
|
|
46
|
+
|
|
47
|
+
console.info(...x);
|
|
48
|
+
const sanitized = this.sanitizeArgs(x);
|
|
49
|
+
this.iterateClients((client) => client.clientProvider.info(...sanitized));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
log(...x: any[]): void {
|
|
53
|
+
if (!this.enabledFor(this.INFO)) return;
|
|
54
|
+
|
|
55
|
+
console.log(...x);
|
|
56
|
+
const sanitized = this.sanitizeArgs(x);
|
|
57
|
+
this.iterateClients((client) => client.clientProvider.log(...sanitized));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
warn(...x: any[]): void {
|
|
61
|
+
if (!this.enabledFor(this.WARN)) return;
|
|
62
|
+
|
|
63
|
+
console.warn(...x);
|
|
64
|
+
const sanitized = this.sanitizeArgs(x);
|
|
65
|
+
this.iterateClients((client) => client.clientProvider.warn(...sanitized));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
error(...x: any[]): void {
|
|
69
|
+
if (!this.enabledFor(this.ERROR)) return;
|
|
70
|
+
|
|
71
|
+
console.error(...x);
|
|
72
|
+
const sanitized = this.sanitizeArgs(x);
|
|
73
|
+
this.iterateClients((client) => client.clientProvider.error(...sanitized));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
time(label: string): void {
|
|
77
|
+
if (!this.enabledFor(this.TIME)) return;
|
|
78
|
+
|
|
79
|
+
console.time(label);
|
|
80
|
+
this.iterateClients((client) => client.clientProvider.time(label));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
timeEnd(label: string): void {
|
|
84
|
+
if (!this.enabledFor(this.TIME)) return;
|
|
85
|
+
|
|
86
|
+
console.timeEnd(label);
|
|
87
|
+
this.iterateClients((client) => client.clientProvider.timeEnd(label));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Set the global log level.
|
|
92
|
+
*/
|
|
93
|
+
setLevel(level: ILogLevel): void {
|
|
94
|
+
this.currentLevel = level;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the current log level.
|
|
99
|
+
*/
|
|
100
|
+
getLevel(): ILogLevel {
|
|
101
|
+
return this.currentLevel;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns true if the given level is enabled.
|
|
106
|
+
*/
|
|
107
|
+
enabledFor(level: ILogLevel): boolean {
|
|
108
|
+
return level.value >= this.currentLevel.value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Iterates all clients, catches individual client exceptions
|
|
113
|
+
* and proceeds to execute for all clients.
|
|
114
|
+
*/
|
|
115
|
+
protected async iterateClients(callback: (client: WrappedSyncPort) => Promise<void>) {
|
|
116
|
+
for (const client of this.clients) {
|
|
117
|
+
try {
|
|
118
|
+
await callback(client);
|
|
119
|
+
} catch (ex) {
|
|
120
|
+
console.error('Caught exception when iterating client', ex);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Guards against any logging errors.
|
|
127
|
+
* We don't want a logging exception to cause further issues upstream
|
|
128
|
+
*/
|
|
129
|
+
protected sanitizeArgs(x: any[]): any[] {
|
|
130
|
+
const sanitizedParams = x.map((param) => {
|
|
131
|
+
try {
|
|
132
|
+
// Try and clone here first. If it fails it won't be passable over a MessagePort
|
|
133
|
+
return structuredClone(param);
|
|
134
|
+
} catch (ex) {
|
|
135
|
+
console.error(ex);
|
|
136
|
+
return 'Could not serialize log params. Check shared worker logs for more details.';
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return sanitizedParams;
|
|
141
|
+
}
|
|
142
|
+
}
|