Skip to content

Commit 0695e92

Browse files
committed
♻️ Reinforce electron async logic and typing
1 parent 6ce736a commit 0695e92

File tree

6 files changed

+113
-46
lines changed

6 files changed

+113
-46
lines changed

electron/backend-manager.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,20 @@ import type { ChildProcess } from 'child_process';
22
import { spawn } from 'child_process';
33
import { app, dialog } from 'electron';
44
import fs from 'fs';
5+
import fsPromises from 'fs/promises';
56
import path from 'path';
67

8+
interface AppSettings {
9+
server?: {
10+
host?: string;
11+
port?: number;
12+
};
13+
mitm?: {
14+
host?: string;
15+
port?: number;
16+
};
17+
}
18+
719
import { BACKEND_SHUTDOWN_API_TIMEOUT_MS, BACKEND_SHUTDOWN_TIMEOUT_MS } from './constants';
820
import type { ResourceStatus } from './resource-validator';
921
import { ResourceValidator } from './resource-validator';
@@ -16,43 +28,51 @@ export class BackendManager {
1628
private readyPromise: Promise<void>;
1729
private resolveReady!: () => void;
1830

19-
public getBackendConfig(): { host: string; port: number } {
31+
public async getBackendConfig(): Promise<{ host: string; port: number }> {
2032
const defaultHost = '127.0.0.1';
2133
const defaultPort = 8765;
2234

2335
try {
2436
const settingsPath = getAssetPath('config', 'settings.json');
37+
const fileContent = await fsPromises.readFile(settingsPath, 'utf8');
38+
const settings = JSON.parse(fileContent) as AppSettings;
2539

26-
if (fs.existsSync(settingsPath)) {
27-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
28-
return {
29-
host: settings?.server?.host || defaultHost,
30-
port: settings?.server?.port || defaultPort,
31-
};
32-
}
40+
return {
41+
host: settings?.server?.host ?? defaultHost,
42+
port: settings?.server?.port ?? defaultPort,
43+
};
3344
} catch (err) {
34-
console.warn('[BackendManager] Failed to read settings.json for backend config:', err);
45+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
46+
console.warn(
47+
'[BackendManager] Failed to read settings.json for backend config:',
48+
err instanceof Error ? err.message : String(err),
49+
);
50+
}
3551
}
3652

3753
return { host: defaultHost, port: defaultPort };
3854
}
3955

40-
public getMitmConfig(): { host: string; port: number } {
56+
public async getMitmConfig(): Promise<{ host: string; port: number }> {
4157
const defaultHost = '127.0.0.1';
4258
const defaultPort = 6789;
4359

4460
try {
4561
const settingsPath = getAssetPath('config', 'settings.json');
62+
const fileContent = await fsPromises.readFile(settingsPath, 'utf8');
63+
const settings = JSON.parse(fileContent) as AppSettings;
4664

47-
if (fs.existsSync(settingsPath)) {
48-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
49-
return {
50-
host: settings?.mitm?.host || defaultHost,
51-
port: settings?.mitm?.port || defaultPort,
52-
};
53-
}
65+
return {
66+
host: settings?.mitm?.host ?? defaultHost,
67+
port: settings?.mitm?.port ?? defaultPort,
68+
};
5469
} catch (err) {
55-
console.warn('[BackendManager] Failed to read settings.json for mitm config:', err);
70+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
71+
console.warn(
72+
'[BackendManager] Failed to read settings.json for mitm config:',
73+
err instanceof Error ? err.message : String(err),
74+
);
75+
}
5676
}
5777

5878
return { host: defaultHost, port: defaultPort };
@@ -69,8 +89,8 @@ export class BackendManager {
6989
this.validator = new ResourceValidator(getProjectRoot());
7090
}
7191

72-
public getResourceStatus(): ResourceStatus {
73-
return this.validator.validate();
92+
public async getResourceStatus(): Promise<ResourceStatus> {
93+
return await this.validator.validate();
7494
}
7595

7696
public start() {
@@ -231,7 +251,7 @@ export class BackendManager {
231251
if (!this.isRunning()) return;
232252

233253
try {
234-
const { host, port } = this.getBackendConfig();
254+
const { host, port } = await this.getBackendConfig();
235255
await fetch(`http://${host}:${port}/api/shutdown`, {
236256
method: 'POST',
237257
signal: AbortSignal.timeout(BACKEND_SHUTDOWN_API_TIMEOUT_MS),

electron/game-handler.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,42 @@
11
import type { Protocol } from 'devtools-protocol';
22
import type { WebContents } from 'electron';
33

4+
export type BackendIngestPayload =
5+
| {
6+
source: 'electron';
7+
type: 'debugger_detached';
8+
reason: string;
9+
time: number;
10+
}
11+
| {
12+
source: 'electron';
13+
type: 'websocket_created';
14+
requestId: string;
15+
url: string;
16+
time: number;
17+
}
18+
| {
19+
source: 'electron';
20+
type: 'websocket_closed';
21+
requestId: string;
22+
time: number;
23+
}
24+
| {
25+
source: 'electron';
26+
type: 'websocket';
27+
requestId: string;
28+
direction: 'inbound' | 'outbound';
29+
data: string;
30+
opcode: number;
31+
time: number;
32+
}
33+
| {
34+
source: 'electron';
35+
type: 'liqi_definition';
36+
data: string;
37+
url: string;
38+
};
39+
440
export class GameHandler {
541
private attached = false;
642
private readonly BACKEND_API: string;
@@ -146,7 +182,7 @@ export class GameHandler {
146182
}
147183
}
148184

149-
const payload = {
185+
const payload: BackendIngestPayload = {
150186
source: 'electron',
151187
type: 'websocket',
152188
requestId: requestId,
@@ -183,13 +219,16 @@ export class GameHandler {
183219
}
184220
}
185221

186-
private sendToBackend(data: unknown) {
222+
private sendToBackend(data: BackendIngestPayload) {
187223
fetch(this.BACKEND_API, {
188224
method: 'POST',
189225
headers: { 'Content-Type': 'application/json' },
190226
body: JSON.stringify(data),
191227
}).catch((err) => {
192-
console.error('[GameHandler] Failed to send to backend:', err);
228+
console.error(
229+
'[GameHandler] Failed to send to backend:',
230+
err instanceof Error ? err.message : String(err),
231+
);
193232
});
194233
}
195234
}

electron/ipc-handlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export function registerIpcHandlers(windowManager: WindowManager, backendManager
106106
});
107107

108108
// Get backend host and port from settings
109-
ipcMain.handle('get-backend-config', () => {
110-
return backendManager.getBackendConfig();
109+
ipcMain.handle('get-backend-config', async () => {
110+
return await backendManager.getBackendConfig();
111111
});
112112
}

electron/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ app.whenReady().then(async () => {
3333

3434
// 4. Try to detect backend readiness (informative only, don't block the UI further)
3535
try {
36-
const { host, port } = backendManager.getBackendConfig();
36+
const { host, port } = await backendManager.getBackendConfig();
3737
for (let i = 0; i < BACKEND_STARTUP_CHECK_RETRIES; i++) {
3838
try {
3939
const controller = new AbortController();

electron/resource-validator.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import fs from 'fs';
1+
import fs from 'fs/promises';
22
import path from 'path';
33

44
export interface ResourceStatus {
@@ -11,12 +11,14 @@ export interface ResourceStatus {
1111
export class ResourceValidator {
1212
constructor(private projectRoot: string) {}
1313

14-
public validate(): ResourceStatus {
14+
public async validate(): Promise<ResourceStatus> {
1515
const libPath = path.join(this.projectRoot, 'lib');
1616
const modelsPath = path.join(this.projectRoot, 'models');
1717

18-
const libExists = this.checkLib(libPath);
19-
const modelsExists = this.checkModels(modelsPath);
18+
const [libExists, modelsExists] = await Promise.all([
19+
this.checkLib(libPath),
20+
this.checkModels(modelsPath),
21+
]);
2022

2123
const missingCritical: string[] = [];
2224
const missingOptional: string[] = [];
@@ -37,21 +39,27 @@ export class ResourceValidator {
3739
};
3840
}
3941

40-
private checkLib(dirPath: string): boolean {
41-
if (!fs.existsSync(dirPath)) return false;
42-
const files = fs.readdirSync(dirPath);
43-
// On Windows look for .pyd, otherwise .so
44-
const isWin = process.platform === 'win32';
45-
const libRiichi = isWin ? 'libriichi.pyd' : 'libriichi.so';
46-
const libRiichi3p = isWin ? 'libriichi3p.pyd' : 'libriichi3p.so';
42+
private async checkLib(dirPath: string): Promise<boolean> {
43+
try {
44+
const files = await fs.readdir(dirPath);
45+
// On Windows look for .pyd, otherwise .so
46+
const isWin = process.platform === 'win32';
47+
const libRiichi = isWin ? 'libriichi.pyd' : 'libriichi.so';
48+
const libRiichi3p = isWin ? 'libriichi3p.pyd' : 'libriichi3p.so';
4749

48-
return files.includes(libRiichi) && files.includes(libRiichi3p);
50+
return files.includes(libRiichi) && files.includes(libRiichi3p);
51+
} catch {
52+
return false;
53+
}
4954
}
5055

51-
private checkModels(dirPath: string): boolean {
52-
if (!fs.existsSync(dirPath)) return false;
53-
const files = fs.readdirSync(dirPath);
54-
// Look for at least one .pth file
55-
return files.some((f) => f.endsWith('.pth'));
56+
private async checkModels(dirPath: string): Promise<boolean> {
57+
try {
58+
const files = await fs.readdir(dirPath);
59+
// Look for at least one .pth file
60+
return files.some((f) => f.endsWith('.pth'));
61+
} catch {
62+
return false;
63+
}
5664
}
5765
}

electron/window-manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export class WindowManager {
258258

259259
// Set up proxy if using MITM
260260
if (useMitm) {
261-
const mitm = this.backendManager.getMitmConfig();
261+
const mitm = await this.backendManager.getMitmConfig();
262262
const proxyRules = `http://${mitm.host}:${mitm.port}`;
263263
console.log(`[WindowManager] Setting game window proxy to: ${proxyRules}`);
264264
await this.gameWindow.webContents.session.setProxy({
@@ -275,7 +275,7 @@ export class WindowManager {
275275
this.gameWindow.webContents &&
276276
!this.gameWindow.webContents.isDestroyed()
277277
) {
278-
const backend = this.backendManager.getBackendConfig();
278+
const backend = await this.backendManager.getBackendConfig();
279279
const apiBase = `http://${backend.host}:${backend.port}`;
280280
this.gameHandler = new GameHandler(this.gameWindow.webContents, apiBase);
281281
this.gameHandler.attach(); // Do not await, let it happen in parallel with loadURL

0 commit comments

Comments
 (0)