Files
database_render/internal/template/hot_reload.go
2025-08-07 20:03:53 +08:00

178 lines
4.4 KiB
Go

package template
import (
"log/slog"
"os"
"path/filepath"
"time"
"github.com/fsnotify/fsnotify"
)
// HotReloadWatcher watches template files for changes
type HotReloadWatcher struct {
watcher *fsnotify.Watcher
loader *TemplateLoader
configPath string
logger *slog.Logger
stopChan chan struct{}
}
// NewHotReloadWatcher creates a new hot reload watcher
func NewHotReloadWatcher(loader *TemplateLoader, configPath string) (*HotReloadWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
return &HotReloadWatcher{
watcher: watcher,
loader: loader,
configPath: configPath,
logger: slog.With("component", "hot_reload"),
stopChan: make(chan struct{}),
}, nil
}
// Start starts watching template files for changes
func (hr *HotReloadWatcher) Start() error {
// Watch template directories
directories := []string{
hr.configPath,
filepath.Join(hr.configPath, "custom"),
filepath.Join(hr.configPath, "builtin"),
filepath.Join(hr.configPath, "custom", "_default"),
}
for _, dir := range directories {
if err := hr.addDirectory(dir); err != nil {
hr.logger.Warn("failed to watch directory", "dir", dir, "error", err)
continue
}
}
go hr.watch()
hr.logger.Info("hot reload watcher started", "directories", directories)
return nil
}
// addDirectory adds a directory and its subdirectories to the watcher
func (hr *HotReloadWatcher) addDirectory(dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return hr.watcher.Add(path)
}
return nil
})
}
// watch watches for file changes and triggers reloads
func (hr *HotReloadWatcher) watch() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
debounce := make(map[string]time.Time)
for {
select {
case event := <-hr.watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
// Debounce rapid file changes
lastChange := debounce[event.Name]
if time.Since(lastChange) > 100*time.Millisecond {
debounce[event.Name] = time.Now()
hr.handleFileChange(event.Name)
}
}
case err := <-hr.watcher.Errors:
if err != nil {
hr.logger.Error("watcher error", "error", err)
}
case <-hr.stopChan:
return
case <-ticker.C:
// Cleanup old debounce entries
now := time.Now()
for file, lastChange := range debounce {
if now.Sub(lastChange) > 5*time.Second {
delete(debounce, file)
}
}
}
}
}
// handleFileChange handles a file change event
func (hr *HotReloadWatcher) handleFileChange(filePath string) {
ext := filepath.Ext(filePath)
if ext != ".html" && ext != ".yaml" && ext != ".yml" {
return
}
hr.logger.Info("detected file change", "file", filePath)
// Reload templates
hr.loader.ClearCache()
// Notify clients via WebSocket or SSE
hr.notifyClients()
}
// notifyClients notifies connected clients about template changes
func (hr *HotReloadWatcher) notifyClients() {
// This could be extended to use WebSocket or Server-Sent Events
// For now, just log the change
hr.logger.Info("templates reloaded")
}
// Stop stops the hot reload watcher
func (hr *HotReloadWatcher) Stop() {
close(hr.stopChan)
hr.watcher.Close()
hr.logger.Info("hot reload watcher stopped")
}
// AddTemplateDirectory adds a new template directory to watch
func (hr *HotReloadWatcher) AddTemplateDirectory(path string) error {
return hr.addDirectory(path)
}
// RemoveTemplateDirectory removes a template directory from watch
func (hr *HotReloadWatcher) RemoveTemplateDirectory(path string) error {
return hr.watcher.Remove(path)
}
// GetWatchedDirectories returns currently watched directories
func (hr *HotReloadWatcher) GetWatchedDirectories() []string {
return hr.watcher.WatchList()
}
// IsWatching returns whether the watcher is active
func (hr *HotReloadWatcher) IsWatching() bool {
select {
case <-hr.stopChan:
return false
default:
return true
}
}
// ReloadTemplates triggers a manual template reload
func (hr *HotReloadWatcher) ReloadTemplates() {
hr.loader.ClearCache()
hr.logger.Info("templates manually reloaded")
}
// GetReloadStats returns hot reload statistics
func (hr *HotReloadWatcher) GetReloadStats() map[string]interface{} {
watched := hr.GetWatchedDirectories()
return map[string]interface{}{
"watched_directories": len(watched),
"watched_paths": watched,
"is_active": hr.IsWatching(),
}
}