@powersync/web 1.28.0 → 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,135 @@
|
|
|
1
|
+
import { type ILogger, SQLOpenOptions } from '@powersync/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common settings used when creating SQL connections on web.
|
|
5
|
+
*/
|
|
6
|
+
export interface WebSQLFlags {
|
|
7
|
+
/**
|
|
8
|
+
* Broadcast logs from shared workers, such as the shared sync worker,
|
|
9
|
+
* to individual tabs. This defaults to true.
|
|
10
|
+
*/
|
|
11
|
+
broadcastLogs?: boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SQLite operations are currently not supported in SSR mode.
|
|
15
|
+
* A warning will be logged if attempting to use SQLite in SSR.
|
|
16
|
+
* Setting this to `true` will disabled the warning above.
|
|
17
|
+
*/
|
|
18
|
+
disableSSRWarning?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enables multi tab support
|
|
22
|
+
*/
|
|
23
|
+
enableMultiTabs?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The SQLite connection is often executed through a web worker
|
|
27
|
+
* in order to offload computation. This can be used to manually
|
|
28
|
+
* disable the use of web workers in environments where web workers
|
|
29
|
+
* might be unstable.
|
|
30
|
+
*/
|
|
31
|
+
useWebWorker?: boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Open in SSR placeholder mode. DB operations and Sync operations will be a No-op
|
|
35
|
+
*/
|
|
36
|
+
ssrMode?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ResolvedWebSQLFlags = Required<WebSQLFlags>;
|
|
40
|
+
|
|
41
|
+
export interface ResolvedWebSQLOpenOptions extends SQLOpenOptions {
|
|
42
|
+
flags: ResolvedWebSQLFlags;
|
|
43
|
+
/**
|
|
44
|
+
* Where to store SQLite temporary files. Defaults to 'MEMORY'.
|
|
45
|
+
* Setting this to `FILESYSTEM` can cause issues with larger queries or datasets.
|
|
46
|
+
*/
|
|
47
|
+
temporaryStorage: TemporaryStorageOption;
|
|
48
|
+
|
|
49
|
+
cacheSizeKb: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Encryption key for the database.
|
|
53
|
+
* If set, the database will be encrypted using ChaCha20.
|
|
54
|
+
*/
|
|
55
|
+
encryptionKey?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export enum TemporaryStorageOption {
|
|
59
|
+
MEMORY = 'memory',
|
|
60
|
+
FILESYSTEM = 'file'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const DEFAULT_CACHE_SIZE_KB = 50 * 1024;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Options for opening a Web SQL connection
|
|
67
|
+
*/
|
|
68
|
+
export interface WebSQLOpenFactoryOptions extends SQLOpenOptions {
|
|
69
|
+
flags?: WebSQLFlags;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Allows you to override the default wasqlite db worker.
|
|
73
|
+
*
|
|
74
|
+
* You can either provide a path to the worker script
|
|
75
|
+
* or a factory method that returns a worker.
|
|
76
|
+
*/
|
|
77
|
+
worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => Worker | SharedWorker);
|
|
78
|
+
|
|
79
|
+
logger?: ILogger;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Where to store SQLite temporary files. Defaults to 'MEMORY'.
|
|
83
|
+
* Setting this to `FILESYSTEM` can cause issues with larger queries or datasets.
|
|
84
|
+
*
|
|
85
|
+
* For details, see: https://www.sqlite.org/pragma.html#pragma_temp_store
|
|
86
|
+
*/
|
|
87
|
+
temporaryStorage?: TemporaryStorageOption;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Maximum SQLite cache size. Defaults to 50MB.
|
|
91
|
+
*
|
|
92
|
+
* For details, see: https://www.sqlite.org/pragma.html#pragma_cache_size
|
|
93
|
+
*/
|
|
94
|
+
cacheSizeKb?: number;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Encryption key for the database.
|
|
98
|
+
* If set, the database will be encrypted using ChaCha20.
|
|
99
|
+
*/
|
|
100
|
+
encryptionKey?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isServerSide() {
|
|
104
|
+
return typeof window == 'undefined';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const DEFAULT_WEB_SQL_FLAGS: ResolvedWebSQLFlags = {
|
|
108
|
+
broadcastLogs: true,
|
|
109
|
+
disableSSRWarning: false,
|
|
110
|
+
ssrMode: isServerSide(),
|
|
111
|
+
/**
|
|
112
|
+
* Multiple tabs are by default not supported on Android, iOS and Safari.
|
|
113
|
+
* Other platforms will have multiple tabs enabled by default.
|
|
114
|
+
*/
|
|
115
|
+
enableMultiTabs:
|
|
116
|
+
typeof globalThis.navigator !== 'undefined' && // For SSR purposes
|
|
117
|
+
typeof SharedWorker !== 'undefined' &&
|
|
118
|
+
!navigator.userAgent.match(/(Android|iPhone|iPod|iPad)/i) &&
|
|
119
|
+
!(window as any).safari,
|
|
120
|
+
useWebWorker: true
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export function resolveWebSQLFlags(flags?: WebSQLFlags): ResolvedWebSQLFlags {
|
|
124
|
+
const resolvedFlags = {
|
|
125
|
+
...DEFAULT_WEB_SQL_FLAGS,
|
|
126
|
+
...(flags ?? {})
|
|
127
|
+
};
|
|
128
|
+
if (typeof flags?.enableMultiTabs != 'undefined') {
|
|
129
|
+
resolvedFlags.enableMultiTabs = flags.enableMultiTabs;
|
|
130
|
+
}
|
|
131
|
+
if (flags?.useWebWorker === false) {
|
|
132
|
+
resolvedFlags.enableMultiTabs = false;
|
|
133
|
+
}
|
|
134
|
+
return resolvedFlags;
|
|
135
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractStreamingSyncImplementationOptions,
|
|
3
|
+
BaseObserver,
|
|
4
|
+
LockOptions,
|
|
5
|
+
LockType,
|
|
6
|
+
PowerSyncConnectionOptions,
|
|
7
|
+
StreamingSyncImplementation,
|
|
8
|
+
SubscribedStream,
|
|
9
|
+
SyncStatus,
|
|
10
|
+
SyncStatusOptions
|
|
11
|
+
} from '@powersync/common';
|
|
12
|
+
import { Mutex } from 'async-mutex';
|
|
13
|
+
|
|
14
|
+
export class SSRStreamingSyncImplementation extends BaseObserver implements StreamingSyncImplementation {
|
|
15
|
+
syncMutex: Mutex;
|
|
16
|
+
crudMutex: Mutex;
|
|
17
|
+
|
|
18
|
+
isConnected: boolean;
|
|
19
|
+
lastSyncedAt?: Date | undefined;
|
|
20
|
+
syncStatus: SyncStatus;
|
|
21
|
+
|
|
22
|
+
constructor(options: AbstractStreamingSyncImplementationOptions) {
|
|
23
|
+
super();
|
|
24
|
+
this.syncMutex = new Mutex();
|
|
25
|
+
this.crudMutex = new Mutex();
|
|
26
|
+
this.syncStatus = new SyncStatus({});
|
|
27
|
+
this.isConnected = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
obtainLock<T>(lockOptions: LockOptions<T>): Promise<T> {
|
|
31
|
+
const mutex = lockOptions.type == LockType.CRUD ? this.crudMutex : this.syncMutex;
|
|
32
|
+
return mutex.runExclusive(lockOptions.callback);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* This is a no-op in SSR mode
|
|
37
|
+
*/
|
|
38
|
+
async connect(options?: PowerSyncConnectionOptions): Promise<void> {}
|
|
39
|
+
|
|
40
|
+
async dispose() {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* This is a no-op in SSR mode
|
|
44
|
+
*/
|
|
45
|
+
async disconnect(): Promise<void> {}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* This SSR Mode implementation is immediately ready.
|
|
49
|
+
*/
|
|
50
|
+
async waitForReady() {}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* This will never resolve in SSR Mode.
|
|
54
|
+
*/
|
|
55
|
+
async waitForStatus(status: SyncStatusOptions) {
|
|
56
|
+
return this.waitUntilStatusMatches(() => false);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* This will never resolve in SSR Mode.
|
|
61
|
+
*/
|
|
62
|
+
waitUntilStatusMatches(_predicate: (status: SyncStatus) => boolean): Promise<void> {
|
|
63
|
+
return new Promise<void>(() => {});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns a placeholder checkpoint. This should not be used.
|
|
68
|
+
*/
|
|
69
|
+
async getWriteCheckpoint() {
|
|
70
|
+
return '1';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The SSR mode adapter will never complete syncing.
|
|
75
|
+
*/
|
|
76
|
+
async hasCompletedSync() {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* This is a no-op in SSR mode.
|
|
82
|
+
*/
|
|
83
|
+
triggerCrudUpload() {}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* No-op in SSR mode.
|
|
87
|
+
*/
|
|
88
|
+
updateSubscriptions(): void {}
|
|
89
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PowerSyncConnectionOptions,
|
|
3
|
+
PowerSyncCredentials,
|
|
4
|
+
SubscribedStream,
|
|
5
|
+
SyncStatus,
|
|
6
|
+
SyncStatusOptions
|
|
7
|
+
} from '@powersync/common';
|
|
8
|
+
import * as Comlink from 'comlink';
|
|
9
|
+
import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
|
|
10
|
+
import { ManualSharedSyncPayload, SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation';
|
|
11
|
+
import { DEFAULT_CACHE_SIZE_KB, resolveWebSQLFlags, TemporaryStorageOption } from '../adapters/web-sql-flags';
|
|
12
|
+
import { WebDBAdapter } from '../adapters/WebDBAdapter';
|
|
13
|
+
import {
|
|
14
|
+
WebStreamingSyncImplementation,
|
|
15
|
+
WebStreamingSyncImplementationOptions
|
|
16
|
+
} from './WebStreamingSyncImplementation';
|
|
17
|
+
import { WorkerClient } from '../../worker/sync/WorkerClient';
|
|
18
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The shared worker will trigger methods on this side of the message port
|
|
22
|
+
* via this client provider.
|
|
23
|
+
*/
|
|
24
|
+
class SharedSyncClientProvider extends AbstractSharedSyncClientProvider {
|
|
25
|
+
constructor(
|
|
26
|
+
protected options: WebStreamingSyncImplementationOptions,
|
|
27
|
+
public statusChanged: (status: SyncStatusOptions) => void,
|
|
28
|
+
protected webDB: WebDBAdapter
|
|
29
|
+
) {
|
|
30
|
+
super();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getDBWorkerPort(): Promise<MessagePort> {
|
|
34
|
+
const { port } = await this.webDB.shareConnection();
|
|
35
|
+
return Comlink.transfer(port, [port]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
invalidateCredentials() {
|
|
39
|
+
this.options.remote.invalidateCredentials();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async fetchCredentials(): Promise<PowerSyncCredentials | null> {
|
|
43
|
+
const credentials = await this.options.remote.getCredentials();
|
|
44
|
+
if (credentials == null) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The credentials need to be serializable.
|
|
49
|
+
* Users might extend [PowerSyncCredentials] to contain
|
|
50
|
+
* items which are not serializable.
|
|
51
|
+
* This returns only the essential fields.
|
|
52
|
+
*/
|
|
53
|
+
return {
|
|
54
|
+
endpoint: credentials.endpoint,
|
|
55
|
+
token: credentials.token
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async uploadCrud(): Promise<void> {
|
|
60
|
+
/**
|
|
61
|
+
* Don't return anything here, just incase something which is not
|
|
62
|
+
* serializable is returned from the `uploadCrud` function.
|
|
63
|
+
*/
|
|
64
|
+
await this.options.uploadCrud();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get logger() {
|
|
68
|
+
return this.options.logger;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
trace(...x: any[]): void {
|
|
72
|
+
this.logger?.trace(...x);
|
|
73
|
+
}
|
|
74
|
+
debug(...x: any[]): void {
|
|
75
|
+
this.logger?.debug(...x);
|
|
76
|
+
}
|
|
77
|
+
info(...x: any[]): void {
|
|
78
|
+
this.logger?.info(...x);
|
|
79
|
+
}
|
|
80
|
+
log(...x: any[]): void {
|
|
81
|
+
this.logger?.log(...x);
|
|
82
|
+
}
|
|
83
|
+
warn(...x: any[]): void {
|
|
84
|
+
this.logger?.warn(...x);
|
|
85
|
+
}
|
|
86
|
+
error(...x: any[]): void {
|
|
87
|
+
this.logger?.error(...x);
|
|
88
|
+
}
|
|
89
|
+
time(label: string): void {
|
|
90
|
+
this.logger?.time(label);
|
|
91
|
+
}
|
|
92
|
+
timeEnd(label: string): void {
|
|
93
|
+
this.logger?.timeEnd(label);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SharedWebStreamingSyncImplementationOptions extends WebStreamingSyncImplementationOptions {
|
|
98
|
+
db: WebDBAdapter;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The local part of the sync implementation on the web, which talks to a sync implementation hosted in a shared worker.
|
|
103
|
+
*/
|
|
104
|
+
export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplementation {
|
|
105
|
+
protected syncManager: Comlink.Remote<WorkerClient>;
|
|
106
|
+
protected clientProvider: SharedSyncClientProvider;
|
|
107
|
+
protected messagePort: MessagePort;
|
|
108
|
+
|
|
109
|
+
protected isInitialized: Promise<void>;
|
|
110
|
+
protected dbAdapter: WebDBAdapter;
|
|
111
|
+
private abortOnClose = new AbortController();
|
|
112
|
+
|
|
113
|
+
constructor(options: SharedWebStreamingSyncImplementationOptions) {
|
|
114
|
+
super(options);
|
|
115
|
+
this.dbAdapter = options.db;
|
|
116
|
+
/**
|
|
117
|
+
* Configure or connect to the shared sync worker.
|
|
118
|
+
* This worker will manage all syncing operations remotely.
|
|
119
|
+
*/
|
|
120
|
+
const resolvedWorkerOptions = {
|
|
121
|
+
dbFilename: this.options.identifier!,
|
|
122
|
+
temporaryStorage: TemporaryStorageOption.MEMORY,
|
|
123
|
+
cacheSizeKb: DEFAULT_CACHE_SIZE_KB,
|
|
124
|
+
...options,
|
|
125
|
+
flags: resolveWebSQLFlags(options.flags)
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const syncWorker = options.sync?.worker;
|
|
129
|
+
if (syncWorker) {
|
|
130
|
+
if (typeof syncWorker === 'function') {
|
|
131
|
+
this.messagePort = syncWorker(resolvedWorkerOptions).port;
|
|
132
|
+
} else {
|
|
133
|
+
this.messagePort = new SharedWorker(`${syncWorker}`, {
|
|
134
|
+
/* @vite-ignore */
|
|
135
|
+
name: `shared-sync-${this.webOptions.identifier}`
|
|
136
|
+
}).port;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
this.messagePort = new SharedWorker(
|
|
140
|
+
new URL('../../worker/sync/SharedSyncImplementation.worker.js', import.meta.url),
|
|
141
|
+
{
|
|
142
|
+
/* @vite-ignore */
|
|
143
|
+
name: `shared-sync-${this.webOptions.identifier}`,
|
|
144
|
+
type: 'module'
|
|
145
|
+
}
|
|
146
|
+
).port;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.syncManager = Comlink.wrap<WorkerClient>(this.messagePort);
|
|
150
|
+
this.syncManager.setLogLevel(this.logger.getLevel());
|
|
151
|
+
|
|
152
|
+
this.triggerCrudUpload = this.syncManager.triggerCrudUpload;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Opens MessagePort to the existing shared DB worker.
|
|
156
|
+
* The sync worker cannot initiate connections directly to the
|
|
157
|
+
* DB worker, but a port to the DB worker can be transferred to the
|
|
158
|
+
* sync worker.
|
|
159
|
+
*/
|
|
160
|
+
const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
|
|
161
|
+
const flags = { ...this.webOptions.flags, workers: undefined };
|
|
162
|
+
|
|
163
|
+
this.isInitialized = this.syncManager.setParams(
|
|
164
|
+
{
|
|
165
|
+
dbParams: this.dbAdapter.getConfiguration(),
|
|
166
|
+
streamOptions: {
|
|
167
|
+
crudUploadThrottleMs,
|
|
168
|
+
identifier,
|
|
169
|
+
retryDelayMs,
|
|
170
|
+
flags: flags
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
options.subscriptions
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Pass along any sync status updates to this listener
|
|
178
|
+
*/
|
|
179
|
+
this.clientProvider = new SharedSyncClientProvider(
|
|
180
|
+
this.webOptions,
|
|
181
|
+
(status) => {
|
|
182
|
+
this.iterateListeners((l) => this.updateSyncStatus(status));
|
|
183
|
+
},
|
|
184
|
+
options.db
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The sync worker will call this client provider when it needs
|
|
189
|
+
* to fetch credentials or upload data.
|
|
190
|
+
* This performs bi-directional method calling.
|
|
191
|
+
*/
|
|
192
|
+
Comlink.expose(this.clientProvider, this.messagePort);
|
|
193
|
+
|
|
194
|
+
// Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which
|
|
195
|
+
// will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker
|
|
196
|
+
// to free resources associated with this tab.
|
|
197
|
+
getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => {
|
|
198
|
+
if (!this.abortOnClose.signal.aborted) {
|
|
199
|
+
this.syncManager.addLockBasedCloseSignal(lock!.name);
|
|
200
|
+
|
|
201
|
+
await new Promise<void>((r) => {
|
|
202
|
+
this.abortOnClose.signal.onabort = () => r();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Starts the sync process, this effectively acts as a call to
|
|
210
|
+
* `connect` if not yet connected.
|
|
211
|
+
*/
|
|
212
|
+
async connect(options?: PowerSyncConnectionOptions): Promise<void> {
|
|
213
|
+
await this.waitForReady();
|
|
214
|
+
return this.syncManager.connect(options);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async disconnect(): Promise<void> {
|
|
218
|
+
await this.waitForReady();
|
|
219
|
+
return this.syncManager.disconnect();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async getWriteCheckpoint(): Promise<string> {
|
|
223
|
+
await this.waitForReady();
|
|
224
|
+
return this.syncManager.getWriteCheckpoint();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async hasCompletedSync(): Promise<boolean> {
|
|
228
|
+
return this.syncManager.hasCompletedSync();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async dispose(): Promise<void> {
|
|
232
|
+
await this.waitForReady();
|
|
233
|
+
|
|
234
|
+
await super.dispose();
|
|
235
|
+
|
|
236
|
+
await new Promise<void>((resolve) => {
|
|
237
|
+
// Listen for the close acknowledgment from the worker
|
|
238
|
+
this.messagePort.addEventListener('message', (event) => {
|
|
239
|
+
const payload = event.data as ManualSharedSyncPayload;
|
|
240
|
+
if (payload?.event === SharedSyncClientEvent.CLOSE_ACK) {
|
|
241
|
+
resolve();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Signal the shared worker that this client is closing its connection to the worker
|
|
246
|
+
const closeMessagePayload: ManualSharedSyncPayload = {
|
|
247
|
+
event: SharedSyncClientEvent.CLOSE_CLIENT,
|
|
248
|
+
data: {}
|
|
249
|
+
};
|
|
250
|
+
this.messagePort.postMessage(closeMessagePayload);
|
|
251
|
+
});
|
|
252
|
+
this.abortOnClose.abort();
|
|
253
|
+
|
|
254
|
+
// Release the proxy
|
|
255
|
+
this.syncManager[Comlink.releaseProxy]();
|
|
256
|
+
this.messagePort.close();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async waitForReady() {
|
|
260
|
+
return this.isInitialized;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
updateSubscriptions(subscriptions: SubscribedStream[]): void {
|
|
264
|
+
this.syncManager.updateSubscriptions(subscriptions);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Used in tests to force a connection states
|
|
269
|
+
*/
|
|
270
|
+
private async _testUpdateStatus(status: SyncStatus) {
|
|
271
|
+
await this.isInitialized;
|
|
272
|
+
return this.syncManager._testUpdateAllStatuses(status.toJSON());
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractRemote,
|
|
3
|
+
AbstractRemoteOptions,
|
|
4
|
+
BSONImplementation,
|
|
5
|
+
DEFAULT_REMOTE_LOGGER,
|
|
6
|
+
FetchImplementation,
|
|
7
|
+
FetchImplementationProvider,
|
|
8
|
+
ILogger,
|
|
9
|
+
RemoteConnector
|
|
10
|
+
} from '@powersync/common';
|
|
11
|
+
|
|
12
|
+
import { getUserAgentInfo } from './userAgent';
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Depends on browser's implementation of global fetch.
|
|
16
|
+
*/
|
|
17
|
+
class WebFetchProvider extends FetchImplementationProvider {
|
|
18
|
+
getFetch(): FetchImplementation {
|
|
19
|
+
return fetch.bind(globalThis);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class WebRemote extends AbstractRemote {
|
|
24
|
+
private _bson: BSONImplementation | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
protected connector: RemoteConnector,
|
|
28
|
+
protected logger: ILogger = DEFAULT_REMOTE_LOGGER,
|
|
29
|
+
options?: Partial<AbstractRemoteOptions>
|
|
30
|
+
) {
|
|
31
|
+
super(connector, logger, {
|
|
32
|
+
...(options ?? {}),
|
|
33
|
+
fetchImplementation: options?.fetchImplementation ?? new WebFetchProvider()
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getUserAgent(): string {
|
|
38
|
+
let ua = [super.getUserAgent(), `powersync-web`];
|
|
39
|
+
try {
|
|
40
|
+
ua.push(...getUserAgentInfo());
|
|
41
|
+
} catch (e) {
|
|
42
|
+
this.logger.warn('Failed to get user agent info', e);
|
|
43
|
+
}
|
|
44
|
+
return ua.join(' ');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getBSON(): Promise<BSONImplementation> {
|
|
48
|
+
if (this._bson) {
|
|
49
|
+
return this._bson;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Dynamic import to be used only when needed.
|
|
54
|
+
*/
|
|
55
|
+
const { BSON } = await import('bson');
|
|
56
|
+
this._bson = BSON;
|
|
57
|
+
return this._bson;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractStreamingSyncImplementation,
|
|
3
|
+
AbstractStreamingSyncImplementationOptions,
|
|
4
|
+
LockOptions,
|
|
5
|
+
LockType
|
|
6
|
+
} from '@powersync/common';
|
|
7
|
+
import { getNavigatorLocks } from '../../shared/navigator';
|
|
8
|
+
import { ResolvedWebSQLOpenOptions, WebSQLFlags } from '../adapters/web-sql-flags';
|
|
9
|
+
|
|
10
|
+
export interface WebStreamingSyncImplementationOptions extends AbstractStreamingSyncImplementationOptions {
|
|
11
|
+
flags?: WebSQLFlags;
|
|
12
|
+
sync?: {
|
|
13
|
+
worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => SharedWorker);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class WebStreamingSyncImplementation extends AbstractStreamingSyncImplementation {
|
|
18
|
+
constructor(options: WebStreamingSyncImplementationOptions) {
|
|
19
|
+
// Super will store and provide default values for options
|
|
20
|
+
super(options);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get webOptions(): WebStreamingSyncImplementationOptions {
|
|
24
|
+
return this.options as WebStreamingSyncImplementationOptions;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async obtainLock<T>(lockOptions: LockOptions<T>): Promise<T> {
|
|
28
|
+
const identifier = `streaming-sync-${lockOptions.type}-${this.webOptions.identifier}`;
|
|
29
|
+
if (lockOptions.type == LockType.SYNC) {
|
|
30
|
+
this.logger.debug('requesting lock for ', identifier);
|
|
31
|
+
}
|
|
32
|
+
return getNavigatorLocks().request(identifier, { signal: lockOptions.signal }, lockOptions.callback);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface NavigatorInfo {
|
|
2
|
+
userAgent: string;
|
|
3
|
+
|
|
4
|
+
userAgentData?: {
|
|
5
|
+
brands?: { brand: string; version: string }[];
|
|
6
|
+
platform?: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get a minimal representation of browser, version and operating system.
|
|
12
|
+
*
|
|
13
|
+
* The goal is to get enough environemnt info to reproduce issues, but no
|
|
14
|
+
* more.
|
|
15
|
+
*/
|
|
16
|
+
export function getUserAgentInfo(nav?: NavigatorInfo): string[] {
|
|
17
|
+
nav ??= navigator;
|
|
18
|
+
|
|
19
|
+
const browser = getBrowserInfo(nav);
|
|
20
|
+
const os = getOsInfo(nav);
|
|
21
|
+
// The cast below is to cater for TypeScript < 5.5.0
|
|
22
|
+
return [browser, os].filter((v) => v != null) as string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getBrowserInfo(nav: NavigatorInfo): string | null {
|
|
26
|
+
const brands = nav.userAgentData?.brands;
|
|
27
|
+
if (brands != null) {
|
|
28
|
+
const tests = [
|
|
29
|
+
{ name: 'Google Chrome', value: 'Chrome' },
|
|
30
|
+
{ name: 'Opera', value: 'Opera' },
|
|
31
|
+
{ name: 'Edge', value: 'Edge' },
|
|
32
|
+
{ name: 'Chromium', value: 'Chromium' }
|
|
33
|
+
];
|
|
34
|
+
for (let { name, value } of tests) {
|
|
35
|
+
const brand = brands.find((b) => b.brand == name);
|
|
36
|
+
if (brand != null) {
|
|
37
|
+
return `${value}/${brand.version}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ua = nav.userAgent;
|
|
43
|
+
const regexps = [
|
|
44
|
+
{ re: /(?:firefox|fxios)\/(\d+)/i, value: 'Firefox' },
|
|
45
|
+
{ re: /(?:edg|edge|edga|edgios)\/(\d+)/i, value: 'Edge' },
|
|
46
|
+
{ re: /opr\/(\d+)/i, value: 'Opera' },
|
|
47
|
+
{ re: /(?:chrome|chromium|crios)\/(\d+)/i, value: 'Chrome' },
|
|
48
|
+
{ re: /version\/(\d+).*safari/i, value: 'Safari' }
|
|
49
|
+
];
|
|
50
|
+
for (let { re, value } of regexps) {
|
|
51
|
+
const match = re.exec(ua);
|
|
52
|
+
if (match != null) {
|
|
53
|
+
return `${value}/${match[1]}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getOsInfo(nav: NavigatorInfo): string | null {
|
|
60
|
+
if (nav.userAgentData?.platform != null) {
|
|
61
|
+
return nav.userAgentData.platform.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ua = nav.userAgent;
|
|
65
|
+
const regexps = [
|
|
66
|
+
{ re: /windows/i, value: 'windows' },
|
|
67
|
+
{ re: /android/i, value: 'android' },
|
|
68
|
+
{ re: /linux/i, value: 'linux' },
|
|
69
|
+
{ re: /iphone|ipad|ipod/i, value: 'ios' },
|
|
70
|
+
{ re: /macintosh|mac os x/i, value: 'macos' }
|
|
71
|
+
];
|
|
72
|
+
for (let { re, value } of regexps) {
|
|
73
|
+
if (re.test(ua)) {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from '@powersync/common';
|
|
2
|
+
export * from './db/adapters/AsyncDatabaseConnection';
|
|
3
|
+
export * from './db/adapters/AbstractWebPowerSyncDatabaseOpenFactory';
|
|
4
|
+
export * from './db/adapters/AbstractWebSQLOpenFactory';
|
|
5
|
+
export * from './db/adapters/wa-sqlite/WASQLiteConnection';
|
|
6
|
+
export * from './db/adapters/wa-sqlite/WASQLiteDBAdapter';
|
|
7
|
+
export * from './db/adapters/wa-sqlite/WASQLiteOpenFactory';
|
|
8
|
+
export * from './db/adapters/wa-sqlite/WASQLitePowerSyncDatabaseOpenFactory';
|
|
9
|
+
export * from './db/adapters/web-sql-flags';
|
|
10
|
+
export * from './db/PowerSyncDatabase';
|
|
11
|
+
export * from './db/sync/SharedWebStreamingSyncImplementation';
|
|
12
|
+
export * from './db/sync/WebRemote';
|
|
13
|
+
export * from './db/sync/WebStreamingSyncImplementation';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const getNavigatorLocks = (): LockManager => {
|
|
2
|
+
if ('locks' in navigator && navigator.locks) {
|
|
3
|
+
return navigator.locks;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
throw new Error(
|
|
7
|
+
'Navigator locks are not available in an insecure context. Use a secure context such as HTTPS or http://localhost.'
|
|
8
|
+
);
|
|
9
|
+
};
|