taist 0.2.15 → 0.2.17

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.
@@ -97,9 +97,16 @@ export class TraceCollector extends EventEmitter {
97
97
  if (lifecycleDebug) {
98
98
  console.log('[LIFECYCLE collector] Socket closed, remaining buffer:', buffer.length, 'chars');
99
99
  }
100
- // Process any remaining data in buffer
100
+ // Process any remaining complete lines in buffer
101
+ // The buffer may contain multiple NDJSON messages if data arrived in chunks
102
+ // that didn't end with newlines
101
103
  if (buffer.trim()) {
102
- this._processMessage(buffer);
104
+ const lines = buffer.split('\n');
105
+ for (const line of lines) {
106
+ if (line.trim()) {
107
+ this._processMessage(line);
108
+ }
109
+ }
103
110
  }
104
111
  this.connections.delete(socket);
105
112
  });
@@ -37,6 +37,7 @@ export class TraceReporter extends EventEmitter {
37
37
  this.shuttingDown = false; // Prevents race between shutdown signal and SIGTERM
38
38
  this.pendingWrites = 0; // Track writes that have been initiated but callback hasn't fired
39
39
  this.pendingWriteResolvers = []; // Resolvers to call when pendingWrites reaches 0
40
+ this._flushScheduled = false; // For micro-batching in flushImmediate mode
40
41
 
41
42
  logger.debug("[reporter] Created with socketPath:", this.socketPath, "flushImmediate:", this.flushImmediate);
42
43
 
@@ -47,9 +48,9 @@ export class TraceReporter extends EventEmitter {
47
48
  }
48
49
 
49
50
  _setupExitHandlers() {
50
- const cleanup = (signal) => {
51
- logger.debug("[reporter] Cleanup triggered by:", signal, "buffer size:", this.buffer.length, "shuttingDown:", this.shuttingDown);
52
- // If shutdown signal handler is already handling graceful shutdown, don't interfere
51
+ // Synchronous cleanup for exit events
52
+ const syncCleanup = (signal) => {
53
+ logger.debug("[reporter] Sync cleanup triggered by:", signal, "buffer size:", this.buffer.length);
53
54
  if (this.shuttingDown) {
54
55
  logger.debug("[reporter] Shutdown already in progress, skipping cleanup");
55
56
  return;
@@ -60,10 +61,75 @@ export class TraceReporter extends EventEmitter {
60
61
  }
61
62
  };
62
63
 
63
- process.on("beforeExit", () => cleanup("beforeExit"));
64
- process.on("exit", () => cleanup("exit"));
65
- process.on("SIGINT", () => cleanup("SIGINT"));
66
- process.on("SIGTERM", () => cleanup("SIGTERM"));
64
+ // Async cleanup for signals - waits for pending writes then exits
65
+ const signalCleanup = async (signal) => {
66
+ const lifecycleDebug = process.env.TAIST_TRACE_LIFECYCLE === 'true';
67
+ if (lifecycleDebug) {
68
+ console.log(`[LIFECYCLE reporter] Signal ${signal} received, pendingWrites:`, this.pendingWrites);
69
+ }
70
+
71
+ if (this.shuttingDown) {
72
+ logger.debug("[reporter] Shutdown already in progress, skipping cleanup");
73
+ return;
74
+ }
75
+ this.shuttingDown = true;
76
+
77
+ // Wait for pending writes to complete (up to 2 seconds)
78
+ await this._waitForPendingWrites(2000);
79
+
80
+ if (lifecycleDebug) {
81
+ console.log(`[LIFECYCLE reporter] After waiting, pendingWrites:`, this.pendingWrites, 'buffer:', this.buffer.length);
82
+ }
83
+
84
+ // Final flush and close
85
+ if (!this.closed) {
86
+ this.flushSync();
87
+ this.close();
88
+ }
89
+
90
+ // Exit the process after cleanup
91
+ process.exit(0);
92
+ };
93
+
94
+ process.on("beforeExit", () => syncCleanup("beforeExit"));
95
+ process.on("exit", () => syncCleanup("exit"));
96
+ process.on("SIGINT", () => signalCleanup("SIGINT"));
97
+ process.on("SIGTERM", () => signalCleanup("SIGTERM"));
98
+ }
99
+
100
+ /**
101
+ * Wait for pending socket writes to complete.
102
+ * @param {number} timeout - Maximum time to wait in milliseconds
103
+ */
104
+ async _waitForPendingWrites(timeout = 2000) {
105
+ if (this.pendingWrites === 0) return;
106
+
107
+ const lifecycleDebug = process.env.TAIST_TRACE_LIFECYCLE === 'true';
108
+ const startTime = Date.now();
109
+
110
+ return new Promise((resolve) => {
111
+ const checkPending = () => {
112
+ if (this.pendingWrites === 0) {
113
+ if (lifecycleDebug) {
114
+ console.log('[LIFECYCLE reporter] All pending writes completed');
115
+ }
116
+ resolve();
117
+ return;
118
+ }
119
+
120
+ if ((Date.now() - startTime) >= timeout) {
121
+ if (lifecycleDebug) {
122
+ console.log('[LIFECYCLE reporter] Timeout waiting for pending writes, still have:', this.pendingWrites);
123
+ }
124
+ resolve();
125
+ return;
126
+ }
127
+
128
+ // Poll at 10ms intervals
129
+ setTimeout(checkPending, 10);
130
+ };
131
+ checkPending();
132
+ });
67
133
  }
68
134
 
69
135
  async connect() {
@@ -313,10 +379,18 @@ export class TraceReporter extends EventEmitter {
313
379
  });
314
380
  }
315
381
 
316
- // Flush immediately if configured (for spawned processes with unpredictable exit)
317
- // Otherwise auto-flush when batch size reached
382
+ // Flush strategy:
383
+ // - flushImmediate=true: Use micro-batching (setImmediate) to batch traces from the same tick
384
+ // This significantly reduces socket writes (e.g., 2762 individual writes → ~100 batched writes)
385
+ // - flushImmediate=false: Traditional batching by size (flushes when buffer reaches batchSize)
318
386
  if (this.flushImmediate) {
319
- this.flush().catch(() => {});
387
+ if (!this._flushScheduled) {
388
+ this._flushScheduled = true;
389
+ setImmediate(() => {
390
+ this._flushScheduled = false;
391
+ this.flush().catch(() => {});
392
+ });
393
+ }
320
394
  } else if (this.buffer.length >= this.batchSize) {
321
395
  this.flush().catch(() => {});
322
396
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taist",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "Token-Optimized Testing Framework for AI-Assisted Development",
5
5
  "main": "index.js",
6
6
  "type": "module",