Files
claude-mnemonic/internal/watcher/watcher.go
lukaszraczylo 4f4b4ac70f feat(chunking): add AST-aware code chunking for Go, Python, TypeScript
- [x] Add language-specific chunkers with AST parsing (Go, Python, TypeScript)
- [x] Implement chunking manager to dispatch files to appropriate chunkers
- [x] Integrate code chunks into vector sync for semantic search
- [x] Add tree-sitter dependency for Python/TypeScript parsing
- [x] Reorder struct fields for consistency across codebase
- [x] Rename error variables to follow Go conventions (err → unmarshalErr, etc.)
- [x] Add code chunk metadata to vector documents (language, symbol name, line ranges)
- [x] Update worker service to initialize chunking pipeline with all three languages
2026-01-07 13:19:58 +00:00

188 lines
4.5 KiB
Go

// Package watcher provides file system watching utilities for detecting
// database file/directory deletions and triggering recreation.
package watcher
import (
"context"
"os"
"path/filepath"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/rs/zerolog/log"
)
// Watcher monitors a file or directory for deletion and calls onDelete when removed.
// It watches the parent directory since fsnotify cannot watch non-existent files.
type Watcher struct {
ctx context.Context
onDelete func()
watcher *fsnotify.Watcher
cancel context.CancelFunc
targetPath string
parentPath string
debounce time.Duration
mu sync.Mutex
running bool
}
// New creates a new Watcher for the given target path.
// The onDelete callback is called when the target is deleted.
func New(targetPath string, onDelete func()) (*Watcher, error) {
fsw, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
return &Watcher{
targetPath: targetPath,
parentPath: filepath.Dir(targetPath),
onDelete: onDelete,
watcher: fsw,
ctx: ctx,
cancel: cancel,
debounce: 100 * time.Millisecond,
}, nil
}
// Start begins watching for file deletion events.
func (w *Watcher) Start() error {
w.mu.Lock()
if w.running {
w.mu.Unlock()
return nil
}
w.running = true
w.mu.Unlock()
// Add watch on parent directory
if err := w.addWatch(); err != nil {
log.Warn().Err(err).Str("path", w.parentPath).Msg("Failed to add initial watch")
// Continue anyway - we'll try to re-establish later
}
go w.watchLoop()
return nil
}
// Stop stops the watcher.
func (w *Watcher) Stop() error {
w.mu.Lock()
defer w.mu.Unlock()
if !w.running {
return nil
}
w.running = false
w.cancel()
return w.watcher.Close()
}
// addWatch adds the parent directory to the watch list.
func (w *Watcher) addWatch() error {
// Ensure parent exists
if _, err := os.Stat(w.parentPath); os.IsNotExist(err) {
return err
}
return w.watcher.Add(w.parentPath)
}
// watchLoop is the main event loop.
func (w *Watcher) watchLoop() {
var (
debounceTimer *time.Timer
pendingDelete bool
)
for {
select {
case <-w.ctx.Done():
if debounceTimer != nil {
debounceTimer.Stop()
}
return
case event, ok := <-w.watcher.Events:
if !ok {
return
}
// Check if this event is for our target
eventPath := filepath.Clean(event.Name)
targetPath := filepath.Clean(w.targetPath)
// Handle parent directory deletion (entire data dir removed)
if eventPath == w.parentPath && event.Op&fsnotify.Remove != 0 {
log.Info().Str("path", w.parentPath).Msg("Parent directory deleted")
pendingDelete = true
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceTimer = time.AfterFunc(w.debounce, func() {
w.handleDeletion()
})
continue
}
// Handle target file/directory deletion
if eventPath == targetPath && event.Op&fsnotify.Remove != 0 {
log.Info().Str("path", w.targetPath).Msg("Target deleted")
pendingDelete = true
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceTimer = time.AfterFunc(w.debounce, func() {
w.handleDeletion()
})
continue
}
// Handle parent directory recreation (re-establish watch)
if eventPath == w.parentPath && event.Op&fsnotify.Create != 0 {
log.Info().Str("path", w.parentPath).Msg("Parent directory recreated, re-establishing watch")
_ = w.addWatch()
continue
}
// If target was recreated after pending delete, cancel the callback
if pendingDelete && eventPath == targetPath && event.Op&fsnotify.Create != 0 {
log.Info().Str("path", w.targetPath).Msg("Target recreated, cancelling deletion callback")
pendingDelete = false
if debounceTimer != nil {
debounceTimer.Stop()
}
}
case err, ok := <-w.watcher.Errors:
if !ok {
return
}
log.Error().Err(err).Msg("Watcher error")
}
}
}
// handleDeletion calls the onDelete callback and attempts to re-establish the watch.
func (w *Watcher) handleDeletion() {
log.Info().Str("path", w.targetPath).Msg("Triggering deletion callback")
// Call the callback
if w.onDelete != nil {
w.onDelete()
}
// Try to re-establish watch after a short delay (parent may have been recreated)
go func() {
time.Sleep(500 * time.Millisecond)
if err := w.addWatch(); err != nil {
log.Warn().Err(err).Str("path", w.parentPath).Msg("Failed to re-establish watch after deletion")
} else {
log.Info().Str("path", w.parentPath).Msg("Re-established watch after recreation")
}
}()
}