taist 0.1.5 → 0.1.7

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.
@@ -184,28 +184,44 @@ export class TraceCollector extends EventEmitter {
184
184
  }
185
185
 
186
186
  return new Promise((resolve) => {
187
- // Force close any remaining connections
187
+ // Gracefully close any remaining connections (allows pending data to be read)
188
188
  for (const socket of this.connections) {
189
- socket.destroy();
189
+ try {
190
+ socket.end();
191
+ } catch {
192
+ try { socket.destroy(); } catch { /* ignore */ }
193
+ }
190
194
  }
191
- this.connections.clear();
192
-
193
- // Close server
194
- this.server.close(() => {
195
- this.started = false;
196
195
 
197
- // Clean up socket file
198
- 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) {
199
200
  try {
200
- fs.unlinkSync(this.socketPath);
201
+ socket.destroy();
201
202
  } catch {
202
203
  // Ignore
203
204
  }
204
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
+ }
205
220
 
206
- this.emit("stopped");
207
- resolve();
208
- });
221
+ this.emit("stopped");
222
+ resolve();
223
+ });
224
+ }, 100); // 100ms grace period for data to be read
209
225
  });
210
226
  }
211
227
 
@@ -30,6 +30,7 @@ export class TraceReporter extends EventEmitter {
30
30
  this.flushTimer = null;
31
31
  this.workerId = options.workerId || process.pid;
32
32
  this.closed = false;
33
+ this.shuttingDown = false; // Prevents race between shutdown signal and SIGTERM
33
34
 
34
35
  logger.debug("[reporter] Created with socketPath:", this.socketPath);
35
36
 
@@ -41,7 +42,12 @@ export class TraceReporter extends EventEmitter {
41
42
 
42
43
  _setupExitHandlers() {
43
44
  const cleanup = (signal) => {
44
- logger.debug("[reporter] Cleanup triggered by:", signal, "buffer size:", this.buffer.length);
45
+ logger.debug("[reporter] Cleanup triggered by:", signal, "buffer size:", this.buffer.length, "shuttingDown:", this.shuttingDown);
46
+ // If shutdown signal handler is already handling graceful shutdown, don't interfere
47
+ if (this.shuttingDown) {
48
+ logger.debug("[reporter] Shutdown already in progress, skipping cleanup");
49
+ return;
50
+ }
45
51
  if (!this.closed) {
46
52
  this.flushSync();
47
53
  this.close();
@@ -152,12 +158,16 @@ export class TraceReporter extends EventEmitter {
152
158
 
153
159
  /**
154
160
  * Handle shutdown signal from collector.
155
- * Immediately flushes buffer and closes connection.
161
+ * Flushes buffer and waits for data to be sent before closing.
156
162
  */
157
163
  _handleShutdown() {
158
- if (this.closed) return;
164
+ if (this.closed || this.shuttingDown) return;
159
165
 
160
- // Flush synchronously and close
166
+ // Set flag immediately to prevent exit handlers from interfering
167
+ this.shuttingDown = true;
168
+ this._stopFlushTimer();
169
+
170
+ // Flush and wait for data to be sent
161
171
  if (this.buffer.length > 0 && this.socket && this.connected) {
162
172
  const traces = this.buffer.splice(0, this.buffer.length);
163
173
  const message = JSON.stringify({
@@ -166,15 +176,63 @@ export class TraceReporter extends EventEmitter {
166
176
  data: traces,
167
177
  });
168
178
 
179
+ logger.debug("[reporter] Shutdown flushing", traces.length, "traces");
180
+
169
181
  try {
170
- this.socket.write(message + "\n", () => {
171
- logger.debug("[reporter] Shutdown flush complete, closing");
172
- this.close();
173
- });
174
- } catch {
182
+ // Write data
183
+ const flushed = this.socket.write(message + "\n");
184
+
185
+ if (flushed) {
186
+ // Data was written to kernel buffer, use setImmediate to allow
187
+ // the event loop to process and send the data before closing
188
+ setImmediate(() => {
189
+ this._gracefulClose();
190
+ });
191
+ } else {
192
+ // Buffer is full, wait for drain
193
+ this.socket.once('drain', () => {
194
+ logger.debug("[reporter] Drained, closing");
195
+ this._gracefulClose();
196
+ });
197
+ }
198
+ } catch (err) {
199
+ logger.debug("[reporter] Write error:", err.message);
175
200
  this.close();
176
201
  }
177
202
  } else {
203
+ this._gracefulClose();
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Close connection gracefully, allowing pending data to be sent.
209
+ */
210
+ _gracefulClose() {
211
+ if (!this.socket) {
212
+ this.close();
213
+ return;
214
+ }
215
+
216
+ // Use socket.end() for graceful TCP close (sends FIN, allows pending data)
217
+ // Add a small delay to allow the collector to read the data
218
+ this.socket.once('close', () => {
219
+ logger.debug("[reporter] Socket closed gracefully");
220
+ this.closed = true;
221
+ this.connected = false;
222
+ this.socket = null;
223
+ });
224
+
225
+ // Set a timeout in case close doesn't happen
226
+ const closeTimeout = setTimeout(() => {
227
+ logger.debug("[reporter] Close timeout, forcing");
228
+ this.close();
229
+ }, 500);
230
+ closeTimeout.unref();
231
+
232
+ try {
233
+ this.socket.end();
234
+ } catch {
235
+ clearTimeout(closeTimeout);
178
236
  this.close();
179
237
  }
180
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taist",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Token-Optimized Testing Framework for AI-Assisted Development",
5
5
  "main": "index.js",
6
6
  "type": "module",