fix(watcher.node): Improve handling of temporary files from atomic editor writes in Node watcher

This commit is contained in:
2025-12-11 09:07:57 +00:00
parent ef2388b16f
commit 90275a0f1c
3 changed files with 57 additions and 8 deletions

View File

@@ -1,5 +1,13 @@
# Changelog
## 2025-12-11 - 6.2.3 - fix(watcher.node)
Improve handling of temporary files from atomic editor writes in Node watcher
- Detect temporary files produced by atomic editor saves and attempt to map them to the real target file instead of silently skipping the event
- Add getTempFileTarget() to extract the real file path from temp filenames (supports patterns like file.ts.tmp.PID.TIMESTAMP and generic .tmp.*)
- When a temp-file event is seen, queue a corresponding event for the resolved real file after a short delay (50ms) to allow rename/replace to complete
- Add logging around temp file detection and real-file checks to aid debugging
## 2025-12-11 - 6.2.2 - fix(watcher.node)
Defer events during initial scan, track full event sequences, and harden watcher shutdown

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartwatch',
version: '6.2.2',
version: '6.2.3',
description: 'A cross-runtime file watcher with glob pattern support for Node.js, Deno, and Bun.'
}

View File

@@ -227,6 +227,35 @@ export class NodeWatcher implements IWatcher {
return false;
}
/**
* Extract the real file path from a temporary file path
* Used to detect atomic writes where only the temp file event is emitted
*
* Patterns:
* - Claude Code: file.ts.tmp.PID.TIMESTAMP -> file.ts
* - Vim swap: .file.ts.swp -> file.ts (but we don't handle this case)
*/
private getTempFileTarget(tempFilePath: string): string | null {
const basename = path.basename(tempFilePath);
// Claude Code pattern: file.ts.tmp.PID.TIMESTAMP
// Match: anything.tmp.digits.digits
const claudeMatch = basename.match(/^(.+)\.tmp\.\d+\.\d+$/);
if (claudeMatch) {
const realBasename = claudeMatch[1];
return path.join(path.dirname(tempFilePath), realBasename);
}
// Generic .tmp. pattern: file.ts.tmp.something -> file.ts
const tmpMatch = basename.match(/^(.+)\.tmp\.[^.]+$/);
if (tmpMatch) {
const realBasename = tmpMatch[1];
return path.join(path.dirname(tempFilePath), realBasename);
}
return null;
}
get isWatching(): boolean {
return this._isWatching;
}
@@ -410,14 +439,26 @@ export class NodeWatcher implements IWatcher {
const fullPath = path.join(basePath, filename);
// Skip temporary files - but ONLY pure temp files, not the target of atomic writes
// Atomic writes: editor writes to file.tmp.xxx then renames to file
// We need to detect the final file, so only skip files that ARE temp files
// and haven't been renamed to the real file yet
// Handle temporary files from atomic writes (Claude Code, editors, etc.)
// Pattern: editor writes to file.tmp.xxx then renames to file
// Problem: fs.watch on Linux may ONLY emit event for the temp file, not the target!
// Solution: When we see a temp file event, also check the corresponding real file
if (this.isTemporaryFile(fullPath)) {
// For temp files, we still want to track if they get renamed TO a real file
// The 'rename' event fires for both source and target, so we'll catch the real file
console.log(`[smartwatch] Skipping temp file event: ${filename}`);
console.log(`[smartwatch] Detected temp file event: ${filename}`);
// Extract the real file path from the temp file path
// Pattern: file.ts.tmp.PID.TIMESTAMP -> file.ts
const realFilePath = this.getTempFileTarget(fullPath);
if (realFilePath) {
console.log(`[smartwatch] Checking corresponding real file: ${realFilePath}`);
// Queue an event for the REAL file - this is the actual file that changed
// Use a short delay to let the rename complete
setTimeout(() => {
if (this._isWatching) {
this.handleFsEvent(basePath, path.relative(basePath, realFilePath), 'change');
}
}, 50);
}
return;
}