feat: update auto render
This commit is contained in:
178
internal/template/hot_reload.go
Normal file
178
internal/template/hot_reload.go
Normal file
@@ -0,0 +1,178 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user