Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tech] Type-check Backend & Frontend events #3649

Merged
merged 2 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"ts-jest": "29.0.5",
"ts-prune": "0.10.3",
"type-fest": "3.6.1",
"typed-emitter": "^2.1.0",
"typescript": "4.9.4",
"unimported": "1.26.0",
"vite": "3.2.8",
Expand Down
9 changes: 7 additions & 2 deletions src/backend/api/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Runner,
InstallPlatform,
WineCommandArgs,
ConnectivityChangedCallback,
ConnectivityStatus,
AppSettings,
GameSettings,
Expand Down Expand Up @@ -115,7 +114,13 @@ export const runWineCommandForGame = async (args: RunWineCommandArgs) =>
ipcRenderer.invoke('runWineCommandForGame', args)

export const onConnectivityChanged = async (
callback: ConnectivityChangedCallback
callback: (
event: Electron.IpcRendererEvent,
status: {
status: ConnectivityStatus
retryIn: number
}
) => void
) => ipcRenderer.on('connectivity-changed', callback)

export const getConnectivityStatus = async () =>
Expand Down
9 changes: 6 additions & 3 deletions src/backend/api/library.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ipcRenderer } from 'electron'
import {
Runner,
InstallParams,
LaunchParams,
ImportGameArgs,
GameStatus,
Expand Down Expand Up @@ -74,11 +73,15 @@ export const handleLaunchGame = (
) => ipcRenderer.on('launchGame', callback)

export const handleInstallGame = (
callback: (event: Electron.IpcRendererEvent, args: InstallParams) => void
callback: (
event: Electron.IpcRendererEvent,
appName: string,
runner: Runner
) => void
) => ipcRenderer.on('installGame', callback)

export const handleRefreshLibrary = (
callback: (event: Electron.IpcRendererEvent, runner: Runner) => void
callback: (event: Electron.IpcRendererEvent, runner?: Runner) => void
) => ipcRenderer.on('refreshLibrary', callback)

export const handleGamePush = (
Expand Down
5 changes: 3 additions & 2 deletions src/backend/api/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ export const pathExists = async (path: string) =>
export const processShortcut = async (combination: string) =>
ipcRenderer.send('processShortcut', combination)

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const handleGoToScreen = (callback: any) => {
export const handleGoToScreen = (
callback: (e: Electron.IpcRendererEvent, screen: string) => void
) => {
ipcRenderer.on('openScreen', callback)
return () => {
ipcRenderer.removeListener('openScreen', callback)
Expand Down
2 changes: 1 addition & 1 deletion src/backend/api/wine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const refreshWineVersionInfo = async (fetch?: boolean): Promise<void> =>
export const handleProgressOfWinetricks = (
onProgress: (
e: Electron.IpcRendererEvent,
payload: { messages: string[]; installingComponent: '' }
payload: { messages: string[]; installingComponent: string }
) => void
): (() => void) => {
ipcRenderer.on('progressOfWinetricks', onProgress)
Expand Down
17 changes: 16 additions & 1 deletion src/backend/backend_events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import EventEmitter from 'events'

import type TypedEventEmitter from 'typed-emitter'
import type { GameStatus, RecentGame } from 'common/types'

type BackendEvents = {
gameStatusUpdate: (payload: GameStatus) => void
recentGamesChanged: (recentGames: RecentGame[]) => void
settingChanged: (obj: {
key: string
oldValue: unknown
newValue: unknown
}) => void
[key: `progressUpdate-${string}`]: (progress: GameStatus) => void
}

// This can be used to emit/listen to events to decouple components
// For example:
// When the list of recent games changes, a `recentGamesChanged` event is emitted.
Expand All @@ -9,4 +23,5 @@ import EventEmitter from 'events'
// Usage:
// Emit events with `backendEvents.emit("eventName", arg1, arg2)
// Listen to events with `backendEvents.on("eventName", (arg1, arg2) => { ... })
export const backendEvents = new EventEmitter()
export const backendEvents =
new EventEmitter() as TypedEventEmitter<BackendEvents>
2 changes: 1 addition & 1 deletion src/backend/logger/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export function initLogger() {
)

if (key === 'disableLogs') {
logsDisabled = newValue
logsDisabled = newValue as boolean
}
})
}
Expand Down
6 changes: 5 additions & 1 deletion src/backend/main_window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AppSettings, WindowProps } from 'common/types'
import { BrowserWindow, screen } from 'electron'
import path from 'path'
import { configStore } from './constants'
import type { FrontendMessages } from 'common/types/frontend_messages'

let mainWindow: BrowserWindow | null = null

Expand All @@ -18,7 +19,10 @@ export const isFrameless = () => {
// send a message to the main window's webContents if available
// returns `false` if no mainWindow or no webContents
// returns `true` if the message was sent to the webContents
export const sendFrontendMessage = (message: string, ...payload: unknown[]) => {
export const sendFrontendMessage = <MessageName extends keyof FrontendMessages>(
message: MessageName,
...payload: Parameters<FrontendMessages[MessageName]>
) => {
// get the first BrowserWindow if for some reason we don't have a webContents
if (!mainWindow?.webContents) {
mainWindow = BrowserWindow.getAllWindows()[0]
Expand Down
6 changes: 3 additions & 3 deletions src/backend/progress_bar.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { InstallProgress } from 'common/types'
import { GameStatus } from 'common/types'
import { backendEvents } from './backend_events'
import { getMainWindow } from './main_window'

const handleProgressUpdate = ({ progress }: { progress: InstallProgress }) => {
if (progress.percent) {
const handleProgressUpdate = ({ progress }: GameStatus) => {
if (progress?.percent) {
getMainWindow()?.setProgressBar(progress.percent / 100)
}
}
Expand Down
7 changes: 2 additions & 5 deletions src/backend/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,15 @@ async function handleLaunch(
icon: icon
})
if (response === 0) {
return sendFrontendMessage('installGame', {
appName: app_name,
runner: gameRunner
})
return sendFrontendMessage('installGame', app_name, gameRunner)
}
if (response === 1) {
return logInfo('Not installing game', LogPrefix.ProtocolHandler)
}
}

mainWindow?.hide()
sendFrontendMessage('launchGame', arg, gameRunner)
sendFrontendMessage('launchGame', app_name, gameRunner)
}

/**
Expand Down
7 changes: 3 additions & 4 deletions src/backend/tools/ipc_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,9 @@ ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => {
if (runner === 'gog') {
// Check if game was modified by offline installer / wine uninstaller
await GOGLibraryManager.checkForOfflineInstallerChanges(appName)
sendFrontendMessage(
'pushGameToLibrary',
GOGLibraryManager.getGameInfo(appName)
)
const maybeNewGameInfo = GOGLibraryManager.getGameInfo(appName)
if (maybeNewGameInfo)
sendFrontendMessage('pushGameToLibrary', maybeNewGameInfo)
}

sendGameStatusUpdate({ appName, runner, status: 'done' })
Expand Down
2 changes: 1 addition & 1 deletion src/backend/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ export async function downloadDefaultWine() {

// download the latest version
const onProgress = (state: State, progress?: ProgressInfo) => {
sendFrontendMessage('progressOfWineManager' + release.version, {
sendFrontendMessage(`progressOfWineManager${release.version}`, {
state,
progress
})
Expand Down
2 changes: 1 addition & 1 deletion src/backend/wine/manager/ipc_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { sendFrontendMessage } from '../../main_window'

ipcMain.handle('installWineVersion', async (e, release) => {
const onProgress = (state: State, progress?: ProgressInfo) => {
sendFrontendMessage('progressOfWineManager' + release.version, {
sendFrontendMessage(`progressOfWineManager${release.version}`, {
state,
progress
})
Expand Down
7 changes: 1 addition & 6 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
GameMetadataInner,
LegendaryInstallInfo
} from './types/legendary'
import { IpcRendererEvent, TitleBarOverlay } from 'electron'
import { TitleBarOverlay } from 'electron'
import { ChildProcess } from 'child_process'
import type { HowLongToBeatEntry } from 'backend/wiki_game_info/howlongtobeat/utils'
import { NileInstallInfo, NileInstallPlatform } from './types/nile'
Expand Down Expand Up @@ -541,11 +541,6 @@ export type InstallPlatform =
| NileInstallPlatform
| 'Browser'

export type ConnectivityChangedCallback = (
event: IpcRendererEvent,
status: ConnectivityStatus
) => void

export type ConnectivityStatus = 'offline' | 'check-online' | 'online'

export interface Tools {
Expand Down
57 changes: 57 additions & 0 deletions src/common/types/frontend_messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type {
ButtonOptions,
ConnectivityStatus,
DialogType,
DMQueueElement,
DownloadManagerState,
GameInfo,
GameStatus,
ProgressInfo,
RecentGame,
Runner,
State
} from 'common/types'

type FrontendMessages = {
gameStatusUpdate: (status: GameStatus) => void
wineVersionsUpdated: () => void
showDialog: (
title: string,
message: string,
type: DialogType,
buttons?: Array<ButtonOptions>
) => void
changedDMQueueInformation: (
elements: DMQueueElement[],
state: DownloadManagerState
) => void
maximized: () => void
unmaximized: () => void
fullscreen: (status: boolean) => void
refreshLibrary: (runner?: Runner) => void
openScreen: (screen: string) => void
'connectivity-changed': (status: {
status: ConnectivityStatus
retryIn: number
}) => void
launchGame: (appName: string, runner: Runner) => void
installGame: (appName: string, runner: Runner) => void
recentGamesChanged: (newRecentGames: RecentGame[]) => void
pushGameToLibrary: (info: GameInfo) => void
progressOfWinetricks: (payload: {
messages: string[]
installingComponent: string
}) => void
'installing-winetricks-component': (component: string) => void

[key: `progressUpdate${string}`]: (progress: GameStatus) => void
[key: `progressOfWineManager${string}`]: (progress: {
state: State
progress?: ProgressInfo
}) => void

// Used inside tests, so we can be a bit lenient with the type checking here
message: (...params: unknown[]) => void
}

export type { FrontendMessages }
2 changes: 1 addition & 1 deletion src/frontend/components/UI/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default React.memo(function Sidebar() {
}, [sidebarEl])

useEffect(() => {
window.api.handleGoToScreen((e: Event, screen: string) => {
window.api.handleGoToScreen((e, screen) => {
// handle navigate to screen
navigate(screen, { state: { fromGameCard: false } })
})
Expand Down
62 changes: 27 additions & 35 deletions src/frontend/state/GlobalState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
RefreshOptions,
Runner,
WineVersionInfo,
InstallParams,
LibraryTopSectionOptions,
ExperimentalFeatures
} from 'common/types'
Expand Down Expand Up @@ -773,44 +772,37 @@ class GlobalState extends PureComponent<Props> {
}
)

window.api.handleInstallGame(
async (e: IpcRendererEvent, args: InstallParams) => {
const currentApp = libraryStatus.filter(
(game) => game.appName === appName
)[0]
const { appName, runner } = args
if (!currentApp || (currentApp && currentApp.status !== 'installing')) {
const gameInfo = await getGameInfo(appName, runner)
if (!gameInfo || gameInfo.runner === 'sideload') {
return
}
return this.setState({
showInstallModal: {
show: true,
appName,
runner,
gameInfo
}
})
window.api.handleInstallGame(async (e, appName, runner) => {
const currentApp = libraryStatus.filter(
(game) => game.appName === appName
)[0]
if (!currentApp || (currentApp && currentApp.status !== 'installing')) {
const gameInfo = await getGameInfo(appName, runner)
if (!gameInfo || gameInfo.runner === 'sideload') {
return
}
return this.setState({
showInstallModal: {
show: true,
appName,
runner,
gameInfo
}
})
}
)
})

window.api.handleGameStatus(
async (e: IpcRendererEvent, args: GameStatus) => {
return this.handleGameStatus({ ...args })
}
)
window.api.handleGameStatus((e, args) => {
this.handleGameStatus({ ...args })
})

window.api.handleRefreshLibrary(
async (e: IpcRendererEvent, runner: Runner) => {
this.refreshLibrary({
checkForUpdates: false,
runInBackground: true,
library: runner
})
}
)
window.api.handleRefreshLibrary((e, runner) => {
this.refreshLibrary({
checkForUpdates: false,
runInBackground: true,
library: runner
})
})

window.api.handleGamePush((e: IpcRendererEvent, args: GameInfo) => {
if (!args.app_name) return
Expand Down
14 changes: 14 additions & 0 deletions yarn.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading