Pad/internal/io/pool/task.go
Greg Pomerantz d73cb0be2c Integrate real filesystem with interface abstraction
- Define pool.FileSystem interface in internal/io/pool/filesystem.go
- Move DirEntry interface to internal/io/pool/types/types.go to break
  the pool <-> mock import cycle
- Implement real.FileSystem with atomic writes (temp file + os.Rename)
- Update mock.FileSystem to satisfy pool.FileSystem interface
- Update worker pool tasks to accept pool.FileSystem interface
- Update main.go to use RealFileSystem by default, with -root flag
- Update all browser/editor/test references to types.DirEntry
2026-06-04 16:19:38 -04:00

495 lines
13 KiB
Go

// Package pool provides the IO worker pool for the Pad editor.
// It follows the architecture's single-owner pattern: the logic goroutine
// owns all state, workers perform IO and post results, never touching state.
package pool
import (
"context"
"fmt"
"path/filepath"
"sync/atomic"
"time"
)
// taskCounter generates unique task IDs.
var taskCounter atomic.Int64
// Priority determines which channel a task is dispatched to.
type Priority int
const (
HighPriority Priority = iota // UI-critical, blocks user progress
LowPriority // Background work, can be delayed
)
func (p Priority) String() string {
switch p {
case HighPriority:
return "high"
case LowPriority:
return "low"
default:
return fmt.Sprintf("unknown(%d)", p)
}
}
// TaskType identifies the kind of work for result routing.
type TaskType string
const (
// Browser tasks
TypeReadDir TaskType = "read_dir"
TypeBuildIndex TaskType = "build_index"
TypeLoadIndex TaskType = "load_index"
TypeLoadPages TaskType = "load_pages"
TypeStatDir TaskType = "stat_dir"
// File tasks
TypeReadFile TaskType = "read_file"
TypeWriteFile TaskType = "write_file"
TypeStatFile TaskType = "stat_file"
// Cache tasks
TypeWriteCache TaskType = "write_cache"
TypeReadCache TaskType = "read_cache"
TypeInvalidate TaskType = "invalidate_cache"
// State persistence
TypeSaveState TaskType = "save_state"
TypeSaveUndo TaskType = "save_undo"
)
// Task represents a unit of work to be executed by a worker.
// Tasks are immutable after creation.
type Task interface {
// Execute performs the task and returns a result.
Execute() Result
// Priority returns the task priority level.
Priority() Priority
// TaskID returns a unique identifier for this task.
TaskID() string
// TaskType returns the type of task (for result routing).
TaskType() TaskType
// DirPath returns the directory this task operates on, if any.
DirPath() string
// Context returns the context for this task, used for cancellation.
Context() context.Context
// Cancel cancels this task if it's still running.
Cancel()
// Timeout returns the timeout for this task. Zero means no timeout.
Timeout() time.Duration
}
// --- Browser Tasks ---
// ReadDirTask reads directory entries from the filesystem.
type ReadDirTask struct {
taskID string
Dir string
FS FileSystem
}
func NewReadDirTask(dir string, fs FileSystem) *ReadDirTask {
return &ReadDirTask{
taskID: fmt.Sprintf("read_dir_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir,
FS: fs,
}
}
func (t *ReadDirTask) Execute() Result {
entries, err := t.FS.ReadDir(t.Dir)
if err != nil {
return Result{
TaskID: t.taskID,
TaskType: TypeReadDir,
Success: false,
Error: err,
}
}
return Result{
TaskID: t.taskID,
TaskType: TypeReadDir,
Success: true,
Data: entries,
}
}
func (t *ReadDirTask) Priority() Priority { return HighPriority }
func (t *ReadDirTask) TaskID() string { return t.taskID }
func (t *ReadDirTask) TaskType() TaskType { return TypeReadDir }
func (t *ReadDirTask) DirPath() string { return t.Dir }
func (t *ReadDirTask) Context() context.Context { return context.Background() }
func (t *ReadDirTask) Cancel() {}
func (t *ReadDirTask) Timeout() time.Duration { return 5 * time.Second }
// BuildIndexTask builds a directory index and writes it to cache.
type BuildIndexTask struct {
taskID string
Dir string
FS FileSystem
}
func NewBuildIndexTask(dir string, fs FileSystem) *BuildIndexTask {
return &BuildIndexTask{
taskID: fmt.Sprintf("build_index_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir,
FS: fs,
}
}
func (t *BuildIndexTask) Execute() Result {
// In production, this would read the directory, sort entries,
// compute letter offsets, and write to cache.
// For now, we just signal success with the directory info.
entries, err := t.FS.ReadDir(t.Dir)
if err != nil {
return Result{
TaskID: t.taskID,
TaskType: TypeBuildIndex,
Success: false,
Error: err,
}
}
return Result{
TaskID: t.taskID,
TaskType: TypeBuildIndex,
Success: true,
Data: entries,
}
}
func (t *BuildIndexTask) Priority() Priority { return HighPriority }
func (t *BuildIndexTask) TaskID() string { return t.taskID }
func (t *BuildIndexTask) TaskType() TaskType { return TypeBuildIndex }
func (t *BuildIndexTask) DirPath() string { return t.Dir }
func (t *BuildIndexTask) Context() context.Context { return context.Background() }
func (t *BuildIndexTask) Cancel() {}
func (t *BuildIndexTask) Timeout() time.Duration { return 5 * time.Second }
// LoadIndexTask loads a cached directory index.
type LoadIndexTask struct {
taskID string
Dir string
FS FileSystem
}
func NewLoadIndexTask(dir string, fs FileSystem) *LoadIndexTask {
return &LoadIndexTask{
taskID: fmt.Sprintf("load_index_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir,
FS: fs,
}
}
func (t *LoadIndexTask) Execute() Result {
// In production, this would read the index from cache.
// For now, we just signal success.
return Result{
TaskID: t.taskID,
TaskType: TypeLoadIndex,
Success: true,
Data: nil,
}
}
func (t *LoadIndexTask) Priority() Priority { return HighPriority }
func (t *LoadIndexTask) TaskID() string { return t.taskID }
func (t *LoadIndexTask) TaskType() TaskType { return TypeLoadIndex }
func (t *LoadIndexTask) DirPath() string { return t.Dir }
func (t *LoadIndexTask) Context() context.Context { return context.Background() }
func (t *LoadIndexTask) Cancel() {}
func (t *LoadIndexTask) Timeout() time.Duration { return 5 * time.Second }
// LoadPagesTask loads specific pages of directory entries from cache.
type LoadPagesTask struct {
taskID string
Dir string
PageIndices []int
FS FileSystem
}
func NewLoadPagesTask(dir string, pageIndices []int, fs FileSystem) *LoadPagesTask {
return &LoadPagesTask{
taskID: fmt.Sprintf("load_pages_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir,
PageIndices: pageIndices,
FS: fs,
}
}
func (t *LoadPagesTask) Execute() Result {
// In production, this would read pages from cache.
return Result{
TaskID: t.taskID,
TaskType: TypeLoadPages,
Success: true,
Data: t.PageIndices,
}
}
func (t *LoadPagesTask) Priority() Priority { return HighPriority }
func (t *LoadPagesTask) TaskID() string { return t.taskID }
func (t *LoadPagesTask) TaskType() TaskType { return TypeLoadPages }
func (t *LoadPagesTask) DirPath() string { return t.Dir }
func (t *LoadPagesTask) Context() context.Context { return context.Background() }
func (t *LoadPagesTask) Cancel() {}
func (t *LoadPagesTask) Timeout() time.Duration { return 5 * time.Second }
// StatDirTask gets directory metadata.
type StatDirTask struct {
taskID string
Dir string
FS FileSystem
}
func NewStatDirTask(dir string, fs FileSystem) *StatDirTask {
return &StatDirTask{
taskID: fmt.Sprintf("stat_dir_%s_%d", filepath.Base(dir), taskCounter.Add(1)),
Dir: dir,
FS: fs,
}
}
func (t *StatDirTask) Execute() Result {
exists := t.FS.DirExists(t.Dir)
return Result{
TaskID: t.taskID,
TaskType: TypeStatDir,
Success: true,
Data: map[string]bool{"exists": exists},
}
}
func (t *StatDirTask) Priority() Priority { return LowPriority }
func (t *StatDirTask) TaskID() string { return t.taskID }
func (t *StatDirTask) TaskType() TaskType { return TypeStatDir }
func (t *StatDirTask) DirPath() string { return t.Dir }
func (t *StatDirTask) Context() context.Context { return context.Background() }
func (t *StatDirTask) Cancel() {}
func (t *StatDirTask) Timeout() time.Duration { return 30 * time.Second }
// --- File Tasks ---
// ReadFileTask reads file content.
type ReadFileTask struct {
taskID string
Path string
FS FileSystem
}
func NewReadFileTask(path string, fs FileSystem) *ReadFileTask {
return &ReadFileTask{
taskID: fmt.Sprintf("read_file_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path,
FS: fs,
}
}
func (t *ReadFileTask) Execute() Result {
content, err := t.FS.ReadFile(t.Path)
if err != nil {
return Result{
TaskID: t.taskID,
TaskType: TypeReadFile,
Success: false,
Error: err,
}
}
return Result{
TaskID: t.taskID,
TaskType: TypeReadFile,
Success: true,
Data: content,
}
}
func (t *ReadFileTask) Priority() Priority { return HighPriority }
func (t *ReadFileTask) TaskID() string { return t.taskID }
func (t *ReadFileTask) TaskType() TaskType { return TypeReadFile }
func (t *ReadFileTask) DirPath() string { return filepath.Dir(t.Path) }
func (t *ReadFileTask) Context() context.Context { return context.Background() }
func (t *ReadFileTask) Cancel() {}
func (t *ReadFileTask) Timeout() time.Duration { return 5 * time.Second }
// WriteFileTask writes file content (auto-save).
type WriteFileTask struct {
taskID string
Path string
FS FileSystem
Data []byte
}
func NewWriteFileTask(path string, data []byte, fs FileSystem) *WriteFileTask {
return &WriteFileTask{
taskID: fmt.Sprintf("write_file_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path,
FS: fs,
Data: data,
}
}
func (t *WriteFileTask) Execute() Result {
err := t.FS.WriteFile(t.Path, t.Data)
if err != nil {
return Result{
TaskID: t.taskID,
TaskType: TypeWriteFile,
Success: false,
Error: err,
FilePath: t.Path, // Set FilePath
}
}
return Result{
TaskID: t.taskID,
TaskType: TypeWriteFile,
Success: true,
FilePath: t.Path, // Set FilePath
}
}
func (t *WriteFileTask) Priority() Priority { return LowPriority }
func (t *WriteFileTask) TaskID() string { return t.taskID }
func (t *WriteFileTask) TaskType() TaskType { return TypeWriteFile }
func (t *WriteFileTask) DirPath() string { return filepath.Dir(t.Path) }
func (t *WriteFileTask) Context() context.Context { return context.Background() }
func (t *WriteFileTask) Cancel() {}
func (t *WriteFileTask) Timeout() time.Duration { return 30 * time.Second }
// --- Cache Tasks ---
// WriteCacheTask writes cache data.
type WriteCacheTask struct {
taskID string
Path string
FS FileSystem
Data []byte
}
func NewWriteCacheTask(path string, data []byte, fs FileSystem) *WriteCacheTask {
return &WriteCacheTask{
taskID: fmt.Sprintf("write_cache_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path,
FS: fs,
Data: data,
}
}
func (t *WriteCacheTask) Execute() Result {
err := t.FS.WriteFile(t.Path, t.Data)
if err != nil {
return Result{
TaskID: t.taskID,
TaskType: TypeWriteCache,
Success: false,
Error: err,
}
}
return Result{
TaskID: t.taskID,
TaskType: TypeWriteCache,
Success: true,
}
}
func (t *WriteCacheTask) Priority() Priority { return LowPriority }
func (t *WriteCacheTask) TaskID() string { return t.taskID }
func (t *WriteCacheTask) TaskType() TaskType { return TypeWriteCache }
func (t *WriteCacheTask) DirPath() string { return filepath.Dir(t.Path) }
func (t *WriteCacheTask) Context() context.Context { return context.Background() }
func (t *WriteCacheTask) Cancel() {}
func (t *WriteCacheTask) Timeout() time.Duration { return 30 * time.Second }
// --- State Persistence Tasks ---
// SaveStateTask persists application state.
type SaveStateTask struct {
taskID string
Path string
FS FileSystem
Data []byte
}
func NewSaveStateTask(path string, data []byte, fs FileSystem) *SaveStateTask {
return &SaveStateTask{
taskID: fmt.Sprintf("save_state_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path,
FS: fs,
Data: data,
}
}
func (t *SaveStateTask) Execute() Result {
err := t.FS.WriteFile(t.Path, t.Data)
if err != nil {
return Result{
TaskID: t.taskID,
TaskType: TypeSaveState,
Success: false,
Error: err,
}
}
return Result{
TaskID: t.taskID,
TaskType: TypeSaveState,
Success: true,
}
}
func (t *SaveStateTask) Priority() Priority { return LowPriority }
func (t *SaveStateTask) TaskID() string { return t.taskID }
func (t *SaveStateTask) TaskType() TaskType { return TypeSaveState }
func (t *SaveStateTask) DirPath() string { return filepath.Dir(t.Path) }
func (t *SaveStateTask) Context() context.Context { return context.Background() }
func (t *SaveStateTask) Cancel() {}
func (t *SaveStateTask) Timeout() time.Duration { return 30 * time.Second }
// SaveUndoTask persists undo stack.
type SaveUndoTask struct {
taskID string
Path string
FS FileSystem
Data []byte
}
func NewSaveUndoTask(path string, data []byte, fs FileSystem) *SaveUndoTask {
return &SaveUndoTask{
taskID: fmt.Sprintf("save_undo_%s_%d", filepath.Base(path), taskCounter.Add(1)),
Path: path,
FS: fs,
Data: data,
}
}
func (t *SaveUndoTask) Execute() Result {
err := t.FS.WriteFile(t.Path, t.Data)
if err != nil {
return Result{
TaskID: t.taskID,
TaskType: TypeSaveUndo,
Success: false,
Error: err,
}
}
return Result{
TaskID: t.taskID,
TaskType: TypeSaveUndo,
Success: true,
}
}
func (t *SaveUndoTask) Priority() Priority { return LowPriority }
func (t *SaveUndoTask) TaskID() string { return t.taskID }
func (t *SaveUndoTask) TaskType() TaskType { return TypeSaveUndo }
func (t *SaveUndoTask) DirPath() string { return filepath.Dir(t.Path) }
func (t *SaveUndoTask) Context() context.Context { return context.Background() }
func (t *SaveUndoTask) Cancel() {}
func (t *SaveUndoTask) Timeout() time.Duration { return 30 * time.Second }