taist 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -157,34 +157,71 @@ export class TraceCollector extends EventEmitter {
157
157
  this.traceIds.clear();
158
158
  }
159
159
 
160
- async stop() {
160
+ /**
161
+ * Stop the collector gracefully.
162
+ * Sends shutdown signal to workers and waits for them to flush before closing.
163
+ * @param {number} timeout - Max time to wait for workers to disconnect (default: 2000ms)
164
+ */
165
+ async stop(timeout = 2000) {
161
166
  if (!this.started) {
162
167
  return;
163
168
  }
164
169
 
170
+ // Send shutdown signal to all connected workers
171
+ const shutdownMessage = JSON.stringify({ type: 'shutdown' }) + '\n';
172
+ for (const socket of this.connections) {
173
+ try {
174
+ socket.write(shutdownMessage);
175
+ } catch {
176
+ // Socket may already be closed
177
+ }
178
+ }
179
+
180
+ // Wait for connections to close gracefully, or timeout
181
+ const startTime = Date.now();
182
+ while (this.connections.size > 0 && (Date.now() - startTime) < timeout) {
183
+ await new Promise(resolve => setTimeout(resolve, 50));
184
+ }
185
+
165
186
  return new Promise((resolve) => {
166
- // Close all active connections
187
+ // Gracefully close any remaining connections (allows pending data to be read)
167
188
  for (const socket of this.connections) {
168
- socket.destroy();
189
+ try {
190
+ socket.end();
191
+ } catch {
192
+ try { socket.destroy(); } catch { /* ignore */ }
193
+ }
169
194
  }
170
- this.connections.clear();
171
-
172
- // Close server
173
- this.server.close(() => {
174
- this.started = false;
175
195
 
176
- // Clean up socket file
177
- if (process.platform !== "win32") {
196
+ // Give a moment for final data to arrive before destroying
197
+ setTimeout(() => {
198
+ // Force destroy any sockets that didn't close gracefully
199
+ for (const socket of this.connections) {
178
200
  try {
179
- fs.unlinkSync(this.socketPath);
201
+ socket.destroy();
180
202
  } catch {
181
203
  // Ignore
182
204
  }
183
205
  }
206
+ this.connections.clear();
207
+
208
+ // Close server
209
+ this.server.close(() => {
210
+ this.started = false;
211
+
212
+ // Clean up socket file
213
+ if (process.platform !== "win32") {
214
+ try {
215
+ fs.unlinkSync(this.socketPath);
216
+ } catch {
217
+ // Ignore
218
+ }
219
+ }
184
220
 
185
- this.emit("stopped");
186
- resolve();
187
- });
221
+ this.emit("stopped");
222
+ resolve();
223
+ });
224
+ }, 100); // 100ms grace period for data to be read
188
225
  });
189
226
  }
190
227
 
@@ -79,6 +79,28 @@ export class TraceReporter extends EventEmitter {
79
79
  // Don't keep the process alive just for tracing
80
80
  this.socket.unref();
81
81
 
82
+ // Handle incoming messages from collector (e.g., shutdown signal)
83
+ let dataBuffer = '';
84
+ this.socket.on("data", (chunk) => {
85
+ dataBuffer += chunk.toString();
86
+ const lines = dataBuffer.split('\n');
87
+ dataBuffer = lines.pop(); // Keep incomplete line
88
+
89
+ for (const line of lines) {
90
+ if (line.trim()) {
91
+ try {
92
+ const message = JSON.parse(line);
93
+ if (message.type === 'shutdown') {
94
+ logger.debug("[reporter] Received shutdown signal, flushing...");
95
+ this._handleShutdown();
96
+ }
97
+ } catch {
98
+ // Ignore parse errors
99
+ }
100
+ }
101
+ }
102
+ });
103
+
82
104
  this.socket.on("error", (err) => {
83
105
  this.connected = false;
84
106
  this.connecting = false;
@@ -128,6 +150,85 @@ export class TraceReporter extends EventEmitter {
128
150
  }
129
151
  }
130
152
 
153
+ /**
154
+ * Handle shutdown signal from collector.
155
+ * Flushes buffer and waits for data to be sent before closing.
156
+ */
157
+ _handleShutdown() {
158
+ if (this.closed) return;
159
+
160
+ this._stopFlushTimer();
161
+
162
+ // Flush and wait for data to be sent
163
+ if (this.buffer.length > 0 && this.socket && this.connected) {
164
+ const traces = this.buffer.splice(0, this.buffer.length);
165
+ const message = JSON.stringify({
166
+ type: "batch",
167
+ workerId: this.workerId,
168
+ data: traces,
169
+ });
170
+
171
+ logger.debug("[reporter] Shutdown flushing", traces.length, "traces");
172
+
173
+ try {
174
+ // Write data
175
+ const flushed = this.socket.write(message + "\n");
176
+
177
+ if (flushed) {
178
+ // Data was written to kernel buffer, use setImmediate to allow
179
+ // the event loop to process and send the data before closing
180
+ setImmediate(() => {
181
+ this._gracefulClose();
182
+ });
183
+ } else {
184
+ // Buffer is full, wait for drain
185
+ this.socket.once('drain', () => {
186
+ logger.debug("[reporter] Drained, closing");
187
+ this._gracefulClose();
188
+ });
189
+ }
190
+ } catch (err) {
191
+ logger.debug("[reporter] Write error:", err.message);
192
+ this.close();
193
+ }
194
+ } else {
195
+ this._gracefulClose();
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Close connection gracefully, allowing pending data to be sent.
201
+ */
202
+ _gracefulClose() {
203
+ if (!this.socket) {
204
+ this.close();
205
+ return;
206
+ }
207
+
208
+ // Use socket.end() for graceful TCP close (sends FIN, allows pending data)
209
+ // Add a small delay to allow the collector to read the data
210
+ this.socket.once('close', () => {
211
+ logger.debug("[reporter] Socket closed gracefully");
212
+ this.closed = true;
213
+ this.connected = false;
214
+ this.socket = null;
215
+ });
216
+
217
+ // Set a timeout in case close doesn't happen
218
+ const closeTimeout = setTimeout(() => {
219
+ logger.debug("[reporter] Close timeout, forcing");
220
+ this.close();
221
+ }, 500);
222
+ closeTimeout.unref();
223
+
224
+ try {
225
+ this.socket.end();
226
+ } catch {
227
+ clearTimeout(closeTimeout);
228
+ this.close();
229
+ }
230
+ }
231
+
131
232
  /**
132
233
  * Report a single trace event
133
234
  */
@@ -148,16 +148,15 @@ export class TaistReporter {
148
148
  await this.collectorReady;
149
149
  }
150
150
 
151
- // Give a small delay for any final traces to arrive
152
- await new Promise(resolve => setTimeout(resolve, 100));
151
+ // Stop the collector gracefully - this sends shutdown signal to workers
152
+ // and waits for them to flush their traces before closing (up to 2s timeout)
153
+ await this.collector.stop();
153
154
 
154
- // Get collected traces
155
+ // Get collected traces (after workers have flushed)
155
156
  if (this.options.showTrace) {
156
157
  this.results.trace = this.collector.getTraces();
157
158
  }
158
159
 
159
- // Stop the collector
160
- await this.collector.stop();
161
160
  this.collector = null;
162
161
  }
163
162
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taist",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Token-Optimized Testing Framework for AI-Assisted Development",
5
5
  "main": "index.js",
6
6
  "type": "module",