Skip to content

Commit 23b5023

Browse files
authored
Make setup modularize (#32)
1 parent ba7a52f commit 23b5023

1 file changed

Lines changed: 140 additions & 61 deletions

File tree

src/runtime/setup.ts

Lines changed: 140 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,22 @@ export function setup(): Promise<void> {
5555
return initPromise;
5656
}
5757

58-
async function doSetup(): Promise<void> {
59-
brakitDebug(`[setup] doSetup called at ${new Date().toISOString()}`);
60-
61-
const bus = new EventBus();
62-
const registry = new ServiceRegistry();
58+
/* ------------------------------------------------------------------ */
59+
/* Phase 1 — Create stores & wire event subscriptions */
60+
/* ------------------------------------------------------------------ */
61+
62+
interface Stores {
63+
requestStore: RequestStore;
64+
fetchStore: FetchStore;
65+
logStore: LogStore;
66+
errorStore: ErrorStore;
67+
queryStore: QueryStore;
68+
}
6369

70+
function createStores(
71+
bus: EventBus,
72+
registry: ServiceRegistry,
73+
): Stores {
6474
const requestStore = new RequestStore();
6575
const fetchStore = new FetchStore();
6676
const logStore = new LogStore();
@@ -81,6 +91,16 @@ async function doSetup(): Promise<void> {
8191

8292
requestStore.onRequest((req) => bus.emit("request:completed", req));
8393

94+
return { requestStore, fetchStore, logStore, errorStore, queryStore };
95+
}
96+
97+
/* ------------------------------------------------------------------ */
98+
/* Phase 2 — Install instrumentation hooks */
99+
/* ------------------------------------------------------------------ */
100+
101+
function installHooks(
102+
bus: EventBus,
103+
): { framework: Framework; adapterNames: string[] } {
84104
const telemetryEmit = (event: TelemetryEvent): void => {
85105
const channel = `telemetry:${event.type}` as keyof ChannelMap;
86106
bus.emit(channel, event.data as ChannelMap[typeof channel]);
@@ -98,22 +118,37 @@ async function doSetup(): Promise<void> {
98118
let framework: Framework = "unknown";
99119
try {
100120
const pkg = JSON.parse(
101-
await readFile(resolve(cwd, "package.json"), "utf-8"),
121+
// readFileSync is acceptable here — runs once at startup
122+
require("node:fs").readFileSync(resolve(cwd, "package.json"), "utf-8"),
102123
);
103124
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
104125
framework = detectFrameworkFromDeps(allDeps);
105126
} catch {
106127
/* no package.json */
107128
}
108129

109-
initSession(
130+
return {
110131
framework,
111-
detectPackageManagerSync(cwd),
112-
false,
113-
adapterRegistry.getActive().map((a) => a.name),
114-
);
132+
adapterNames: adapterRegistry.getActive().map((a) => a.name),
133+
};
134+
}
115135

116-
const dataDir = getProjectDataDir(cwd);
136+
/* ------------------------------------------------------------------ */
137+
/* Phase 3 — Start analysis, metrics & issue tracking */
138+
/* ------------------------------------------------------------------ */
139+
140+
interface AnalysisServices {
141+
analysisEngine: AnalysisEngine;
142+
metricsStore: MetricsStore;
143+
issueStore: IssueStore;
144+
}
145+
146+
function startAnalysis(
147+
registry: ServiceRegistry,
148+
stores: Stores,
149+
dataDir: string,
150+
): AnalysisServices {
151+
const bus = registry.get("event-bus");
117152

118153
const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
119154
metricsStore.start();
@@ -128,15 +163,102 @@ async function doSetup(): Promise<void> {
128163
registry.register("analysis-engine", analysisEngine);
129164

130165
bus.on("request:completed", (req) => {
131-
const queries = queryStore.getByRequest(req.id);
132-
const fetches = fetchStore.getByRequest(req.id);
166+
const queries = stores.queryStore.getByRequest(req.id);
167+
const fetches = stores.fetchStore.getByRequest(req.id);
133168
metricsStore.recordRequest(req, {
134169
queryCount: queries.length,
135170
queryTimeMs: queries.reduce((s, q) => s + q.durationMs, 0),
136171
fetchTimeMs: fetches.reduce((s, f) => s + f.durationMs, 0),
137172
});
138173
});
139174

175+
return { analysisEngine, metricsStore, issueStore };
176+
}
177+
178+
/* ------------------------------------------------------------------ */
179+
/* Phase 4 — Register lifecycle (teardown + process handlers) */
180+
/* ------------------------------------------------------------------ */
181+
182+
function registerLifecycle(
183+
registry: ServiceRegistry,
184+
stores: Stores,
185+
services: AnalysisServices,
186+
cwd: string,
187+
): void {
188+
let telemetrySent = false;
189+
const sendTelemetry = (): void => {
190+
if (telemetrySent) return;
191+
telemetrySent = true;
192+
recordRequestCount(stores.requestStore.getAll().length);
193+
recordInsightTypes(services.analysisEngine.getInsights().map((i) => i.type));
194+
recordRulesTriggered(
195+
services.analysisEngine.getFindings().map((f) => f.rule),
196+
);
197+
trackSession(registry);
198+
};
199+
200+
let teardownCalled = false;
201+
const runTeardown = (): void => {
202+
if (teardownCalled) return;
203+
teardownCalled = true;
204+
205+
sendTelemetry();
206+
uninstallInterceptor();
207+
services.analysisEngine.stop();
208+
services.issueStore.stop();
209+
services.metricsStore.stop();
210+
211+
try {
212+
const portPath = resolve(cwd, PORT_FILE);
213+
if (existsSync(portPath)) unlinkSync(portPath);
214+
} catch (err) {
215+
brakitDebug(`[setup] port file cleanup failed: ${getErrorMessage(err)}`);
216+
}
217+
};
218+
219+
health.setTeardown(runTeardown);
220+
221+
// Send telemetry while async operations still work (before 'exit').
222+
process.on("beforeExit", () => {
223+
sendTelemetry();
224+
});
225+
// Run full teardown on exit — only sync code runs here, which is fine
226+
// because runTeardown is fully synchronous. Do NOT call process.exit()
227+
// — let the host app control its own shutdown lifecycle.
228+
process.on("exit", () => {
229+
runTeardown();
230+
});
231+
}
232+
233+
/* ------------------------------------------------------------------ */
234+
/* Orchestrator */
235+
/* ------------------------------------------------------------------ */
236+
237+
async function doSetup(): Promise<void> {
238+
brakitDebug(`[setup] doSetup called at ${new Date().toISOString()}`);
239+
240+
const bus = new EventBus();
241+
const registry = new ServiceRegistry();
242+
const cwd = process.cwd();
243+
244+
// Phase 1 — stores & event wiring
245+
const stores = createStores(bus, registry);
246+
247+
// Phase 2 — instrumentation hooks
248+
const { framework, adapterNames } = installHooks(bus);
249+
250+
initSession(
251+
framework,
252+
detectPackageManagerSync(cwd),
253+
false,
254+
adapterNames,
255+
);
256+
257+
// Phase 3 — analysis, metrics & issues
258+
const dataDir = getProjectDataDir(cwd);
259+
const services = startAnalysis(registry, stores, dataDir);
260+
261+
// Phase 4 — HTTP interceptor + dashboard
140262
const config: BrakitConfig = {
141263
proxyPort: 0,
142264
targetPort: 0,
@@ -146,15 +268,12 @@ async function doSetup(): Promise<void> {
146268

147269
const handleDashboard = createDashboardHandler(registry);
148270

149-
let terminalDispose: (() => void) | null = null;
150-
151271
installInterceptor({
152272
handleDashboard,
153273
config,
154-
requestStore,
274+
requestStore: stores.requestStore,
155275
onFirstRequest(port) {
156276
setBrakitPort(port);
157-
158277
brakitDebug(`[setup] onFirstRequest fired, port=${port}`);
159278

160279
void (async () => {
@@ -184,53 +303,13 @@ async function doSetup(): Promise<void> {
184303
}
185304
})();
186305

187-
terminalDispose = startTerminalInsights(registry, port);
306+
startTerminalInsights(registry, port);
188307
process.stdout.write(
189308
` brakit v${VERSION} — http://localhost:${port}${DASHBOARD_PREFIX}\n`,
190309
);
191310
},
192311
});
193312

194-
let telemetrySent = false;
195-
const sendTelemetry = (): void => {
196-
if (telemetrySent) return;
197-
telemetrySent = true;
198-
recordRequestCount(requestStore.getAll().length);
199-
recordInsightTypes(analysisEngine.getInsights().map((i) => i.type));
200-
recordRulesTriggered(analysisEngine.getFindings().map((f) => f.rule));
201-
trackSession(registry);
202-
};
203-
204-
let teardownCalled = false;
205-
const runTeardown = (): void => {
206-
if (teardownCalled) return;
207-
teardownCalled = true;
208-
209-
sendTelemetry();
210-
uninstallInterceptor();
211-
terminalDispose?.();
212-
analysisEngine.stop();
213-
issueStore.stop();
214-
metricsStore.stop();
215-
216-
try {
217-
const portPath = resolve(cwd, PORT_FILE);
218-
if (existsSync(portPath)) unlinkSync(portPath);
219-
} catch (err) {
220-
brakitDebug(`[setup] port file cleanup failed: ${getErrorMessage(err)}`);
221-
}
222-
};
223-
224-
health.setTeardown(runTeardown);
225-
226-
// Send telemetry while async operations still work (before 'exit').
227-
process.on("beforeExit", () => {
228-
sendTelemetry();
229-
});
230-
// Run full teardown on exit — only sync code runs here, which is fine
231-
// because runTeardown is fully synchronous. Do NOT call process.exit()
232-
// — let the host app control its own shutdown lifecycle.
233-
process.on("exit", () => {
234-
runTeardown();
235-
});
313+
// Phase 5 — lifecycle (teardown + process handlers)
314+
registerLifecycle(registry, stores, services, cwd);
236315
}

0 commit comments

Comments
 (0)