11diff --git a/lib/commonjs/cli.js b/lib/commonjs/cli.js
2- index 35c04da87c63a79bc26707aaffe3156fb98eca79..cc9243fabdc014bc945dd6e85cbe55d85692ad56 100644
2+ index 35c04da87c63a79bc26707aaffe3156fb98eca79..3ad2cea4c214e8c3c4f4698acacfff2610902863 100644
33--- a/lib/commonjs/cli.js
44+++ b/lib/commonjs/cli.js
5- @@ -72,22 +72,64 @@ function cleanup() {
5+ @@ -71,23 +71,77 @@ function cleanup() {
6+ console.log(`[🟥] Failed to run a cleanup task: ${err.message}`);
67 });
78 }
8- async function startServer(server, config, after) {
9+ - async function startServer(server, config, after) {
10+ + async function startServer(server, config, after, runControl) {
911+ let reconnectGraceTimer = null;
1012+ let pendingDisconnectCode = null;
1113+ let disconnectStartedAt = 0;
14+ + let orchestratePhase = 'jet-starting';
15+ + let lastTestTitle = '';
16+ + let lastSuiteTitle = '';
17+ + let coverageUploadInProgress = false;
1218 server.on('started', s => {
1319 const url = s.url;
1420 console.log(`[🟩] Jet remote server listening at "${url}".`);
21+ + startControlHttpServer(config, {
22+ + setOrchestratePhase: phase => {
23+ + orchestratePhase = phase || orchestratePhase;
24+ + },
25+ + onLaunchReady: () => runControl?.beginRun?.(),
26+ + });
1527 });
1628 server.on('connection', (_, req) => {
1729 console.log(`[🟩] Jet client connected from "${req.socket.remoteAddress + ':' + req.socket.remotePort}".`);
@@ -34,12 +46,14 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..cc9243fabdc014bc945dd6e85cbe55d8
3446- cleanup();
3547+ try {
3648+ const os = require('os');
49+ + const suite = lastSuiteTitle.replace(/"/g, "'");
50+ + const test = lastTestTitle.replace(/"/g, "'");
3751+ console.warn(
38- + `[jet-ws] disconnect_context code=${code} loadavg=${os.loadavg().join(',')} mem=${JSON.stringify(process.memoryUsage())}`,
52+ + `[jet-ws] disconnect_context code=${code} loadavg=${os.loadavg().join(',')} orchestratePhase=${orchestratePhase} currentTest="${test}" currentSuite="${suite}" coverageUpload=${coverageUploadInProgress} mem=${JSON.stringify(process.memoryUsage())}`,
3953+ );
4054+ } catch (_) {
4155+ // Best-effort diagnostics only.
42- }
56+ + }
4357+ if (code === 1000 || !config.exitOnError) {
4458+ return;
4559+ }
@@ -63,30 +77,41 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..cc9243fabdc014bc945dd6e85cbe55d8
6377+ cleanup();
6478+ }, graceMs);
6579+ return;
66- + }
80+ }
6781+ console.error(`[jet-ws] fatal_disconnect code=${code} reason="${reason || ''}" immediate=true`);
6882+ print(`[🟥] Exiting after an abnormal disconnect.`);
6983+ process.exitCode = 1;
7084+ cleanup();
7185 });
7286 server.on('error', error => {
7387 if (error instanceof _mochaRemoteServer.ClientError) {
74- @@ -100,6 +142,14 @@ async function startServer(server, config, after) {
88+ @@ -100,6 +154,25 @@ async function startServer(server, config, after) {
7589 cleanup();
7690 }
7791 });
92+ + server.on('coverage-upload-start', () => {
93+ + coverageUploadInProgress = true;
94+ + });
7895+ server.on('coverage-data', coverage => {
96+ + coverageUploadInProgress = false;
7997+ const incomingKeys = Object.keys(coverage).length;
8098+ console.log(`[jet-coverage] WS received ${incomingKeys} file(s)`);
8199+ coverageMap.merge(coverage);
82100+ global.__coverage__ = coverageMap.toJSON();
83101+ const totalKeys = Object.keys(global.__coverage__).length;
84102+ console.log(`[jet-coverage] WS merged total ${totalKeys} file(s)`);
103+ + });
104+ + server.on('running', runner => {
105+ + const constants = require('mocha').Runner.constants;
106+ + runner.on(constants.EVENT_TEST_BEGIN, test => {
107+ + lastTestTitle = test?.title || '';
108+ + lastSuiteTitle = test?.parent?.fullTitle?.() || test?.parent?.title || '';
109+ + });
85110+ });
86111 cleanupTasks.add(async () => {
87112 if (server.listening) {
88113 await server.stop();
89- @@ -115,41 +165,6 @@ async function startServer(server, config, after) {
114+ @@ -115,39 +188,48 @@ async function startServer(server, config, after) {
90115 process.on('SIGINT', cleanup);
91116 process.on('exit', cleanup);
92117 }
@@ -98,12 +123,25 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..cc9243fabdc014bc945dd6e85cbe55d8
98123- }
99124- server.on('request', (req, res) => {
100125- if (req.url === '/coverage' && req.method === 'POST') {
101- - let body = '';
102- - req.on('data', chunk => {
103- - body += chunk.toString();
104- - });
105- - req.on('end', () => {
106- - try {
126+ + function startControlHttpServer(config, handlers) {
127+ + const http = require('http');
128+ + const controlPort = parseInt(process.env.RNFB_JET_CONTROL_PORT || String(config.port + 1), 10);
129+ + const server = http.createServer((req, res) => {
130+ + const url = req.url?.split('?')[0];
131+ + if (url === '/launch-ready' && req.method === 'POST') {
132+ + console.log('[jet-control] launch-ready received');
133+ + handlers.onLaunchReady?.();
134+ + res.writeHead(200, { 'Content-Type': 'text/plain' });
135+ + res.end('ok');
136+ + return;
137+ + }
138+ + if (url === '/orchestrate-state' && req.method === 'POST') {
139+ let body = '';
140+ req.on('data', chunk => {
141+ body += chunk.toString();
142+ });
143+ req.on('end', () => {
144+ try {
107145- coverageMap.merge(JSON.parse(body));
108146- global.__coverage__ = coverageMap.toJSON();
109147- res.end(JSON.stringify({
@@ -113,36 +151,85 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..cc9243fabdc014bc945dd6e85cbe55d8
113151- res.end(JSON.stringify({
114152- error: 'Invalid JSON'
115153- }));
116- - }
117- - });
154+ + const parsed = JSON.parse(body || '{}');
155+ + if (parsed.phase) {
156+ + handlers.setOrchestratePhase?.(parsed.phase);
157+ + console.log(`[jet-control] orchestrate-state=${parsed.phase}`);
158+ + }
159+ + } catch (_) {
160+ + // Best-effort only.
161+ }
162+ + res.writeHead(200, { 'Content-Type': 'text/plain' });
163+ + res.end('ok');
164+ });
118165- } else {
119166- res.writeHead(404, {
120167- 'Content-Type': 'application/json'
121168- });
122169- res.end(JSON.stringify({
123170- error: 'Not Found'
124171- }));
125- - }
126- - });
127- - }
172+ + return;
173+ }
174+ + res.writeHead(404, { 'Content-Type': 'application/json' });
175+ + res.end(JSON.stringify({ error: 'Not Found' }));
176+ + });
177+ + server.listen(controlPort, config.hostname, () => {
178+ + console.log(`[jet-control] listening on http://${config.hostname}:${controlPort}`);
179+ + });
180+ + cleanupTasks.add(async () => {
181+ + await new Promise(resolve => {
182+ + server.close(() => resolve());
183+ + });
184+ });
185+ }
128186 (0, _yargs.default)((0, _helpers.hideBin)(process.argv)).scriptName('jet').version('version', 'Show version number & exit', jetPackageJson.version).alias('v', 'version').option('target', {
129- description: 'The name of the test target to run as defined in your "jet" package.json config.',
130- type: 'string',
131- @@ -302,12 +317,11 @@ function attachHttpServer(wss) {
187+ @@ -301,17 +383,34 @@ function attachHttpServer(wss) {
188+ timeout: finalConfig.timeout,
132189 slow: finalConfig.slow
133190 });
134- return startServer(server, finalConfig, target.after).then(() => {
191+ - return startServer(server, finalConfig, target.after).then(() => {
135192- if (finalConfig.coverage) {
136193- attachHttpServer(server.wss);
137194- }
138- if (!finalConfig.watch) {
139- server.run(async failures => {
140- global.__coverage__ = coverageMap.toJSON();
141- + const mergedKeys = Object.keys(global.__coverage__).length;
142- + console.log(`[jet-coverage] merged ${mergedKeys} file(s) before NYC shutdown`);
143- await cleanup();
144- process.exit(failures > 0 ? 1 : 0);
145- });
195+ - if (!finalConfig.watch) {
196+ - server.run(async failures => {
197+ - global.__coverage__ = coverageMap.toJSON();
198+ - await cleanup();
199+ - process.exit(failures > 0 ? 1 : 0);
200+ - });
201+ + const deferRun =
202+ + process.env.RNFB_JET_DEFER_RUN === '1' || process.env.RNFB_JET_DEFER_RUN === 'true';
203+ + const runControl = {
204+ + beginRun: null,
205+ + };
206+ + return startServer(server, finalConfig, target.after, runControl).then(() => {
207+ + let runStarted = false;
208+ + const beginRun = () => {
209+ + if (runStarted) {
210+ + return;
211+ + }
212+ + runStarted = true;
213+ + if (!finalConfig.watch) {
214+ + server.run(async failures => {
215+ + global.__coverage__ = coverageMap.toJSON();
216+ + const mergedKeys = Object.keys(global.__coverage__).length;
217+ + console.log(`[jet-coverage] merged ${mergedKeys} file(s) before NYC shutdown`);
218+ + await cleanup();
219+ + process.exit(failures > 0 ? 1 : 0);
220+ + });
221+ + }
222+ + };
223+ + runControl.beginRun = beginRun;
224+ + if (deferRun) {
225+ + console.log('[jet-control] deferring server.run until POST /launch-ready');
226+ + return;
227+ }
228+ + beginRun();
229+ });
230+ }).help(true).alias('h', 'help').argv;
231+ //# sourceMappingURL=cli.js.map
232+ \ No newline at end of file
146233diff --git a/lib/commonjs/index.js b/lib/commonjs/index.js
147234index a72380ed2ace01b19b028bb8e6f9d29dc3affbcd..030475cff75349b2b7f820cf8a3556fb1d7bea45 100644
148235--- a/lib/commonjs/index.js
0 commit comments