Skip to content

Commit d326ef4

Browse files
committed
chore: add check for locks
1 parent 3c1b036 commit d326ef4

File tree

8 files changed

+91
-7
lines changed

8 files changed

+91
-7
lines changed

packages/web/src/db/PowerSyncDatabase.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
WebStreamingSyncImplementation,
2929
WebStreamingSyncImplementationOptions
3030
} from './sync/WebStreamingSyncImplementation';
31+
import { sdkNavigator } from '../shared/navigator';
3132

3233
export interface WebPowerSyncFlags extends WebSQLFlags {
3334
/**
@@ -160,7 +161,7 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
160161
if (this.resolvedFlags.ssrMode) {
161162
return PowerSyncDatabase.SHARED_MUTEX.runExclusive(cb);
162163
}
163-
return navigator.locks.request(`lock-${this.database.name}`, cb);
164+
return sdkNavigator.locks.request(`lock-${this.database.name}`, cb);
164165
}
165166

166167
protected generateSyncStreamImplementation(connector: PowerSyncBackendConnector): StreamingSyncImplementation {

packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { DBFunctionsInterface, OpenDB } from '../../../shared/types';
1515
import { _openDB } from '../../../shared/open-db';
1616
import { getWorkerDatabaseOpener, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database';
1717
import { ResolvedWebSQLOpenOptions, resolveWebSQLFlags, WebSQLFlags } from '../web-sql-flags';
18+
import { sdkNavigator } from '../../../shared/navigator';
1819

1920
/**
2021
* These flags are the same as {@link WebSQLFlags}.
@@ -186,7 +187,7 @@ export class WASQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
186187
}
187188

188189
protected acquireLock(callback: () => Promise<any>): Promise<any> {
189-
return navigator.locks.request(`db-lock-${this.options.dbFilename}`, callback);
190+
return sdkNavigator.locks.request(`db-lock-${this.options.dbFilename}`, callback);
190191
}
191192

192193
async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {

packages/web/src/db/adapters/web-sql-flags.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SQLOpenOptions } from '@powersync/common';
2+
import { sdkNavigator } from '../../shared/navigator';
23

34
/**
45
* Common settings used when creating SQL connections on web.
@@ -72,7 +73,7 @@ export const DEFAULT_WEB_SQL_FLAGS: ResolvedWebSQLFlags = {
7273
enableMultiTabs:
7374
typeof globalThis.navigator !== 'undefined' && // For SSR purposes
7475
typeof SharedWorker !== 'undefined' &&
75-
!navigator.userAgent.match(/(Android|iPhone|iPod|iPad)/i) &&
76+
!sdkNavigator.userAgent.match(/(Android|iPhone|iPod|iPad)/i) &&
7677
!(window as any).safari,
7778
useWebWorker: true
7879
};

packages/web/src/db/sync/WebStreamingSyncImplementation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
LockType
66
} from '@powersync/common';
77
import { ResolvedWebSQLOpenOptions, WebSQLFlags } from '../adapters/web-sql-flags';
8+
import { sdkNavigator } from '../../shared/navigator';
89

910
export interface WebStreamingSyncImplementationOptions extends AbstractStreamingSyncImplementationOptions {
1011
flags?: WebSQLFlags;
@@ -32,6 +33,6 @@ export class WebStreamingSyncImplementation extends AbstractStreamingSyncImpleme
3233
obtainLock<T>(lockOptions: LockOptions<T>): Promise<T> {
3334
const identifier = `streaming-sync-${lockOptions.type}-${this.webOptions.identifier}`;
3435
lockOptions.type == LockType.SYNC && console.debug('requesting lock for ', identifier);
35-
return navigator.locks.request(identifier, { signal: lockOptions.signal }, lockOptions.callback);
36+
return sdkNavigator.locks.request(identifier, { signal: lockOptions.signal }, lockOptions.callback);
3637
}
3738
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class SDKNavigator extends Navigator {
2+
private static instance: SDKNavigator | null = null;
3+
4+
constructor() {
5+
super();
6+
Object.setPrototypeOf(this, SDKNavigator.prototype);
7+
}
8+
9+
public static getInstance(): SDKNavigator {
10+
if (!SDKNavigator.instance) {
11+
SDKNavigator.instance = new SDKNavigator();
12+
}
13+
return SDKNavigator.instance;
14+
}
15+
16+
get locks(): LockManager {
17+
if (!super.locks) {
18+
throw new Error('Navigator locks are not available in this context. ' +
19+
'This may be due to running in an unsecure context. ' +
20+
'Consider using HTTPS or a secure context for full functionality.');
21+
}
22+
return new Proxy(super.locks, {
23+
get(target: LockManager, prop: keyof LockManager) {
24+
return target[prop];
25+
}
26+
});
27+
}
28+
}
29+
30+
export const sdkNavigator = SDKNavigator.getInstance();

packages/web/src/worker/db/WASQLiteDB.worker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import '@journeyapps/wa-sqlite';
66
import * as Comlink from 'comlink';
77
import { _openDB } from '../../shared/open-db';
88
import type { DBFunctionsInterface } from '../../shared/types';
9+
import { sdkNavigator } from '../../shared/navigator';
910

1011
/**
1112
* Keeps track of open DB connections and the clients which
@@ -23,7 +24,7 @@ let nextClientId = 1;
2324

2425
const openDBShared = async (dbFileName: string): Promise<DBFunctionsInterface> => {
2526
// Prevent multiple simultaneous opens from causing race conditions
26-
return navigator.locks.request(OPEN_DB_LOCK, async () => {
27+
return sdkNavigator.locks.request(OPEN_DB_LOCK, async () => {
2728
const clientId = nextClientId++;
2829

2930
if (!DBMap.has(dbFileName)) {

packages/web/src/worker/sync/SharedSyncImplementation.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { WASQLiteDBAdapter } from '../../db/adapters/wa-sqlite/WASQLiteDBAdapter';
2424
import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider';
2525
import { BroadcastLogger } from './BroadcastLogger';
26+
import { sdkNavigator } from '../../shared/navigator';
2627

2728
/**
2829
* Manual message events for shared sync clients
@@ -165,7 +166,7 @@ export class SharedSyncImplementation
165166
async connect(options?: PowerSyncConnectionOptions) {
166167
await this.waitForReady();
167168
// This effectively queues connect and disconnect calls. Ensuring multiple tabs' requests are synchronized
168-
return navigator.locks.request('shared-sync-connect', async () => {
169+
return sdkNavigator.locks.request('shared-sync-connect', async () => {
169170
this.syncStreamClient = this.generateStreamingImplementation();
170171

171172
this.syncStreamClient.registerListener({
@@ -181,7 +182,7 @@ export class SharedSyncImplementation
181182
async disconnect() {
182183
await this.waitForReady();
183184
// This effectively queues connect and disconnect calls. Ensuring multiple tabs' requests are synchronized
184-
return navigator.locks.request('shared-sync-connect', async () => {
185+
return sdkNavigator.locks.request('shared-sync-connect', async () => {
185186
await this.syncStreamClient?.disconnect();
186187
await this.syncStreamClient?.dispose();
187188
this.syncStreamClient = null;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, test, expect, beforeEach, vi, afterEach } from 'vitest';
2+
import { sdkNavigator } from '../../src/shared/navigator';
3+
4+
describe('sdkNavigator', () => {
5+
let originalNavigator: Navigator;
6+
7+
beforeEach(() => {
8+
originalNavigator = global.navigator;
9+
vi.stubGlobal('navigator', {
10+
...originalNavigator,
11+
locks: {
12+
request: vi.fn(),
13+
},
14+
});
15+
});
16+
17+
afterEach(() => {
18+
vi.stubGlobal('navigator', originalNavigator);
19+
});
20+
21+
test('should inherit properties from Navigator', () => {
22+
expect(sdkNavigator.userAgent).toBe(navigator.userAgent);
23+
});
24+
25+
test('should have locks property', () => {
26+
expect(sdkNavigator.locks).toBeDefined();
27+
expect(typeof sdkNavigator.locks.request).toBe('function');
28+
});
29+
30+
test('should throw error when locks are not available', () => {
31+
vi.stubGlobal('navigator', { ...originalNavigator, locks: undefined });
32+
expect(() => sdkNavigator.locks).toThrowError('Navigator locks are not available in this context.');
33+
});
34+
35+
test('locks proxy should pass through method calls when locks are available', () => {
36+
const mockCallback = vi.fn();
37+
sdkNavigator.locks.request('test', mockCallback);
38+
expect(navigator.locks.request).toHaveBeenCalledWith('test', mockCallback);
39+
});
40+
41+
test('should only expose expected Navigator properties', () => {
42+
const sdkNavigatorKeys = Object.keys(sdkNavigator);
43+
const navigatorKeys = Object.keys(navigator);
44+
sdkNavigatorKeys.forEach(key => {
45+
expect(navigatorKeys).toContain(key);
46+
});
47+
});
48+
});

0 commit comments

Comments
 (0)