Skip to content

Commit a3ae2ed

Browse files
committed
refactor: restructure state to target->app->session with nested apps
1 parent b944d41 commit a3ae2ed

14 files changed

Lines changed: 167 additions & 197 deletions

File tree

src/main.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const __dirname = path.dirname(__filename);
66

77
import { app, BrowserWindow, ipcMain, Menu, MenuItem, nativeImage, shell } from "electron";
88

9-
import type { AppInfo } from "./reducers/app";
9+
import type { AppInfo } from "./reducers/target";
1010

1111
import { addRemoteDevice, debug, debugPath, init, refreshDeviceApps, removeDevice } from "./main/actions";
1212
import { store } from "./main/store";
@@ -101,10 +101,10 @@ if (!gotTheLock) {
101101
);
102102
}
103103

104-
ipcMain.on("debug", (e, appInfo: AppInfo) => {
104+
ipcMain.on("debug", (e, payload: { targetId: string; app: AppInfo }) => {
105105
store.dispatch(
106106
// @ts-expect-error - Redux thunk action dispatch
107-
debug(appInfo),
107+
debug(payload),
108108
);
109109
});
110110
ipcMain.on("debug-path", (_, path: string) => {

src/main/actions.ts

Lines changed: 93 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import type { Action, ThunkDispatch } from "@reduxjs/toolkit";
22
import { dialog } from "electron";
33
import { chunk } from "lodash-es";
44

5-
import { type AppInfo, appSlice } from "../reducers/app";
5+
import { type AppInfo, targetSlice } from "../reducers/target";
66
import { type PageInfo, sessionSlice } from "../reducers/session";
7-
import { targetSlice } from "../reducers/target";
87

98
import type { State } from "./store";
109
import type { RemoteDeviceOptions } from "./targets/types";
@@ -22,24 +21,25 @@ export const init: ThunkActionCreator = () => async (dispatch, getState) => {
2221
// Initialize local targets
2322
await targetRegistry.initializeLocalTargets();
2423

25-
// Register all targets in Redux store
24+
// Register all targets in Redux store and discover their apps
2625
const targets = targetRegistry.getAllInfo();
27-
targets.forEach((target) => {
28-
dispatch(targetSlice.actions.registered(target));
29-
});
26+
for (const targetInfo of targets) {
27+
dispatch(targetSlice.actions.registered(targetInfo));
3028

31-
// Discover apps from all targets
32-
const allApps: AppInfo[] = [];
33-
for (const target of targetRegistry.getAll()) {
34-
const appsResult = await target.discoverApps();
35-
if (appsResult.ok) {
36-
allApps.push(...appsResult.val);
37-
dispatch(targetSlice.actions.discoveryCompleted(target.id));
29+
// Discover apps for this target
30+
const target = targetRegistry.getById(targetInfo.id);
31+
if (target) {
32+
const appsResult = await target.discoverApps();
33+
if (appsResult.ok) {
34+
dispatch(targetSlice.actions.appsUpdated({
35+
targetId: target.id,
36+
apps: appsResult.val,
37+
}));
38+
dispatch(targetSlice.actions.discoveryCompleted(target.id));
39+
}
3840
}
3941
}
4042

41-
dispatch(appSlice.actions.found(allApps));
42-
4343
// Timer for polling debug endpoints
4444
setInterval(() => {
4545
void (async () => {
@@ -78,79 +78,80 @@ export const init: ThunkActionCreator = () => async (dispatch, getState) => {
7878
}, 3000);
7979
};
8080

81-
export const debug: ThunkActionCreator<AppInfo> = (app) => async (dispatch) => {
82-
try {
83-
// Get the target adapter for this app
84-
const target = targetRegistry.getById(app.targetId);
85-
if (!target) {
86-
throw new Error(`Target ${app.targetId} not found`);
87-
}
88-
89-
// Launch the app using the target adapter
90-
const connectionResult = await target.launch(app, {});
81+
export const debug: ThunkActionCreator<{ targetId: string; app: AppInfo }>
82+
= ({ targetId, app }) => async (dispatch) => {
83+
try {
84+
// Get the target adapter
85+
const target = targetRegistry.getById(targetId);
86+
if (!target) {
87+
throw new Error(`Target ${targetId} not found`);
88+
}
9189

92-
if (!connectionResult.ok) {
93-
throw connectionResult.val;
94-
}
90+
// Launch the app using the target adapter
91+
const connectionResult = await target.launch(app, {});
9592

96-
const connection = connectionResult.val;
97-
const sessionId = connection.connectionId;
98-
99-
// Determine connection type
100-
let connectionType: "local-process" | "remote-adb" | "remote-websocket";
101-
if (target.type === "local") {
102-
connectionType = "local-process";
103-
} else if (target.id.startsWith("remote-adb")) {
104-
connectionType = "remote-adb";
105-
} else {
106-
connectionType = "remote-websocket";
107-
}
93+
if (!connectionResult.ok) {
94+
throw connectionResult.val;
95+
}
10896

109-
// Add session to Redux store
110-
dispatch(
111-
sessionSlice.actions.added({
112-
sessionId,
113-
appId: app.id,
114-
targetId: app.targetId,
115-
connection: {
116-
type: connectionType,
117-
nodePort: connection.debugPorts.node,
118-
windowPort: connection.debugPorts.renderer,
119-
websocketUrl: connection.debugPorts.websocket,
120-
},
121-
}),
122-
);
123-
124-
// Handle local process events
125-
if (connection.processHandle) {
126-
const sp = connection.processHandle;
127-
128-
sp.on("error", (err: Error) => {
129-
dialog.showErrorBox(`Error: ${app.name}`, err.message);
130-
});
97+
const connection = connectionResult.val;
98+
const sessionId = connection.connectionId;
99+
100+
// Determine connection type
101+
let connectionType: "local-process" | "remote-adb" | "remote-websocket";
102+
if (target.type === "local") {
103+
connectionType = "local-process";
104+
} else if (target.id.startsWith("remote-adb")) {
105+
connectionType = "remote-adb";
106+
} else {
107+
connectionType = "remote-websocket";
108+
}
131109

132-
sp.on("close", () => {
133-
dispatch(sessionSlice.actions.removed(sessionId));
134-
void connection.cleanup();
135-
});
110+
// Add session to Redux store
111+
dispatch(
112+
sessionSlice.actions.added({
113+
sessionId,
114+
targetId,
115+
appId: app.id,
116+
connection: {
117+
type: connectionType,
118+
nodePort: connection.debugPorts.node,
119+
windowPort: connection.debugPorts.renderer,
120+
websocketUrl: connection.debugPorts.websocket,
121+
},
122+
}),
123+
);
136124

137-
const handleStdout = (chunk: Buffer) => {
138-
dispatch(
139-
sessionSlice.actions.logAppended({
140-
sessionId,
141-
content: chunk.toString(),
142-
}),
143-
);
144-
};
145-
146-
sp.stdout?.on("data", handleStdout);
147-
sp.stderr?.on("data", handleStdout);
125+
// Handle local process events
126+
if (connection.processHandle) {
127+
const sp = connection.processHandle;
128+
129+
sp.on("error", (err: Error) => {
130+
dialog.showErrorBox(`Error: ${app.name}`, err.message);
131+
});
132+
133+
sp.on("close", () => {
134+
dispatch(sessionSlice.actions.removed(sessionId));
135+
void connection.cleanup();
136+
});
137+
138+
const handleStdout = (chunk: Buffer) => {
139+
dispatch(
140+
sessionSlice.actions.logAppended({
141+
sessionId,
142+
content: chunk.toString(),
143+
}),
144+
);
145+
};
146+
147+
sp.stdout?.on("data", handleStdout);
148+
sp.stderr?.on("data", handleStdout);
149+
}
150+
} catch (error) {
151+
const errorMessage = error instanceof Error ? error.message : String(error);
152+
dialog.showErrorBox(`Error: ${app.name}`, errorMessage);
148153
}
149-
} catch (error) {
150-
const errorMessage = error instanceof Error ? error.message : String(error);
151-
dialog.showErrorBox(`Error: ${app.name}`, errorMessage);
152-
}
153-
};
154+
};
154155

155156
export const debugPath: ThunkActionCreator<string> = () => async () => {
156157
// TODO:
@@ -181,9 +182,10 @@ export const addRemoteDevice: ThunkActionCreator<RemoteDeviceOptions>
181182
// Discover apps from the new target
182183
const appsResult = await target.discoverApps();
183184
if (appsResult.ok) {
184-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
185-
const existingApps = Object.values((dispatch as any).getState?.()?.app ?? {});
186-
dispatch(appSlice.actions.found([...(existingApps as AppInfo[]), ...appsResult.val]));
185+
dispatch(targetSlice.actions.appsUpdated({
186+
targetId: target.id,
187+
apps: appsResult.val,
188+
}));
187189
dispatch(targetSlice.actions.discoveryCompleted(target.id));
188190
}
189191

@@ -200,17 +202,11 @@ export const addRemoteDevice: ThunkActionCreator<RemoteDeviceOptions>
200202

201203
export const removeDevice: ThunkActionCreator<string> = (targetId) => (dispatch) => {
202204
try {
203-
// Remove device from registry
205+
// Remove target from registry
204206
targetRegistry.unregister(targetId);
205207

206-
// Remove from Redux store
208+
// Remove from Redux store (apps will be removed automatically as they're nested)
207209
dispatch(targetSlice.actions.unregistered(targetId));
208-
209-
// Remove all apps from this device
210-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
211-
const existingApps = Object.values((dispatch as any).getState?.()?.app ?? {});
212-
const filteredApps = (existingApps as AppInfo[]).filter((app) => app.targetId !== targetId);
213-
dispatch(appSlice.actions.found(filteredApps));
214210
} catch (error) {
215211
const errorMessage = error instanceof Error ? error.message : String(error);
216212
dialog.showErrorBox("Remove Device Error", errorMessage);
@@ -227,13 +223,10 @@ export const refreshDeviceApps: ThunkActionCreator<string> = (targetId) => async
227223
// Discover apps from the target
228224
const appsResult = await target.discoverApps();
229225
if (appsResult.ok) {
230-
// Get existing apps from other devices
231-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
232-
const existingApps = Object.values((dispatch as any).getState?.()?.app ?? {});
233-
const otherDeviceApps = (existingApps as AppInfo[]).filter((app) => app.targetId !== targetId);
234-
235-
// Merge with new apps from this device
236-
dispatch(appSlice.actions.found([...otherDeviceApps, ...appsResult.val]));
226+
dispatch(targetSlice.actions.appsUpdated({
227+
targetId,
228+
apps: appsResult.val,
229+
}));
237230
dispatch(targetSlice.actions.discoveryCompleted(targetId));
238231
}
239232
} catch (error) {

src/main/store.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { configureStore } from "@reduxjs/toolkit";
22
import { stateSyncEnhancer } from "electron-redux/main";
33

4-
import { appSlice } from "../reducers/app";
5-
import { sessionSlice } from "../reducers/session";
64
import { targetSlice } from "../reducers/target";
5+
import { sessionSlice } from "../reducers/session";
76

87
export const store = configureStore({
98
reducer: {
10-
app: appSlice.reducer,
11-
session: sessionSlice.reducer,
129
target: targetSlice.reducer,
10+
session: sessionSlice.reducer,
1311
},
1412
enhancers: (g) => g().concat(stateSyncEnhancer()),
1513
});

src/main/targets/local/adapter.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import getPort from "get-port";
55
import { Result } from "ts-results";
66
import { v4 } from "uuid";
77

8-
import type { AppInfo } from "../../../reducers/app";
8+
import type { AppInfo } from "../../../reducers/target";
99
import type { DebugConnection, LaunchOptions, TargetAdapter } from "../types";
1010

1111
import { importByPlatform } from "./platforms";
@@ -44,12 +44,9 @@ export class LocalTargetAdapter implements TargetAdapter {
4444

4545
const apps = result.val;
4646

47-
// Add target metadata to each app
47+
// Return apps with metadata (device context is added by caller)
4848
return apps.map((app) => ({
4949
...app,
50-
id: `${this.id}:${app.id}`,
51-
targetId: this.id,
52-
targetType: "local" as const,
5350
metadata: {
5451
platform: process.platform,
5552
},

src/main/targets/remote/adb.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { promisify } from "util";
44
import { Result } from "ts-results";
55
import { v4 } from "uuid";
66

7-
import type { AppInfo } from "../../../reducers/app";
7+
import type { AppInfo } from "../../../reducers/target";
88
import type { DebugConnection, TargetAdapter } from "../types";
99

1010
const execAsync = promisify(exec);
@@ -59,19 +59,17 @@ export class AdbTargetAdapter implements TargetAdapter {
5959
.filter((line) => line.startsWith("package:"))
6060
.map((line) => line.replace("package:", "").trim());
6161

62-
// Get app labels for each package
62+
// Get app labels for each package (device context is added by caller)
6363
const apps: AppInfo[] = packageNames.map((packageName) => {
6464
const label = packageName;
6565
// TODO: Try to get app label from package manager
6666
// This is a simplified approach - ideally we'd parse the APK using aapt
6767
// For now, we just use the package name
6868

6969
return {
70-
id: `${this.id}:${packageName}`,
70+
id: packageName,
7171
name: label,
7272
icon: "", // TODO: Extract app icon from APK
73-
targetId: this.id,
74-
targetType: "remote" as const,
7573
metadata: {
7674
packageName,
7775
deviceInfo: `ADB ${this.address}:${this.port}`,
@@ -88,7 +86,7 @@ export class AdbTargetAdapter implements TargetAdapter {
8886
): Promise<Result<DebugConnection, Error>> {
8987
return Result.wrapAsync(async () => {
9088
// Extract package name from app metadata or ID
91-
const packageName = app.metadata?.packageName ?? app.id.replace(`${this.id}:`, "");
89+
const packageName = app.metadata?.packageName ?? app.id;
9290

9391
// Get the main activity for the package
9492
const activityOutput = await this.execAdb([

src/main/targets/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ChildProcess } from "child_process";
22
import type { Result } from "ts-results";
33

4-
import type { AppInfo } from "../../reducers/app";
4+
import type { AppInfo } from "../../reducers/target";
55

66
export type TargetType = "local" | "remote";
77

0 commit comments

Comments
 (0)