-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor the Extensions settings page
- Simplify the install logic by refactoring out the multi-install logic - Revamp the ExtensionInstallStateStore to more strictly track the lifetime of an install or uninstall request - Fix the install of an already installed extension just hanging visually - Display a spinner more often for more visual feedback Signed-off-by: Sebastian Malton <sebastian@malton.name>
- Loading branch information
Showing
6 changed files
with
581 additions
and
403 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export type Disposer = () => void; | ||
|
||
interface Extendable<T> { | ||
push(...vals: T[]): void; | ||
} | ||
|
||
export function disposer(...args: Disposer[]): Disposer & Extendable<Disposer> { | ||
const res = () => { | ||
args.forEach(dispose => dispose?.()); | ||
args.length = 0; | ||
}; | ||
|
||
res.push = (...vals: Disposer[]) => { | ||
args.push(...vals); | ||
}; | ||
|
||
return res; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 183 additions & 9 deletions
192
src/renderer/components/+extensions/extension-install.store.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,187 @@ | ||
import { observable } from "mobx"; | ||
import { autobind, Singleton } from "../../utils"; | ||
import { action, computed, observable } from "mobx"; | ||
import logger from "../../../main/logger"; | ||
import { disposer, Disposer } from "../../utils"; | ||
import * as uuid from "uuid"; | ||
|
||
interface ExtensionState { | ||
displayName: string; | ||
// Possible states the extension can be | ||
state: "installing" | "uninstalling"; | ||
export enum ExtensionInstallationState { | ||
INSTALLING = "installing", | ||
UNINSTALLING = "uninstalling", | ||
IDLE = "IDLE", | ||
} | ||
|
||
@autobind() | ||
export class ExtensionStateStore extends Singleton { | ||
extensionState = observable.map<string, ExtensionState>(); | ||
const Prefix = "[ExtensionInstallationStore]"; | ||
const installingExtensions = observable.set<string>(); | ||
const uninstallingExtensions = observable.set<string>(); | ||
const preInstallIds = observable.set<string>(); | ||
|
||
export class ExtensionInstallationStateStore { | ||
@action static reset() { | ||
logger.warn(`${Prefix}: resetting, may throw errors`); | ||
installingExtensions.clear(); | ||
uninstallingExtensions.clear(); | ||
preInstallIds.clear(); | ||
} | ||
|
||
/** | ||
* Strictly transitions an extension from not installing to installing | ||
* @param extId the ID of the extension | ||
* @throws if state is not IDLE | ||
*/ | ||
@action static setInstalling(extId: string): void { | ||
logger.debug(`${Prefix}: trying to set ${extId} as installing`); | ||
|
||
const curState = ExtensionInstallationStateStore.getInstallationState(extId); | ||
|
||
if (curState !== ExtensionInstallationState.IDLE) { | ||
throw new Error(`${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`); | ||
} | ||
|
||
installingExtensions.add(extId); | ||
} | ||
|
||
/** | ||
* Marks the start of a pre-install phase of an extension installation. The | ||
* part of the installation before the tarball has been unpacked and the ID | ||
* determined. | ||
* @returns a disposer which should be called to mark the end of the install phase | ||
*/ | ||
@action static startPreInstall(): Disposer { | ||
const preInstallStepId = uuid.v4(); | ||
|
||
logger.debug(`${Prefix}: starting a new preinstall phase: ${preInstallStepId}`); | ||
preInstallIds.add(preInstallStepId); | ||
|
||
return disposer(() => { | ||
preInstallIds.delete(preInstallStepId); | ||
logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`); | ||
}); | ||
} | ||
|
||
/** | ||
* Strictly transitions an extension from not uninstalling to uninstalling | ||
* @param extId the ID of the extension | ||
* @throws if state is not IDLE | ||
*/ | ||
@action static setUninstalling(extId: string): void { | ||
logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`); | ||
|
||
const curState = ExtensionInstallationStateStore.getInstallationState(extId); | ||
|
||
if (curState !== ExtensionInstallationState.IDLE) { | ||
throw new Error(`${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`); | ||
} | ||
|
||
uninstallingExtensions.add(extId); | ||
} | ||
|
||
/** | ||
* Strictly clears the INSTALLING state of an extension | ||
* @param extId The ID of the extension | ||
* @throws if state is not INSTALLING | ||
*/ | ||
@action static clearInstalling(extId: string): void { | ||
logger.debug(`${Prefix}: trying to clear ${extId} as installing`); | ||
|
||
const curState = ExtensionInstallationStateStore.getInstallationState(extId); | ||
|
||
switch (curState) { | ||
case ExtensionInstallationState.INSTALLING: | ||
return void installingExtensions.delete(extId); | ||
default: | ||
throw new Error(`${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`); | ||
} | ||
} | ||
|
||
/** | ||
* Strictly clears the UNINSTALLING state of an extension | ||
* @param extId The ID of the extension | ||
* @throws if state is not UNINSTALLING | ||
*/ | ||
@action static clearUninstalling(extId: string): void { | ||
logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`); | ||
|
||
const curState = ExtensionInstallationStateStore.getInstallationState(extId); | ||
|
||
switch (curState) { | ||
case ExtensionInstallationState.UNINSTALLING: | ||
return void uninstallingExtensions.delete(extId); | ||
default: | ||
throw new Error(`${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`); | ||
} | ||
} | ||
|
||
/** | ||
* Returns the current state of the extension. IDLE is default value. | ||
* @param extId The ID of the extension | ||
*/ | ||
static getInstallationState(extId: string): ExtensionInstallationState { | ||
if (installingExtensions.has(extId)) { | ||
return ExtensionInstallationState.INSTALLING; | ||
} | ||
|
||
if (uninstallingExtensions.has(extId)) { | ||
return ExtensionInstallationState.UNINSTALLING; | ||
} | ||
|
||
return ExtensionInstallationState.IDLE; | ||
} | ||
|
||
/** | ||
* Returns true if the extension is currently INSTALLING | ||
* @param extId The ID of the extension | ||
*/ | ||
static isExtensionInstalling(extId: string): boolean { | ||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.INSTALLING; | ||
} | ||
|
||
/** | ||
* Returns true if the extension is currently UNINSTALLING | ||
* @param extId The ID of the extension | ||
*/ | ||
static isExtensionUninstalling(extId: string): boolean { | ||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.UNINSTALLING; | ||
} | ||
|
||
/** | ||
* Returns true if the extension is currently IDLE | ||
* @param extId The ID of the extension | ||
*/ | ||
static isExtensionIdle(extId: string): boolean { | ||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.IDLE; | ||
} | ||
|
||
/** | ||
* The current number of extensions installing | ||
*/ | ||
@computed static get installing(): number { | ||
return installingExtensions.size; | ||
} | ||
|
||
/** | ||
* If there is at least one extension currently installing | ||
*/ | ||
@computed static get anyInstalling(): boolean { | ||
return ExtensionInstallationStateStore.installing > 0; | ||
} | ||
|
||
/** | ||
* The current number of extensions preinstallig | ||
*/ | ||
@computed static get preinstalling(): number { | ||
return preInstallIds.size; | ||
} | ||
|
||
/** | ||
* If there is at least one extension currently downloading | ||
*/ | ||
@computed static get anyPreinstalling(): boolean { | ||
return ExtensionInstallationStateStore.preinstalling > 0; | ||
} | ||
|
||
/** | ||
* If there is at least one installing or preinstalling step taking place | ||
*/ | ||
@computed static get anyPreInstallingOrInstalling(): boolean { | ||
return ExtensionInstallationStateStore.anyInstalling || ExtensionInstallationStateStore.anyPreinstalling; | ||
} | ||
} |
Oops, something went wrong.