package pool import ( "context" "fmt" "os" "path/filepath" "sync/atomic" "time" "pad/internal/io/pool/types" ) // taskIDCounter provides unique IDs for tasks. var taskIDCounter atomic.Int64 // Task defines the interface for a worker task. type Task interface { Execute() Result Priority() Priority TaskType() TaskType TaskID() string DirPath() string Context() context.Context Timeout() time.Duration Cancel() } // Priority defines task priority. type Priority int const ( LowPriority Priority = iota MediumPriority HighPriority ) func (p Priority) String() string { switch p { case LowPriority: return "low" case MediumPriority: return "medium" case HighPriority: return "high" default: return "unknown" } } // TaskType defines the type of task. type TaskType int const ( TypeUnknown TaskType = iota TypeReadFile TypeReadChunk TypeStatFile TypeBuildLineIndex TypeWriteFile // Browser task types TypeReadDir TypeBuildIndex TypeLoadIndex TypeLoadPages TypeStatDir // Cache task types TypeWriteCache TypeReadCache TypeInvalidate // State persistence types TypeSaveState TypeSaveUndo ) func (t TaskType) String() string { switch t { case TypeReadFile: return "read_file" case TypeReadChunk: return "read_chunk" case TypeStatFile: return "stat_file" case TypeBuildLineIndex: return "build_line_index" case TypeWriteFile: return "write_file" case TypeReadDir: return "read_dir" case TypeBuildIndex: return "build_index" case TypeLoadIndex: return "load_index" case TypeLoadPages: return "load_pages" case TypeStatDir: return "stat_dir" case TypeWriteCache: return "write_cache" case TypeReadCache: return "read_cache" case TypeInvalidate: return "invalidate_cache" case TypeSaveState: return "save_state" case TypeSaveUndo: return "save_undo" default: return "unknown" } } // --- Specific Task Implementations --- // ReadChunkTask reads a specific chunk of a file. type ReadChunkTask struct { taskID string Path string ChunkIdx int FS FileSystem ctx context.Context cancel context.CancelFunc } // NewReadChunkTask creates a new ReadChunkTask. func NewReadChunkTask(path string, chunkIdx int, fs FileSystem) *ReadChunkTask { ctx, cancel := context.WithCancel(context.Background()) return &ReadChunkTask{ taskID: fmt.Sprintf("readchunk-%d-%s", chunkIdx, filepath.Base(path)), Path: path, ChunkIdx: chunkIdx, FS: fs, ctx: ctx, cancel: cancel, } } func (t *ReadChunkTask) Execute() Result { // Use ReadFileAt to read only the specific chunk range (not the entire file) chunkSize := 64 * 1024 // Must match the chunk size used in ChunkedBuffer start := t.ChunkIdx * chunkSize chunk, err := t.FS.ReadFileAt(t.Path, start, chunkSize) if err != nil { return Result{TaskID: t.taskID, TaskType: TypeReadChunk, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read chunk %d: %w", t.ChunkIdx, err)} } return Result{TaskID: t.taskID, TaskType: TypeReadChunk, FilePath: t.Path, Success: true, Data: chunk} } func (t *ReadChunkTask) Priority() Priority { return HighPriority } func (t *ReadChunkTask) TaskType() TaskType { return TypeReadChunk } func (t *ReadChunkTask) TaskID() string { return t.taskID } func (t *ReadChunkTask) DirPath() string { return filepath.Dir(t.Path) } func (t *ReadChunkTask) Context() context.Context { return t.ctx } func (t *ReadChunkTask) Timeout() time.Duration { return 5 * time.Second } func (t *ReadChunkTask) Cancel() { if t.cancel != nil { t.cancel() } } // StatFileTask retrieves file metadata (like size and modification time). // The plan indicates this replaces the full-file ReadFileTask for opening. type StatFileTask struct { taskID string Path string FS FileSystem ctx context.Context cancel context.CancelFunc } // NewStatFileTask creates a new StatFileTask. func NewStatFileTask(path string, fs FileSystem) *StatFileTask { ctx, cancel := context.WithCancel(context.Background()) return &StatFileTask{ taskID: fmt.Sprintf("statfile-%s", filepath.Base(path)), Path: path, FS: fs, ctx: ctx, cancel: cancel, } } func (t *StatFileTask) Execute() Result { // In the current plan, this reads the full file content to get size. // A more optimized version for large files would use os.Stat or similar, // which returns size directly without reading content. content, err := t.FS.ReadFile(t.Path) if err != nil { return Result{TaskID: t.taskID, TaskType: TypeStatFile, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read file for stat: %w", err)} } // We also need mtime for invalidation, but FileInfo might not be directly returned as Data. // For now, just returning size. return Result{ TaskID: t.taskID, TaskType: TypeStatFile, FilePath: t.Path, Success: true, Data: &FileStat{Path: t.Path, Size: int64(len(content))}, } } func (t *StatFileTask) Priority() Priority { return MediumPriority } func (t *StatFileTask) TaskType() TaskType { return TypeStatFile } func (t *StatFileTask) TaskID() string { return t.taskID } func (t *StatFileTask) DirPath() string { return filepath.Dir(t.Path) } func (t *StatFileTask) Context() context.Context { return t.ctx } func (t *StatFileTask) Timeout() time.Duration { return 5 * time.Second } func (t *StatFileTask) Cancel() { if t.cancel != nil { t.cancel() } } // FileStat holds file metadata. type FileStat struct { Path string Size int64 // MTime time.Time // Add this if FileSystem.Stat returns it and we need it } // BuildLineIndexTask builds the line index for a file. type BuildLineIndexTask struct { taskID string Path string FS FileSystem ctx context.Context cancel context.CancelFunc } // NewBuildLineIndexTask creates a new BuildLineIndexTask. func NewBuildLineIndexTask(path string, fs FileSystem) *BuildLineIndexTask { ctx, cancel := context.WithCancel(context.Background()) return &BuildLineIndexTask{ taskID: fmt.Sprintf("buildindex-%s", filepath.Base(path)), Path: path, FS: fs, ctx: ctx, cancel: cancel, } } func (t *BuildLineIndexTask) Execute() Result { content, err := t.FS.ReadFile(t.Path) if err != nil { return Result{TaskID: t.taskID, TaskType: TypeBuildLineIndex, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read file for index build: %w", err)} } offsets := []int32{0} // Line 0 starts at byte offset 0 for i := 0; i < len(content); i++ { if content[i] == '\n' { offsets = append(offsets, int32(i+1)) } } lineIndex := types.NewLineIndex(offsets, 0, int64(len(content))) return Result{ TaskID: t.taskID, TaskType: TypeBuildLineIndex, FilePath: t.Path, Success: true, Data: lineIndex, } } func (t *BuildLineIndexTask) Priority() Priority { return LowPriority } func (t *BuildLineIndexTask) TaskType() TaskType { return TypeBuildLineIndex } func (t *BuildLineIndexTask) TaskID() string { return t.taskID } func (t *BuildLineIndexTask) DirPath() string { return filepath.Dir(t.Path) } func (t *BuildLineIndexTask) Context() context.Context { return t.ctx } func (t *BuildLineIndexTask) Timeout() time.Duration { return 30 * time.Second } func (t *BuildLineIndexTask) Cancel() { if t.cancel != nil { t.cancel() } } // BuildIndexTask builds the directory index for the browser. type BuildIndexTask struct { taskID string Dir string FS FileSystem ctx context.Context cancel context.CancelFunc } // NewBuildIndexTask creates a new BuildIndexTask. func NewBuildIndexTask(dir string, fs FileSystem) *BuildIndexTask { ctx, cancel := context.WithCancel(context.Background()) return &BuildIndexTask{ taskID: fmt.Sprintf("buildindex-%s", filepath.Base(dir)), Dir: dir, FS: fs, ctx: ctx, cancel: cancel, } } func (t *BuildIndexTask) Execute() Result { entries, err := t.FS.ReadDir(t.Dir) if err != nil { return Result{TaskID: t.taskID, TaskType: TypeBuildIndex, Success: false, Error: fmt.Errorf("failed to read directory: %w", err)} } return Result{TaskID: t.taskID, TaskType: TypeBuildIndex, Success: true, Data: entries} } func (t *BuildIndexTask) Priority() Priority { return HighPriority } func (t *BuildIndexTask) TaskType() TaskType { return TypeBuildIndex } func (t *BuildIndexTask) TaskID() string { return t.taskID } func (t *BuildIndexTask) DirPath() string { return t.Dir } func (t *BuildIndexTask) Context() context.Context { return t.ctx } func (t *BuildIndexTask) Timeout() time.Duration { return 5 * time.Second } func (t *BuildIndexTask) Cancel() { if t.cancel != nil { t.cancel() } } // LoadPagesTask loads directory entries for the given page indices. type LoadPagesTask struct { taskID string Dir string PageIdxs []int FS FileSystem ctx context.Context cancel context.CancelFunc } // NewLoadPagesTask creates a new LoadPagesTask. func NewLoadPagesTask(dir string, pageIdxs []int, fs FileSystem) *LoadPagesTask { ctx, cancel := context.WithCancel(context.Background()) id := taskIDCounter.Add(1) return &LoadPagesTask{ taskID: fmt.Sprintf("loadpages-%s-%d", filepath.Base(dir), id), Dir: dir, PageIdxs: pageIdxs, FS: fs, ctx: ctx, cancel: cancel, } } func (t *LoadPagesTask) Execute() Result { // In a real implementation, this would read the actual entry data for the pages. // For now, just return the page indices as success — the browser manager // will load the actual data from the SortIndex. return Result{TaskID: t.taskID, TaskType: TypeLoadPages, Success: true, Data: t.PageIdxs} } func (t *LoadPagesTask) Priority() Priority { return HighPriority } func (t *LoadPagesTask) TaskType() TaskType { return TypeLoadPages } func (t *LoadPagesTask) TaskID() string { return t.taskID } func (t *LoadPagesTask) DirPath() string { return t.Dir } func (t *LoadPagesTask) Context() context.Context { return t.ctx } func (t *LoadPagesTask) Timeout() time.Duration { return 5 * time.Second } func (t *LoadPagesTask) Cancel() { if t.cancel != nil { t.cancel() } } // ReadDirTask reads directory entries. type ReadDirTask struct { taskID string Dir string FS FileSystem ctx context.Context cancel context.CancelFunc } // NewReadDirTask creates a new ReadDirTask. func NewReadDirTask(dir string, fs FileSystem) *ReadDirTask { ctx, cancel := context.WithCancel(context.Background()) id := taskIDCounter.Add(1) return &ReadDirTask{ taskID: fmt.Sprintf("readdir-%s-%d", filepath.Base(dir), id), Dir: dir, FS: fs, ctx: ctx, cancel: cancel, } } 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: fmt.Errorf("failed to read directory: %w", err)} } return Result{TaskID: t.taskID, TaskType: TypeReadDir, Success: true, Data: entries} } func (t *ReadDirTask) Priority() Priority { return HighPriority } func (t *ReadDirTask) TaskType() TaskType { return TypeReadDir } func (t *ReadDirTask) TaskID() string { return t.taskID } func (t *ReadDirTask) DirPath() string { return t.Dir } func (t *ReadDirTask) Context() context.Context { return t.ctx } func (t *ReadDirTask) Timeout() time.Duration { return 5 * time.Second } func (t *ReadDirTask) Cancel() { if t.cancel != nil { t.cancel() } } // SaveStateTask persists application state. type SaveStateTask struct { taskID string Path string Content []byte FS FileSystem ctx context.Context cancel context.CancelFunc } // NewSaveStateTask creates a new SaveStateTask. func NewSaveStateTask(path string, content []byte, fs FileSystem) *SaveStateTask { ctx, cancel := context.WithCancel(context.Background()) id := taskIDCounter.Add(1) return &SaveStateTask{ taskID: fmt.Sprintf("savestate-%s-%d", filepath.Base(path), id), Path: path, Content: content, FS: fs, ctx: ctx, cancel: cancel, } } func (t *SaveStateTask) Execute() Result { err := t.FS.WriteFileAtomic(t.Path, t.Content) if err != nil { return Result{TaskID: t.taskID, TaskType: TypeSaveState, Success: false, Error: fmt.Errorf("failed to save state: %w", err)} } return Result{TaskID: t.taskID, TaskType: TypeSaveState, Success: true} } func (t *SaveStateTask) Priority() Priority { return LowPriority } func (t *SaveStateTask) TaskType() TaskType { return TypeSaveState } func (t *SaveStateTask) TaskID() string { return t.taskID } func (t *SaveStateTask) DirPath() string { return filepath.Dir(t.Path) } func (t *SaveStateTask) Context() context.Context { return t.ctx } func (t *SaveStateTask) Timeout() time.Duration { return 5 * time.Second } func (t *SaveStateTask) Cancel() { if t.cancel != nil { t.cancel() } } // SaveUndoTask persists undo stack. type SaveUndoTask struct { taskID string Path string Content []byte FS FileSystem ctx context.Context cancel context.CancelFunc } // NewSaveUndoTask creates a new SaveUndoTask. func NewSaveUndoTask(path string, content []byte, fs FileSystem) *SaveUndoTask { ctx, cancel := context.WithCancel(context.Background()) id := taskIDCounter.Add(1) return &SaveUndoTask{ taskID: fmt.Sprintf("saveundo-%s-%d", filepath.Base(path), id), Path: path, Content: content, FS: fs, ctx: ctx, cancel: cancel, } } func (t *SaveUndoTask) Execute() Result { err := t.FS.WriteFileAtomic(t.Path, t.Content) if err != nil { return Result{TaskID: t.taskID, TaskType: TypeSaveUndo, Success: false, Error: fmt.Errorf("failed to save undo: %w", err)} } return Result{TaskID: t.taskID, TaskType: TypeSaveUndo, Success: true} } func (t *SaveUndoTask) Priority() Priority { return LowPriority } func (t *SaveUndoTask) TaskType() TaskType { return TypeSaveUndo } func (t *SaveUndoTask) TaskID() string { return t.taskID } func (t *SaveUndoTask) DirPath() string { return filepath.Dir(t.Path) } func (t *SaveUndoTask) Context() context.Context { return t.ctx } func (t *SaveUndoTask) Timeout() time.Duration { return 5 * time.Second } func (t *SaveUndoTask) Cancel() { if t.cancel != nil { t.cancel() } } // ReadFileTask reads the full content of a file. // Used as a fallback for small files or initial load before chunking is set up. type ReadFileTask struct { taskID string Path string FS FileSystem ctx context.Context cancel context.CancelFunc } // NewReadFileTask creates a new ReadFileTask. func NewReadFileTask(path string, fs FileSystem) *ReadFileTask { ctx, cancel := context.WithCancel(context.Background()) return &ReadFileTask{ taskID: fmt.Sprintf("readfile-%s", filepath.Base(path)), Path: path, FS: fs, ctx: ctx, cancel: cancel, } } func (t *ReadFileTask) Execute() Result { content, err := t.FS.ReadFile(t.Path) if err != nil { return Result{TaskID: t.taskID, TaskType: TypeReadFile, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read file: %w", err)} } return Result{TaskID: t.taskID, TaskType: TypeReadFile, FilePath: t.Path, Success: true, Data: content} } func (t *ReadFileTask) Priority() Priority { return HighPriority } func (t *ReadFileTask) TaskType() TaskType { return TypeReadFile } func (t *ReadFileTask) TaskID() string { return t.taskID } func (t *ReadFileTask) DirPath() string { return filepath.Dir(t.Path) } func (t *ReadFileTask) Context() context.Context { return t.ctx } func (t *ReadFileTask) Timeout() time.Duration { return 5 * time.Second } func (t *ReadFileTask) Cancel() { if t.cancel != nil { t.cancel() } } // WriteFileTask writes content to a file. type WriteFileTask struct { taskID string Path string Content []byte FS FileSystem ctx context.Context cancel context.CancelFunc } // NewWriteFileTask creates a new WriteFileTask. func NewWriteFileTask(path string, content []byte, fs FileSystem) *WriteFileTask { ctx, cancel := context.WithCancel(context.Background()) return &WriteFileTask{ taskID: fmt.Sprintf("writefile-%s", filepath.Base(path)), Path: path, Content: content, FS: fs, ctx: ctx, cancel: cancel, } } func (t *WriteFileTask) Execute() Result { err := t.FS.WriteFile(t.Path, t.Content) if err != nil { return Result{TaskID: t.taskID, TaskType: TypeWriteFile, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to write file: %w", err)} } return Result{TaskID: t.taskID, TaskType: TypeWriteFile, FilePath: t.Path, Success: true} } func (t *WriteFileTask) Priority() Priority { return LowPriority } func (t *WriteFileTask) TaskType() TaskType { return TypeWriteFile } func (t *WriteFileTask) TaskID() string { return t.taskID } func (t *WriteFileTask) DirPath() string { return filepath.Dir(t.Path) } func (t *WriteFileTask) Context() context.Context { return t.ctx } func (t *WriteFileTask) Timeout() time.Duration { return 5 * time.Second } func (t *WriteFileTask) Cancel() { if t.cancel != nil { t.cancel() } } // --- Mock File System (for testing/development) --- // MockFS implements the pool.FileSystem interface for testing. type MockFS struct { files map[string][]byte } func NewMockFS() *MockFS { return &MockFS{ files: make(map[string][]byte), } } func (m *MockFS) ReadFile(path string) ([]byte, error) { content, ok := m.files[path] if !ok { return nil, fmt.Errorf("file not found: %s", path) } return content, nil } func (m *MockFS) ReadFileAt(path string, offset, size int) ([]byte, error) { content, ok := m.files[path] if !ok { return nil, fmt.Errorf("file not found: %s", path) } if offset >= len(content) { return []byte{}, nil } end := offset + size if end > len(content) { end = len(content) } result := make([]byte, len(content[offset:end])) copy(result, content[offset:end]) return result, nil } func (m *MockFS) WriteFile(path string, content []byte) error { m.files[path] = content return nil } func (m *MockFS) WriteFileAtomic(path string, content []byte) error { m.files[path] = content return nil } func (m *MockFS) DeleteFile(path string) error { delete(m.files, path) return nil } func (m *MockFS) CreateDir(path string) error { return nil } func (m *MockFS) DirExists(path string) bool { _, ok := m.files[path] return ok } func (m *MockFS) FileExists(path string) bool { _, ok := m.files[path] return ok } // mockFileInfo implements io.FileInfo for MockFS. type mockFileInfo struct { name string size int64 mode uint32 modTime time.Time } func (f *mockFileInfo) Name() string { return f.name } func (f *mockFileInfo) Size() int64 { return f.size } func (f *mockFileInfo) Mode() os.FileMode { return os.FileMode(f.mode) } func (f *mockFileInfo) ModTime() time.Time { return f.modTime } func (f *mockFileInfo) IsDir() bool { return false } func (f *mockFileInfo) Sys() any { return nil } // mockDirEntry implements types.DirEntry for MockFS ReadDir. type mockDirEntry struct { name string isDir bool } func (e *mockDirEntry) Name() string { return e.name } func (e *mockDirEntry) IsDir() bool { return e.isDir } func (e *mockDirEntry) Info() (os.FileInfo, error) { return &mockFileInfo{name: e.name, size: 0}, nil } func (m *MockFS) ReadDir(path string) ([]types.DirEntry, error) { var entries []types.DirEntry for p := range m.files { if filepath.Dir(p) == path { entries = append(entries, &mockDirEntry{name: filepath.Base(p), isDir: false}) } } return entries, nil } var _ FileSystem = (*MockFS)(nil) // Compile-time interface check