Skip to content

Commit 8170760

Browse files
committed
🤖 refactor: convert ExtensionMetadataService to use fs promises
Replaced synchronous fs operations with async fs/promises throughout ExtensionMetadataService. Updated IpcMain and all callers to use static factory methods for async initialization. Changes: - ExtensionMetadataService: Use fs/promises (readFile, writeFile, mkdir) - Added ExtensionMetadataService.create() static factory method - Made all write methods async (updateRecency, setStreaming, deleteWorkspace) - Added IpcMain.create() static factory method for async initialization - Updated all callers: main-desktop.ts, main-server.ts, tests/ipcMain/setup.ts, bench/headlessEnvironment.ts - Used 'void' keyword for fire-and-forget async calls in event handlers Benefits: - Non-blocking I/O operations - Consistent async patterns throughout codebase - Better error handling potential - No blocking main thread on file operations
1 parent 28ed10a commit 8170760

File tree

6 files changed

+107
-63
lines changed

6 files changed

+107
-63
lines changed

src/bench/headlessEnvironment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export async function createHeadlessEnvironment(
104104
const mockIpcMainModule = mockedElectron.ipcMain;
105105
const mockIpcRendererModule = mockedElectron.ipcRenderer;
106106

107-
const ipcMain = new IpcMain(config);
107+
const ipcMain = await IpcMain.create(config);
108108
ipcMain.register(mockIpcMainModule, mockWindow);
109109

110110
const dispose = async () => {

src/main-desktop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ async function loadServices(): Promise<void> {
315315
]);
316316
/* eslint-enable no-restricted-syntax */
317317
config = new ConfigClass();
318-
ipcMain = new IpcMainClass(config);
318+
ipcMain = await IpcMainClass.create(config);
319319

320320
loadTokenizerModules().catch((error) => {
321321
console.error("Failed to preload tokenizer modules:", error);

src/main-server.ts

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -135,26 +135,28 @@ const app = express();
135135
app.use(cors());
136136
app.use(express.json({ limit: "50mb" }));
137137

138-
// Initialize config and IPC service
139-
const config = new Config();
140-
const ipcMainService = new IpcMain(config);
141-
142138
// Track WebSocket clients and their subscriptions
143139
const clients: Clients = new Map();
144140

145141
const mockWindow = new MockBrowserWindow(clients);
146142
const httpIpcMain = new HttpIpcMainAdapter(app);
147143

148-
// Register IPC handlers
149-
ipcMainService.register(
150-
httpIpcMain as unknown as ElectronIpcMain,
151-
mockWindow as unknown as BrowserWindow
152-
);
153-
154-
// Add custom endpoint for launch project (only for server mode)
155-
httpIpcMain.handle("server:getLaunchProject", () => {
156-
return Promise.resolve(launchProjectPath);
157-
});
144+
// Initialize async services and register handlers
145+
(async () => {
146+
// Initialize config and IPC service
147+
const config = new Config();
148+
const ipcMainService = await IpcMain.create(config);
149+
150+
// Register IPC handlers
151+
ipcMainService.register(
152+
httpIpcMain as unknown as ElectronIpcMain,
153+
mockWindow as unknown as BrowserWindow
154+
);
155+
156+
// Add custom endpoint for launch project (only for server mode)
157+
httpIpcMain.handle("server:getLaunchProject", () => {
158+
return Promise.resolve(launchProjectPath);
159+
});
158160

159161
// Serve static files from dist directory (built renderer)
160162
app.use(express.static(path.join(__dirname, ".")));
@@ -338,12 +340,17 @@ async function initializeProject(
338340
}
339341
}
340342

341-
server.listen(PORT, HOST, () => {
342-
console.log(`Server is running on http://${HOST}:${PORT}`);
343+
// Start server after initialization
344+
server.listen(PORT, HOST, () => {
345+
console.log(`Server is running on http://${HOST}:${PORT}`);
343346

344-
// Handle --add-project flag if present
345-
if (ADD_PROJECT_PATH) {
346-
console.log(`Initializing project at: ${ADD_PROJECT_PATH}`);
347-
void initializeProject(ADD_PROJECT_PATH, httpIpcMain);
348-
}
347+
// Handle --add-project flag if present
348+
if (ADD_PROJECT_PATH) {
349+
console.log(`Initializing project at: ${ADD_PROJECT_PATH}`);
350+
void initializeProject(ADD_PROJECT_PATH, httpIpcMain);
351+
}
352+
});
353+
})().catch((error) => {
354+
console.error("Failed to initialize server:", error);
355+
process.exit(1);
349356
});

src/services/ExtensionMetadataService.ts

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { dirname } from "path";
2-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2+
import { mkdir, readFile, writeFile } from "fs/promises";
3+
import { existsSync } from "fs";
34
import {
45
type ExtensionMetadata,
56
type ExtensionMetadataFile,
@@ -29,29 +30,42 @@ export class ExtensionMetadataService {
2930
private readonly filePath: string;
3031
private data: ExtensionMetadataFile;
3132

32-
constructor(filePath?: string) {
33-
this.filePath = filePath ?? getExtensionMetadataPath();
33+
private constructor(filePath: string, data: ExtensionMetadataFile) {
34+
this.filePath = filePath;
35+
this.data = data;
36+
}
37+
38+
/**
39+
* Create a new ExtensionMetadataService instance.
40+
* Use this static factory method instead of the constructor.
41+
*/
42+
static async create(filePath?: string): Promise<ExtensionMetadataService> {
43+
const path = filePath ?? getExtensionMetadataPath();
3444

3545
// Ensure directory exists
36-
const dir = dirname(this.filePath);
46+
const dir = dirname(path);
3747
if (!existsSync(dir)) {
38-
mkdirSync(dir, { recursive: true });
48+
await mkdir(dir, { recursive: true });
3949
}
4050

4151
// Load existing data or initialize
42-
this.data = this.load();
52+
const data = await ExtensionMetadataService.loadData(path);
53+
54+
const service = new ExtensionMetadataService(path, data);
4355

4456
// Clear stale streaming flags (from crashes)
45-
this.clearStaleStreaming();
57+
await service.clearStaleStreaming();
58+
59+
return service;
4660
}
4761

48-
private load(): ExtensionMetadataFile {
49-
if (!existsSync(this.filePath)) {
62+
private static async loadData(filePath: string): Promise<ExtensionMetadataFile> {
63+
if (!existsSync(filePath)) {
5064
return { version: 1, workspaces: {} };
5165
}
5266

5367
try {
54-
const content = readFileSync(this.filePath, "utf-8");
68+
const content = await readFile(filePath, "utf-8");
5569
const parsed = JSON.parse(content) as ExtensionMetadataFile;
5670

5771
// Validate structure
@@ -69,12 +83,10 @@ export class ExtensionMetadataService {
6983
}
7084
}
7185

72-
private save() {
86+
private async save(): Promise<void> {
7387
try {
7488
const content = JSON.stringify(this.data, null, 2);
75-
// Simple synchronous write - atomic enough for our use case
76-
// VS Code extension only reads, never writes concurrently
77-
writeFileSync(this.filePath, content, "utf-8");
89+
await writeFile(this.filePath, content, "utf-8");
7890
} catch (error) {
7991
console.error("[ExtensionMetadataService] Failed to save metadata:", error);
8092
}
@@ -84,7 +96,7 @@ export class ExtensionMetadataService {
8496
* Update the recency timestamp for a workspace.
8597
* Call this on user messages or other interactions.
8698
*/
87-
updateRecency(workspaceId: string, timestamp: number = Date.now()) {
99+
async updateRecency(workspaceId: string, timestamp: number = Date.now()): Promise<void> {
88100
if (!this.data.workspaces[workspaceId]) {
89101
this.data.workspaces[workspaceId] = {
90102
recency: timestamp,
@@ -94,14 +106,14 @@ export class ExtensionMetadataService {
94106
} else {
95107
this.data.workspaces[workspaceId].recency = timestamp;
96108
}
97-
this.save();
109+
await this.save();
98110
}
99111

100112
/**
101113
* Set the streaming status for a workspace.
102114
* Call this when streams start/end.
103115
*/
104-
setStreaming(workspaceId: string, streaming: boolean, model?: string) {
116+
async setStreaming(workspaceId: string, streaming: boolean, model?: string): Promise<void> {
105117
const now = Date.now();
106118
if (!this.data.workspaces[workspaceId]) {
107119
this.data.workspaces[workspaceId] = {
@@ -115,7 +127,7 @@ export class ExtensionMetadataService {
115127
this.data.workspaces[workspaceId].lastModel = model;
116128
}
117129
}
118-
this.save();
130+
await this.save();
119131
}
120132

121133
/**
@@ -158,18 +170,18 @@ export class ExtensionMetadataService {
158170
* Delete metadata for a workspace.
159171
* Call this when a workspace is deleted.
160172
*/
161-
deleteWorkspace(workspaceId: string) {
173+
async deleteWorkspace(workspaceId: string): Promise<void> {
162174
if (this.data.workspaces[workspaceId]) {
163175
delete this.data.workspaces[workspaceId];
164-
this.save();
176+
await this.save();
165177
}
166178
}
167179

168180
/**
169181
* Clear all streaming flags.
170182
* Call this on app startup to clean up stale streaming states from crashes.
171183
*/
172-
clearStaleStreaming() {
184+
async clearStaleStreaming(): Promise<void> {
173185
let modified = false;
174186
for (const entry of Object.values(this.data.workspaces)) {
175187
if (entry.streaming) {
@@ -178,7 +190,7 @@ export class ExtensionMetadataService {
178190
}
179191
}
180192
if (modified) {
181-
this.save();
193+
await this.save();
182194
}
183195
}
184196
}

src/services/ipcMain.ts

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,46 @@ export class IpcMain {
5959

6060
private registered = false;
6161

62-
constructor(config: Config) {
62+
private constructor(
63+
config: Config,
64+
historyService: HistoryService,
65+
partialService: PartialService,
66+
initStateManager: InitStateManager,
67+
extensionMetadata: ExtensionMetadataService,
68+
aiService: AIService
69+
) {
6370
this.config = config;
64-
this.historyService = new HistoryService(config);
65-
this.partialService = new PartialService(config, this.historyService);
66-
this.initStateManager = new InitStateManager(config);
67-
this.extensionMetadata = new ExtensionMetadataService();
68-
this.aiService = new AIService(
69-
config,
70-
this.historyService,
71-
this.partialService,
72-
this.initStateManager
73-
);
71+
this.historyService = historyService;
72+
this.partialService = partialService;
73+
this.initStateManager = initStateManager;
74+
this.extensionMetadata = extensionMetadata;
75+
this.aiService = aiService;
7476

7577
// Listen to AIService events to update metadata
7678
this.setupMetadataListeners();
7779
}
7880

81+
/**
82+
* Create a new IpcMain instance.
83+
* Use this static factory method instead of the constructor.
84+
*/
85+
static async create(config: Config): Promise<IpcMain> {
86+
const historyService = new HistoryService(config);
87+
const partialService = new PartialService(config, historyService);
88+
const initStateManager = new InitStateManager(config);
89+
const extensionMetadata = await ExtensionMetadataService.create();
90+
const aiService = new AIService(config, historyService, partialService, initStateManager);
91+
92+
return new IpcMain(
93+
config,
94+
historyService,
95+
partialService,
96+
initStateManager,
97+
extensionMetadata,
98+
aiService
99+
);
100+
}
101+
79102
/**
80103
* Setup listeners to update metadata store based on AIService events.
81104
* This tracks workspace recency and streaming status for VS Code extension integration.
@@ -90,14 +113,16 @@ export class IpcMain {
90113
// Update streaming status and recency on stream start
91114
this.aiService.on("stream-start", (data: unknown) => {
92115
if (isStreamStartEvent(data)) {
93-
this.extensionMetadata.setStreaming(data.workspaceId, true, data.model);
116+
// Fire and forget - don't block event handler
117+
void this.extensionMetadata.setStreaming(data.workspaceId, true, data.model);
94118
}
95119
});
96120

97121
// Clear streaming status on stream end/abort
98122
const handleStreamStop = (data: unknown) => {
99123
if (isWorkspaceEvent(data)) {
100-
this.extensionMetadata.setStreaming(data.workspaceId, false);
124+
// Fire and forget - don't block event handler
125+
void this.extensionMetadata.setStreaming(data.workspaceId, false);
101126
}
102127
};
103128
this.aiService.on("stream-end", handleStreamStop);
@@ -692,8 +717,8 @@ export class IpcMain {
692717
try {
693718
const session = this.getOrCreateSession(workspaceId);
694719

695-
// Update recency on user message
696-
this.extensionMetadata.updateRecency(workspaceId);
720+
// Update recency on user message (fire and forget)
721+
void this.extensionMetadata.updateRecency(workspaceId);
697722

698723
const result = await session.sendMessage(message, options);
699724
if (!result.success) {
@@ -1087,8 +1112,8 @@ export class IpcMain {
10871112
return { success: false, error: aiResult.error };
10881113
}
10891114

1090-
// Delete workspace metadata
1091-
this.extensionMetadata.deleteWorkspace(workspaceId);
1115+
// Delete workspace metadata (fire and forget)
1116+
void this.extensionMetadata.deleteWorkspace(workspaceId);
10921117

10931118
// Update config to remove the workspace from all projects
10941119
const projectsConfig = this.config.loadConfigOrDefault();

tests/ipcMain/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export async function createTestEnvironment(): Promise<TestEnvironment> {
6666
const mockIpcRendererModule = mocked.ipcRenderer;
6767

6868
// Create IpcMain instance
69-
const ipcMain = new IpcMain(config);
69+
const ipcMain = await IpcMain.create(config);
7070

7171
// Register handlers with mock ipcMain and window
7272
ipcMain.register(mockIpcMainModule, mockWindow);

0 commit comments

Comments
 (0)