Handle browsers without Web Locks

This commit is contained in:
Eric Bailey
2026-08-31 13:15:38 -05:00
parent 03a3ec0476
commit 2ea281bb54
3 changed files with 53 additions and 1 deletions
+2
View File
@@ -287,6 +287,8 @@ All credential-changing commits use the same per-account lock:
Because all persisted values share one localStorage blob, every write also takes a root persisted-storage lock. The root lock prevents an unrelated preference write from racing the session read-modify-write; the per-account lock expresses credential ownership and gives account operations a consistent order.
Feature-detect the Web Locks API. If `navigator.locks.request` is unavailable, run the operation without a lock rather than failing startup or session operations. Generation-specific conditional commits still reject stale work in this fallback mode, but localStorage read-modify-write is not fully serialized across tabs.
If Tab A holds the locks, Tab B waits. Once Tab A writes and releases them, Tab B acquires them and rereads Tab A's new localStorage state before deciding what to commit.
The complete refresh flow is:
@@ -0,0 +1,42 @@
import {afterEach, describe, expect, it, jest} from '@jest/globals'
import {runWithSessionCredentialLock} from '../session-lock.web'
const originalNavigatorDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
'navigator',
)
function setNavigator(value: unknown) {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value,
})
}
afterEach(() => {
if (originalNavigatorDescriptor) {
Object.defineProperty(globalThis, 'navigator', originalNavigatorDescriptor)
} else {
Reflect.deleteProperty(globalThis, 'navigator')
}
})
describe('session credential locks on unsupported browsers', () => {
it.each([
['navigator is unavailable', undefined],
['navigator.locks is unavailable', {}],
['navigator.locks.request is unavailable', {locks: {}}],
])('runs without a lock when %s', async (_, navigatorValue) => {
setNavigator(navigatorValue)
const operation = jest.fn(() => 'result')
await expect(
runWithSessionCredentialLock({
accountDids: ['did:plc:example'],
operation,
}),
).resolves.toBe('result')
expect(operation).toHaveBeenCalledTimes(1)
})
})
+9 -1
View File
@@ -8,7 +8,7 @@ export function runWithSessionCredentialLock<T>({
accountDids: string[]
operation: () => T | Promise<T>
}): Promise<T> {
const lockManager = navigator.locks
const lockManager = getLockManager()
if (!lockManager) {
try {
return Promise.resolve(operation())
@@ -35,3 +35,11 @@ export function runWithSessionCredentialLock<T>({
return run(0)
}
function getLockManager(): LockManager | undefined {
if (typeof navigator === 'undefined' || !('locks' in navigator)) {
return undefined
}
const lockManager = navigator.locks
return typeof lockManager?.request === 'function' ? lockManager : undefined
}