@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,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
+ };