@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.
Files changed (40) hide show
  1. package/dist/index.umd.js +64 -26
  2. package/dist/index.umd.js.map +1 -1
  3. package/dist/worker/SharedSyncImplementation.umd.js +13640 -440
  4. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -1
  5. package/dist/worker/WASQLiteDB.umd.js +13467 -161
  6. package/dist/worker/WASQLiteDB.umd.js.map +1 -1
  7. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js +66 -38
  8. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +1 -1
  9. package/lib/package.json +6 -5
  10. package/lib/tsconfig.tsbuildinfo +1 -1
  11. package/package.json +7 -6
  12. package/src/db/PowerSyncDatabase.ts +224 -0
  13. package/src/db/adapters/AbstractWebPowerSyncDatabaseOpenFactory.ts +47 -0
  14. package/src/db/adapters/AbstractWebSQLOpenFactory.ts +48 -0
  15. package/src/db/adapters/AsyncDatabaseConnection.ts +40 -0
  16. package/src/db/adapters/LockedAsyncDatabaseAdapter.ts +358 -0
  17. package/src/db/adapters/SSRDBAdapter.ts +94 -0
  18. package/src/db/adapters/WebDBAdapter.ts +20 -0
  19. package/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +175 -0
  20. package/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +444 -0
  21. package/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts +86 -0
  22. package/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +134 -0
  23. package/src/db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory.ts +24 -0
  24. package/src/db/adapters/web-sql-flags.ts +135 -0
  25. package/src/db/sync/SSRWebStreamingSyncImplementation.ts +89 -0
  26. package/src/db/sync/SharedWebStreamingSyncImplementation.ts +274 -0
  27. package/src/db/sync/WebRemote.ts +59 -0
  28. package/src/db/sync/WebStreamingSyncImplementation.ts +34 -0
  29. package/src/db/sync/userAgent.ts +78 -0
  30. package/src/index.ts +13 -0
  31. package/src/shared/navigator.ts +9 -0
  32. package/src/worker/db/WASQLiteDB.worker.ts +112 -0
  33. package/src/worker/db/open-worker-database.ts +62 -0
  34. package/src/worker/sync/AbstractSharedSyncClientProvider.ts +21 -0
  35. package/src/worker/sync/BroadcastLogger.ts +142 -0
  36. package/src/worker/sync/SharedSyncImplementation.ts +520 -0
  37. package/src/worker/sync/SharedSyncImplementation.worker.ts +14 -0
  38. package/src/worker/sync/WorkerClient.ts +106 -0
  39. package/dist/worker/node_modules_crypto-browserify_index_js.umd.js +0 -33734
  40. 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
+ }