From d19082ffc7112275133599786bf9f317915ab6b9 Mon Sep 17 00:00:00 2001 From: Erik Moura Date: Sun, 9 Jul 2023 21:51:08 -0300 Subject: [PATCH 1/8] feat: prevent Electron versions that are in use by some window from being removed --- src/renderer/components/settings-electron.tsx | 36 +++++++++++- src/renderer/state.ts | 58 ++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/settings-electron.tsx b/src/renderer/components/settings-electron.tsx index 52eae7598e..eb0d2e8f2c 100644 --- a/src/renderer/components/settings-electron.tsx +++ b/src/renderer/components/settings-electron.tsx @@ -52,6 +52,39 @@ export const ElectronSettings = observer( this.handleStateChange = this.handleStateChange.bind(this); } + /** + * Queries the currently active versions and update the local state. + * + * This currently gives a warning/error in development mode when the ElectronSettings component is unmounted + * ("Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak + * in your application"). This is a false positive, the warning has been removed in React 18+ (see + * https://github.com/facebook/react/pull/22114). + * + * @TODO upgrade to React 18 + */ + updateActiveVersions = () => { + this.props.appState + .getActiveVersions() + .then((activeVersions) => + this.setState({ ...this.state, activeVersions }), + ) + .catch((err) => { + console.error( + 'Error updating the currently active Electron versions:', + ); + console.error(err); + }); + }; + + public componentDidMount() { + this.updateActiveVersions(); + } + + // Fired when other windows change their active Electron version + public componentDidUpdate() { + this.updateActiveVersions(); + } + public handleUpdateElectronVersions() { this.props.appState.updateElectronVersions(); } @@ -215,6 +248,7 @@ export const ElectronSettings = observer( ); } + private filterSection(): JSX.Element { const { appState } = this.props; return ( @@ -401,7 +435,7 @@ export const ElectronSettings = observer( break; } - if (version === appState.currentElectronVersion.version) { + if (this.state.activeVersions.has(appState.getVersionLockName(version))) { return ( > { + return ((await navigator.locks.query()).held || []).reduce>( + (acc, item) => { + if (item.name?.startsWith(AppState.versionLockNamePrefix)) { + acc.add(item.name); + } + + return acc; + }, + new Set(), + ); + } + constructor(versions: RunnableVersion[]) { makeObservable(this, { Bisector: observable, @@ -795,7 +823,9 @@ export class AppState { public async removeVersion(ver: RunnableVersion): Promise { const { version, state, source } = ver; - if (ver === this.currentElectronVersion) { + const activeVersions = await this.getActiveVersions(); + + if (activeVersions.has(this.getVersionLockName(ver.version))) { console.log(`State: Not removing active version ${version}`); return; } @@ -954,10 +984,36 @@ export class AppState { return; } + if (this.versionLock) { + console.log(`Releasing lock on version ${this.version}`); + + // release the lock on the previous version + this.versionLockController.abort(); + + // replace the spent AbortController + this.versionLockController = new AbortController(); + } + const { version } = ver; console.log(`State: Switching to Electron ${version}`); this.version = version; + navigator.locks.request( + this.getVersionLockName(version), + { mode: 'shared' }, + (lock) => { + this.versionLock = lock; + + /** + * The lock is released when this promise resolves, so we keep it in the + * pending state until our AbortController is aborted. + */ + return new Promise((resolve) => { + this.versionLockController.signal.onabort = () => resolve(); + }); + }, + ); + // If there's no current fiddle, // or if the current fiddle is the previous version's template, // then load the new version's template. From 9aa66bb0e4789c9d954e9ce5b07caf363bd4f874 Mon Sep 17 00:00:00 2001 From: Erik Moura Date: Mon, 10 Jul 2023 22:54:21 -0300 Subject: [PATCH 2/8] refactor: update `getVersionLock` implementation --- src/renderer/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/state.ts b/src/renderer/state.ts index 71fddd8630..84986ac331 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -234,7 +234,7 @@ export class AppState { private static versionLockNamePrefix = 'version:'; public getVersionLockName(ver: string) { - return [AppState.versionLockNamePrefix, ver].join(''); + return `${AppState.versionLockNamePrefix}${ver}`; } /** From d70f7f982215c65540168ce6bad276a36b5d08c4 Mon Sep 17 00:00:00 2001 From: Erik Moura Date: Mon, 10 Jul 2023 23:43:22 -0300 Subject: [PATCH 3/8] refactor: return only the version names from `getActiveVersions` --- src/renderer/components/settings-electron.tsx | 2 +- src/renderer/state.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/settings-electron.tsx b/src/renderer/components/settings-electron.tsx index eb0d2e8f2c..d2e98819fb 100644 --- a/src/renderer/components/settings-electron.tsx +++ b/src/renderer/components/settings-electron.tsx @@ -435,7 +435,7 @@ export const ElectronSettings = observer( break; } - if (this.state.activeVersions.has(appState.getVersionLockName(version))) { + if (this.state.activeVersions.has(version)) { return ( >( (acc, item) => { if (item.name?.startsWith(AppState.versionLockNamePrefix)) { - acc.add(item.name); + acc.add(item.name.split(AppState.versionLockNamePrefix)[1]); } return acc; @@ -825,7 +825,7 @@ export class AppState { const activeVersions = await this.getActiveVersions(); - if (activeVersions.has(this.getVersionLockName(ver.version))) { + if (activeVersions.has(ver.version)) { console.log(`State: Not removing active version ${version}`); return; } From 6e6c27e4c5733d30e279148e8a7a4a1ad26a689d Mon Sep 17 00:00:00 2001 From: Erik Moura Date: Mon, 10 Jul 2023 23:47:42 -0300 Subject: [PATCH 4/8] refactor: use a boolean instead of the `Lock` object --- src/renderer/state.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/state.ts b/src/renderer/state.ts index 5341823244..6416f5a6fb 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -225,8 +225,8 @@ export class AppState { }); } - // Lock on the active Electron version that prevents other windows from removing it - private versionLock: Lock | null = null; + // Do we have a lock on the active Electron version that prevents other windows from removing it? + private hasActiveLock = false; // Used to release the lock when the current window switches Electron versions private versionLockController = new AbortController(); @@ -984,7 +984,7 @@ export class AppState { return; } - if (this.versionLock) { + if (this.hasActiveLock) { console.log(`Releasing lock on version ${this.version}`); // release the lock on the previous version @@ -1002,7 +1002,7 @@ export class AppState { this.getVersionLockName(version), { mode: 'shared' }, (lock) => { - this.versionLock = lock; + this.hasActiveLock = Boolean(lock); /** * The lock is released when this promise resolves, so we keep it in the From b63bfe67a4e7877015335bb8d97dcbed6a4cc7e8 Mon Sep 17 00:00:00 2001 From: Erik Moura Date: Fri, 21 Jul 2023 23:22:15 -0300 Subject: [PATCH 5/8] refactor: use the broadcast channel to update the currently active versions --- src/interfaces.ts | 5 +++ src/renderer/components/settings-electron.tsx | 35 +-------------- src/renderer/state.ts | 43 +++++++++++++------ 3 files changed, 35 insertions(+), 48 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index 45de794e11..2c30ac792f 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -236,9 +236,14 @@ export type AppStateBroadcastMessage = | { type: AppStateBroadcastMessageType.syncVersions; payload: RunnableVersion[]; + } + | { + type: AppStateBroadcastMessageType.activeVersionsChanged; + payload?: never; }; export enum AppStateBroadcastMessageType { + activeVersionsChanged = 'activeVersionsChanged', isDownloadingAll = 'isDownloadingAll', syncVersions = 'syncVersions', } diff --git a/src/renderer/components/settings-electron.tsx b/src/renderer/components/settings-electron.tsx index d2e98819fb..9d47b61624 100644 --- a/src/renderer/components/settings-electron.tsx +++ b/src/renderer/components/settings-electron.tsx @@ -52,39 +52,6 @@ export const ElectronSettings = observer( this.handleStateChange = this.handleStateChange.bind(this); } - /** - * Queries the currently active versions and update the local state. - * - * This currently gives a warning/error in development mode when the ElectronSettings component is unmounted - * ("Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak - * in your application"). This is a false positive, the warning has been removed in React 18+ (see - * https://github.com/facebook/react/pull/22114). - * - * @TODO upgrade to React 18 - */ - updateActiveVersions = () => { - this.props.appState - .getActiveVersions() - .then((activeVersions) => - this.setState({ ...this.state, activeVersions }), - ) - .catch((err) => { - console.error( - 'Error updating the currently active Electron versions:', - ); - console.error(err); - }); - }; - - public componentDidMount() { - this.updateActiveVersions(); - } - - // Fired when other windows change their active Electron version - public componentDidUpdate() { - this.updateActiveVersions(); - } - public handleUpdateElectronVersions() { this.props.appState.updateElectronVersions(); } @@ -435,7 +402,7 @@ export const ElectronSettings = observer( break; } - if (this.state.activeVersions.has(version)) { + if (appState.activeVersions.has(version)) { return ( = new Set(); + private getVersionLockName(ver: string) { return `${AppState.versionLockNamePrefix}${ver}`; } /** - * Retrieves all Electron versions that are currently active in some window. + * Updates the Electron versions that are currently active in some window. */ - public async getActiveVersions(): Promise> { - return ((await navigator.locks.query()).held || []).reduce>( - (acc, item) => { - if (item.name?.startsWith(AppState.versionLockNamePrefix)) { - acc.add(item.name.split(AppState.versionLockNamePrefix)[1]); - } + private async updateActiveVersions(): Promise { + this.activeVersions = ((await navigator.locks.query()).held || []).reduce< + Set + >((acc, item) => { + if (item.name?.startsWith(AppState.versionLockNamePrefix)) { + acc.add(item.name.split(AppState.versionLockNamePrefix)[1]); + } - return acc; - }, - new Set(), - ); + return acc; + }, new Set()); } constructor(versions: RunnableVersion[]) { @@ -261,6 +262,7 @@ export class AppState { addAcceleratorToBlock: action, addLocalVersion: action, addNewVersions: action, + activeVersions: observable, channelsToShow: observable, clearConsole: action, currentElectronVersion: computed, @@ -504,6 +506,12 @@ export class AppState { const { type, payload } = event.data; switch (type) { + case AppStateBroadcastMessageType.activeVersionsChanged: { + this.updateActiveVersions(); + + break; + } + case AppStateBroadcastMessageType.isDownloadingAll: { this.isDownloadingAll = payload; break; @@ -823,9 +831,7 @@ export class AppState { public async removeVersion(ver: RunnableVersion): Promise { const { version, state, source } = ver; - const activeVersions = await this.getActiveVersions(); - - if (activeVersions.has(ver.version)) { + if (this.activeVersions.has(ver.version)) { console.log(`State: Not removing active version ${version}`); return; } @@ -1002,6 +1008,15 @@ export class AppState { this.getVersionLockName(version), { mode: 'shared' }, (lock) => { + // let other windows know we're using this version + this.broadcastChannel.postMessage({ + type: AppStateBroadcastMessageType.activeVersionsChanged, + }); + + // the current window's state also needs an update - that's how + // the current window knows it can't remove this version + this.updateActiveVersions(); + this.hasActiveLock = Boolean(lock); /** From 03851f514a9cb91f53364c7e75ec986e4d2e9665 Mon Sep 17 00:00:00 2001 From: Erik Moura Date: Sun, 23 Jul 2023 13:37:20 -0300 Subject: [PATCH 6/8] test: mock `navigator.locks` and fix existing tests --- tests/mocks/state.ts | 3 ++ .../components/settings-electron-spec.tsx | 10 +++- tests/renderer/state-spec.ts | 10 +++- tests/setup.ts | 46 +++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/tests/mocks/state.ts b/tests/mocks/state.ts index 5fb1f5ea2c..cccb47856c 100644 --- a/tests/mocks/state.ts +++ b/tests/mocks/state.ts @@ -17,6 +17,7 @@ import { objectDifference } from '../utils'; export class StateMock { public acceleratorsToBlock: BlockableAccelerator[] = []; public activeGistAction = GistActionState.none; + public activeVersions = new Set(); public channelsToShow: ElectronReleaseChannel[] = []; public editorMosaic = new EditorMosaic(); public environmentVariables: string[] = []; @@ -85,6 +86,7 @@ export class StateMock { public setVersion = jest.fn().mockImplementation((version: string) => { this.currentElectronVersion = this.versions[version]; this.version = version; + this.activeVersions.add(version); }); public isVersionUsable = jest.fn().mockImplementation(() => { return { ver: this.currentElectronVersion }; @@ -120,6 +122,7 @@ export class StateMock { makeObservable(this, { acceleratorsToBlock: observable, activeGistAction: observable, + activeVersions: observable, channelsToShow: observable, editorMosaic: observable, environmentVariables: observable, diff --git a/tests/renderer/components/settings-electron-spec.tsx b/tests/renderer/components/settings-electron-spec.tsx index 254c13e822..bc688b1a80 100644 --- a/tests/renderer/components/settings-electron-spec.tsx +++ b/tests/renderer/components/settings-electron-spec.tsx @@ -13,6 +13,7 @@ import { ElectronSettings } from '../../../src/renderer/components/settings-elec import { AppState } from '../../../src/renderer/state'; import { disableDownload } from '../../../src/renderer/utils/disable-download'; import { AppMock, StateMock, VersionsMock } from '../../mocks/mocks'; +import { waitFor } from '../../utils'; jest.mock('../../../src/renderer/utils/disable-download.ts'); @@ -20,12 +21,13 @@ describe('ElectronSettings component', () => { let store: StateMock; let mockVersions: Record; let mockVersionsArray: RunnableVersion[]; + const version = '2.0.1'; beforeEach(() => { ({ mockVersions, mockVersionsArray } = new VersionsMock()); ({ state: store } = window.ElectronFiddle.app as unknown as AppMock); - store.initVersions('2.0.1', { ...mockVersions }); + store.initVersions(version, { ...mockVersions }); store.channelsToShow = [ ElectronReleaseChannel.stable, ElectronReleaseChannel.beta, @@ -39,7 +41,7 @@ describe('ElectronSettings component', () => { store.versionsToShow[i++].state = InstallState.installing; }); - it('renders', () => { + it('renders', async () => { const spy = jest .spyOn(window.ElectronFiddle, 'getOldestSupportedMajor') .mockReturnValue(9); @@ -65,6 +67,10 @@ describe('ElectronSettings component', () => { const wrapper = shallow( , ); + + await store.setVersion(version); + await waitFor(() => store.activeVersions.size > 0); + expect(wrapper).toMatchSnapshot(); spy.mockRestore(); diff --git a/tests/renderer/state-spec.ts b/tests/renderer/state-spec.ts index ca8467e55b..6968c41c73 100644 --- a/tests/renderer/state-spec.ts +++ b/tests/renderer/state-spec.ts @@ -21,7 +21,11 @@ import { saveLocalVersions, } from '../../src/renderer/versions'; import { VersionsMock, createEditorValues } from '../mocks/mocks'; -import { overrideRendererPlatform, resetRendererPlatform } from '../utils'; +import { + overrideRendererPlatform, + resetRendererPlatform, + waitFor, +} from '../utils'; jest.mock('../../src/renderer/versions', () => { const { getReleaseChannel } = jest.requireActual( @@ -311,6 +315,10 @@ describe('AppState', () => { it('does not remove the active version', async () => { const ver = appState.versions[active]; + + await appState.setVersion(ver.version); + await waitFor(() => appState.activeVersions.size > 0); + broadcastMessageSpy.mockClear(); await appState.removeVersion(ver); expect(removeSpy).not.toHaveBeenCalled(); diff --git a/tests/setup.ts b/tests/setup.ts index 0377efb039..ade07ab6b5 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -76,6 +76,52 @@ delete (window as any).localStorage; window.navigator = window.navigator ?? {}; (window.navigator.clipboard as any) = {}; +class FakeNavigatorLocks implements LockManager { + locks = { + held: new Set(), + pending: new Set(), + }; + + query = async () => { + const result = { + held: [...this.locks.held], + pending: [...this.locks.pending], + }; + + return result as LockManagerSnapshot; + }; + + /** + * WIP. Right now, this is a **very** naive mock that will just happily grant a shared lock when one is requested, + * but I'll add some bookkeeping and expand it to cover the exclusive lock case as well. + * + * @TODO remove this comment + */ + request = (async (...args: Parameters) => { + const [ + name, + options = { + mode: 'exclusive', + }, + cb, + ] = args; + + const { mode } = options; + + const lock = { name, mode, cb } as Lock; + + if (mode === 'shared') { + this.locks.held.add(lock); + + await cb(lock); + + return; + } + }) as LockManager['request']; +} + +(window.navigator.locks as any) = new FakeNavigatorLocks(); + /** * Mock these properties twice so that they're available * both at the top-level of files and also within the From 666cadad1859b3c3f38acff34d05770736c03c13 Mon Sep 17 00:00:00 2001 From: Erik Moura Date: Mon, 24 Jul 2023 23:43:32 -0300 Subject: [PATCH 7/8] test: update the mock to handle exclusive locks --- tests/setup.ts | 141 ++++++++++++++++++++++++++++++++++++++++++++----- tests/utils.ts | 22 ++++++++ 2 files changed, 149 insertions(+), 14 deletions(-) diff --git a/tests/setup.ts b/tests/setup.ts index ade07ab6b5..7d9d6ccbd2 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -5,6 +5,7 @@ import { mocked } from 'jest-mock'; import { configure as mobxConfigure } from 'mobx'; import { ElectronFiddleMock } from './mocks/mocks'; +import { getOrCreateMapValue } from './utils'; enzymeConfigure({ adapter: new Adapter() }); @@ -76,47 +77,159 @@ delete (window as any).localStorage; window.navigator = window.navigator ?? {}; (window.navigator.clipboard as any) = {}; +type MockLock = Lock & { + abortController: AbortController; +}; + class FakeNavigatorLocks implements LockManager { locks = { - held: new Set(), - pending: new Set(), + held: new Map>>(), }; query = async () => { const result = { - held: [...this.locks.held], - pending: [...this.locks.pending], + held: [...this.locks.held.values()].reduce((acc, item) => { + acc.push(...[...item.get('exclusive')!.values()]); + acc.push(...[...item.get('shared')!.values()]); + + return acc; + }, [] as MockLock[]), }; return result as LockManagerSnapshot; }; - /** - * WIP. Right now, this is a **very** naive mock that will just happily grant a shared lock when one is requested, - * but I'll add some bookkeeping and expand it to cover the exclusive lock case as well. - * - * @TODO remove this comment - */ request = (async (...args: Parameters) => { const [ name, options = { + ifAvailable: false, mode: 'exclusive', + steal: false, }, cb, ] = args; - const { mode } = options; + const { ifAvailable, mode, steal } = options; + + const lock = { + name, + mode, + abortController: new AbortController(), + } as MockLock; + + const heldLocksWithSameName = getOrCreateMapValue( + this.locks.held, + name, + new Map>(), + ); + + const exclusiveLocksWithSameName = getOrCreateMapValue( + heldLocksWithSameName, + 'exclusive', + new Set(), + ); - const lock = { name, mode, cb } as Lock; + const sharedLocksWithSameName = getOrCreateMapValue( + heldLocksWithSameName, + 'shared', + new Set(), + ); if (mode === 'shared') { - this.locks.held.add(lock); + sharedLocksWithSameName.add(lock); - await cb(lock); + try { + await cb(lock); + } finally { + sharedLocksWithSameName.delete(lock); + } + + return; + } + + // exclusive lock + + // no locks with this name -> grant an exclusive lock + if ( + exclusiveLocksWithSameName.size === 0 && + sharedLocksWithSameName.size === 0 + ) { + exclusiveLocksWithSameName.add(lock); + + try { + await cb(lock); + } finally { + exclusiveLocksWithSameName.delete(lock); + } return; } + + // steal any currently held locks + if (steal) { + for (const lock of sharedLocksWithSameName) { + lock.abortController.abort(); + } + + for (const lock of exclusiveLocksWithSameName) { + lock.abortController.abort(); + } + + sharedLocksWithSameName.clear(); + exclusiveLocksWithSameName.clear(); + + exclusiveLocksWithSameName.add(lock); + + try { + await cb(lock); + } finally { + exclusiveLocksWithSameName.delete(lock); + } + + return; + } + + // run the callback without waiting for the lock to be released + if (ifAvailable) { + // just run the callback without waiting for it + cb(null); + + return; + } + + // @TODO add the lock to the list of pending locks? + + // it's an exclusive lock, so there's only one value + const currentLock = exclusiveLocksWithSameName.values().next() + .value as MockLock; + + const { abortController: currentLockAbortController } = currentLock; + + // wait for the current lock to be released + await new Promise((resolve, reject) => { + currentLockAbortController.signal.onabort = () => resolve(); + + const { abortController: pendingLockAbortController } = lock; + + // this allows the locking mechanism to release this lock + pendingLockAbortController.signal.onabort = () => reject(); + }); + + // clear the exclusive locks + exclusiveLocksWithSameName.clear(); + + // grant our lock + exclusiveLocksWithSameName.add(lock); + + try { + // run the callback + await cb(lock); + } finally { + exclusiveLocksWithSameName.delete(lock); + } + + return; }) as LockManager['request']; } diff --git a/tests/utils.ts b/tests/utils.ts index 4aa309cc23..20ec9029b0 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -147,3 +147,25 @@ export function emitEvent(type: FiddleEvent, ...args: any[]) { }, ); } + +export function getOrCreateMapValue>( + map: T, + key: MapKey, + fallbackValue: MapValue, +): MapValue { + if (!map.has(key)) { + map.set(key, fallbackValue); + + return fallbackValue; + } + + return map.get(key) as MapValue; +} + +type MapKey> = T extends Map + ? I + : never; + +type MapValue> = T extends Map + ? I + : never; From fd6d0f4834629c4fba80a1433ad761c3090dc2ba Mon Sep 17 00:00:00 2001 From: Erik Moura Date: Mon, 24 Jul 2023 23:45:20 -0300 Subject: [PATCH 8/8] feat: get a lock when downloading / removing a version --- src/renderer/state.ts | 74 +++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/src/renderer/state.ts b/src/renderer/state.ts index cf68a5d2e7..ac989d6d60 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -836,29 +836,48 @@ export class AppState { return; } - console.log(`State: Removing Electron ${version}`); - if (source === VersionSource.local) { - if (version in this.versions) { - delete this.versions[version]; - saveLocalVersions(Object.values(this.versions)); - } else { - console.log(`State: Version ${version} already removed, doing nothing`); - } - } else { - if ( - state === InstallState.installed || - state == InstallState.downloaded - ) { - await this.installer.remove(version); - if (this.installer.state(version) === InstallState.missing) { - await window.ElectronFiddle.app.electronTypes.uncache(ver); + await navigator.locks.request( + this.getVersionLockName(version), + { + mode: 'exclusive', + ifAvailable: true, + }, + async (lock) => { + // another window is already removing this version + if (!lock) { + return; + } - this.broadcastVersionStates([ver]); + console.log(`State: Removing Electron ${version}`); + + if (source === VersionSource.local) { + if (version in this.versions) { + delete this.versions[version]; + saveLocalVersions(Object.values(this.versions)); + } else { + console.log( + `State: Version ${version} already removed, doing nothing`, + ); + } + } else { + if ( + state === InstallState.installed || + state == InstallState.downloaded + ) { + await this.installer.remove(version); + if (this.installer.state(version) === InstallState.missing) { + await window.ElectronFiddle.app.electronTypes.uncache(ver); + + this.broadcastVersionStates([ver]); + } + } else { + console.log( + `State: Version ${version} already removed, doing nothing`, + ); + } } - } else { - console.log(`State: Version ${version} already removed, doing nothing`); - } - } + }, + ); } /** @@ -1044,8 +1063,17 @@ export class AppState { } } - // Fetch new binaries, maybe? - await this.downloadVersion(ver); + await navigator.locks.request( + `downloading:${version}`, + { mode: 'exclusive' }, + async (lock) => { + console.log(`exclusive download lock granted:`); + console.log(lock); + + // Fetch new binaries, maybe? + await this.downloadVersion(ver); + }, + ); } /**