package barkfile import ( "context" "crypto/sha256" "errors" "fmt" "os" "time" ) const defaultPollInterval = 250 * time.Millisecond // Change is one validated Barkfile revision or an error reading or validating it. // Changes are delivered in source order and include the initial valid revision. type Change struct { Config Config Err error } // WatchOptions configures file watching. A zero PollInterval uses 250ms. type WatchOptions struct { PollInterval time.Duration } // Load reads, parses, and validates a Barkfile. func Load(path string) (Config, error) { contents, err := os.ReadFile(path) if err != nil { return Config{}, fmt.Errorf("read Barkfile: %w", err) } return parseAndValidate(contents) } // Watch emits the initial valid Barkfile and each subsequent file-content change. // An invalid or unreadable revision is delivered as Change.Err; watching continues // so a later valid revision is delivered. Cancel ctx to close the returned channel. func Watch(ctx context.Context, path string, options WatchOptions) (<-chan Change, error) { if ctx == nil { return nil, errors.New("watch context is required") } interval := options.PollInterval if interval == 0 { interval = defaultPollInterval } if interval < 0 { return nil, errors.New("watch poll interval cannot be negative") } initial, fingerprint, err := loadRevision(path) if err != nil { return nil, err } changes := make(chan Change) go watchLoop(ctx, path, interval, initial, fingerprint, changes) return changes, nil } func watchLoop(ctx context.Context, path string, interval time.Duration, initial Config, fingerprint [sha256.Size]byte, changes chan<- Change) { defer close(changes) if !sendChange(ctx, changes, Change{Config: initial}) { return } ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: config, nextFingerprint, err := loadRevision(path) if nextFingerprint == fingerprint { continue } fingerprint = nextFingerprint if !sendChange(ctx, changes, Change{Config: config, Err: err}) { return } } } } func sendChange(ctx context.Context, changes chan<- Change, change Change) bool { select { case changes <- change: return true case <-ctx.Done(): return false } } func loadRevision(path string) (Config, [sha256.Size]byte, error) { contents, err := os.ReadFile(path) if err != nil { return Config{}, sha256.Sum256([]byte("read error:\x00" + err.Error())), fmt.Errorf("read Barkfile: %w", err) } fingerprint := sha256.Sum256(contents) config, err := parseAndValidate(contents) return config, fingerprint, err } func parseAndValidate(contents []byte) (Config, error) { config, err := Parse(contents) if err != nil { return Config{}, err } if err := config.Validate(); err != nil { return Config{}, fmt.Errorf("invalid Barkfile: %w", err) } return config, nil }