utils: extract acquireLock function from fileStorage

This commit is contained in:
David Lechner
2022-05-19 12:03:10 -05:00
parent 3dc03f3879
commit 8b6939a983
3 changed files with 153 additions and 33 deletions
+92 -2
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
import { assert, defined, ensureError, hex, maybe, timestamp } from '.';
import { acquireLock, assert, defined, ensureError, hex, maybe, timestamp } from '.';
test('assert', () => {
const assertTrue = jest.fn(() => assert(true, 'should not throw'));
@@ -62,3 +62,93 @@ describe('timestamp', () => {
expect(timestamp()).not.toMatch(/[A-Za-z]/);
});
});
describe('acquireLock', () => {
it.each([true, false])('should acquire lock when shared is %o', async () => {
const releaseLock = await acquireLock('test');
try {
expect(releaseLock).toBeDefined();
} finally {
await releaseLock?.();
}
});
it.each([true, false])(
'should fail to acquire exclusive second lock when first lock shared is %o',
async (shared) => {
const releaseLock = await acquireLock('test', shared);
try {
const releaseLock2 = await acquireLock('test');
try {
expect(releaseLock2).toBeUndefined();
} finally {
await releaseLock2?.();
}
} finally {
await releaseLock?.();
}
},
);
it('should acquire shared second lock when first lock is shared', async () => {
const releaseLock = await acquireLock('test', true);
try {
const releaseLock2 = await acquireLock('test', true);
try {
expect(releaseLock2).toBeDefined();
} finally {
await releaseLock2?.();
}
} finally {
await releaseLock?.();
}
});
it('should fail to acquire shared second lock when first lock is exclusive', async () => {
const releaseLock = await acquireLock('test');
try {
const releaseLock2 = await acquireLock('test', true);
try {
expect(releaseLock2).toBeUndefined();
} finally {
await releaseLock2?.();
}
} finally {
await releaseLock?.();
}
});
it('should acquire second lock when first lock is released', async () => {
const releaseLock = await acquireLock('test');
try {
expect(releaseLock).toBeDefined();
} finally {
await releaseLock?.();
}
const releaseLock2 = await acquireLock('test');
try {
expect(releaseLock2).toBeDefined();
} finally {
await releaseLock2?.();
}
});
it('should fail to acquire second lock when first lock is released but release is not awaited', async () => {
const releaseLock = await acquireLock('test');
try {
expect(releaseLock).toBeDefined();
// not awaited!
releaseLock?.();
const releaseLock2 = await acquireLock('test');
try {
expect(releaseLock2).toBeUndefined();
} finally {
await releaseLock2?.();
}
} finally {
await releaseLock?.();
}
});
});
+56 -1
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Copyright (c) 2020-2022 The Pybricks Authors
/**
* Asserts that an assumption is true. This is used to detect programmer errors
@@ -75,3 +75,58 @@ export function timestamp(): string {
.replaceAll(':', '-')
.replace(/\..*$/, '');
}
/**
* Helper function to wrap navigator.locks in a promise so that it can be used
* in code where using it natively doesn't work well (e.g. in sagas). Care must
* be taken so that all code paths (including exceptions) release the lock.
*
* To release the lock, await the returned release function. When the release
* function resolves, the lock will no longer be held.
*
* @param name The name of the lock.
* @param shared If true, the lock will be share (e.g. for reading), otherwise
* the lock will be exclusive (e.g. for writing).
* @returns A release function if the lock was acquired or nothing if the lock
* was already held exclusively by someone else.
*/
export async function acquireLock(
name: string,
shared?: boolean,
): Promise<(() => Promise<void>) | void> {
let lockWaiter: Promise<void>;
const release = await new Promise<(() => void) | void>((resolve, reject) => {
lockWaiter = navigator.locks
.request(
name,
{
ifAvailable: true,
mode: shared ? 'shared' : 'exclusive',
},
(lock) => {
// if the locks is already held, lock will be null here
if (lock === null) {
resolve();
return;
}
// Now we own the lock and it will be held until the returned
// promise is resolved.
return new Promise<void>((resolve2) => resolve(resolve2));
},
)
.catch(reject);
});
if (!release) {
return;
}
return async () => {
// trigger the release
release();
// then wait until the release is complete and the lock is no longer held
await lockWaiter;
};
}