fix(watcher.node): Improve handling of temporary files from atomic editor writes in Node watcher
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-12-11 - 6.2.2 - fix(watcher.node)
|
||||||
Defer events during initial scan, track full event sequences, and harden watcher shutdown
|
Defer events during initial scan, track full event sequences, and harden watcher shutdown
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartwatch',
|
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.'
|
description: 'A cross-runtime file watcher with glob pattern support for Node.js, Deno, and Bun.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,6 +227,35 @@ export class NodeWatcher implements IWatcher {
|
|||||||
return false;
|
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 {
|
get isWatching(): boolean {
|
||||||
return this._isWatching;
|
return this._isWatching;
|
||||||
}
|
}
|
||||||
@@ -410,14 +439,26 @@ export class NodeWatcher implements IWatcher {
|
|||||||
|
|
||||||
const fullPath = path.join(basePath, filename);
|
const fullPath = path.join(basePath, filename);
|
||||||
|
|
||||||
// Skip temporary files - but ONLY pure temp files, not the target of atomic writes
|
// Handle temporary files from atomic writes (Claude Code, editors, etc.)
|
||||||
// Atomic writes: editor writes to file.tmp.xxx then renames to file
|
// Pattern: 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
|
// Problem: fs.watch on Linux may ONLY emit event for the temp file, not the target!
|
||||||
// and haven't been renamed to the real file yet
|
// Solution: When we see a temp file event, also check the corresponding real file
|
||||||
if (this.isTemporaryFile(fullPath)) {
|
if (this.isTemporaryFile(fullPath)) {
|
||||||
// For temp files, we still want to track if they get renamed TO a real file
|
console.log(`[smartwatch] Detected temp file event: ${filename}`);
|
||||||
// The 'rename' event fires for both source and target, so we'll catch the real file
|
|
||||||
console.log(`[smartwatch] Skipping 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user