Files
kjol/tools/tsgo/internal/fswatch/event.go
2026-07-09 16:50:43 -04:00

272 lines
6.3 KiB
Go

package fswatch
import "sync"
// EventKind classifies a filesystem change.
type EventKind int
const (
EventUpdate EventKind = iota + 1
EventDelete
)
func (k EventKind) String() string {
switch k {
case EventUpdate:
return "update"
case EventDelete:
return "delete"
default:
return "unknown"
}
}
// Event describes a single filesystem change.
type Event struct {
Kind EventKind
Path string
includedWatchRoot bool
}
// eventEntry tracks coalescing state during a debounce batch.
type eventEntry struct {
createdSeq uint64
updatedSeq uint64
deletedSeq uint64
includedWatchRoot bool
}
// eventList coalesces filesystem events by path within a debounce window.
// - create after delete → update (rapid delete+recreate)
// - getEvents skips entries that were both created and deleted
type eventList struct {
mu sync.Mutex
entries map[string]*eventEntry
err error
seq uint64
}
// create records a new-file event for path. Both create and update
// produce EventUpdate externally; sequence state tracks coalescing
// (create+delete within a batch cancels out).
func (el *eventList) create(path string) {
el.mu.Lock()
defer el.mu.Unlock()
seq := el.nextSeqLocked()
el.createLocked(path, seq)
}
func (el *eventList) createAt(path string, seq uint64) {
el.mu.Lock()
defer el.mu.Unlock()
el.advanceSeqLocked(seq)
el.createLocked(path, seq)
}
func (el *eventList) createLocked(path string, seq uint64) {
entry := el.getOrCreate(path)
if entry.isDeleted() {
// Rapid delete+recreate: clear both flags so the entry
// emits EventUpdate (the default for non-deleted entries).
// https://github.com/parcel-bundler/watcher/issues/72
entry.deletedSeq = 0
entry.createdSeq = 0
entry.updatedSeq = seq
} else {
entry.createdSeq = seq
}
}
// update records an update event for path.
func (el *eventList) update(path string) {
el.mu.Lock()
defer el.mu.Unlock()
seq := el.nextSeqLocked()
el.updateLocked(path, seq)
}
func (el *eventList) updateAt(path string, seq uint64) {
el.mu.Lock()
defer el.mu.Unlock()
el.advanceSeqLocked(seq)
el.updateLocked(path, seq)
}
func (el *eventList) updateWatchRootAt(path string, seq uint64) {
el.mu.Lock()
defer el.mu.Unlock()
el.advanceSeqLocked(seq)
el.updateLocked(path, seq)
el.getOrCreate(path).includedWatchRoot = true
}
func (el *eventList) updateLocked(path string, seq uint64) {
el.getOrCreate(path).updatedSeq = seq
}
// remove records a delete event for path.
func (el *eventList) remove(path string) {
el.mu.Lock()
defer el.mu.Unlock()
seq := el.nextSeqLocked()
el.removeLocked(path, seq)
}
func (el *eventList) removeAndGetSequence(path string) uint64 {
el.mu.Lock()
defer el.mu.Unlock()
seq := el.nextSeqLocked()
el.removeLocked(path, seq)
return seq
}
func (el *eventList) removeAt(path string, seq uint64) {
el.mu.Lock()
defer el.mu.Unlock()
el.advanceSeqLocked(seq)
el.removeLocked(path, seq)
}
func (el *eventList) removeWatchRootAt(path string, seq uint64) {
el.mu.Lock()
defer el.mu.Unlock()
el.advanceSeqLocked(seq)
el.removeLocked(path, seq)
el.getOrCreate(path).includedWatchRoot = true
}
func (el *eventList) removeLocked(path string, seq uint64) {
entry := el.getOrCreate(path)
entry.deletedSeq = seq
}
// size returns the number of tracked entries (including ones that may
// cancel out in getEvents).
func (el *eventList) size() int {
el.mu.Lock()
defer el.mu.Unlock()
return len(el.entries)
}
// snapshotLocked returns the current set of pending events with
// create+delete pairs filtered out. Caller must hold el.mu.
func (el *eventList) snapshotLocked() []Event {
return el.snapshotSinceLocked(0)
}
func (el *eventList) snapshotSinceLocked(startSeq uint64) []Event {
out := make([]Event, 0, len(el.entries))
for path, e := range el.entries {
kind, ok := e.kindSince(startSeq)
if !ok {
continue
}
out = append(out, Event{Kind: kind, Path: path, includedWatchRoot: e.includedWatchRoot})
}
return out
}
// getEvents returns a snapshot of events, skipping entries that were both
// created and deleted. Order is not guaranteed.
func (el *eventList) getEvents() []Event {
el.mu.Lock()
defer el.mu.Unlock()
return el.snapshotLocked()
}
// drain atomically snapshots all pending events and the stored error,
// then clears the list. This prevents events added between a separate
// getEvents+clear from being silently dropped.
func (el *eventList) drain() ([]Event, error) {
el.mu.Lock()
defer el.mu.Unlock()
out := el.snapshotLocked()
err := el.err
el.entries = nil
el.err = nil
return out, err
}
func (el *eventList) drainForSequences(startSeqs []uint64) ([][]Event, error) {
el.mu.Lock()
defer el.mu.Unlock()
out := make([][]Event, len(startSeqs))
for i, startSeq := range startSeqs {
out[i] = el.snapshotSinceLocked(startSeq)
}
err := el.err
el.entries = nil
el.err = nil
return out, err
}
// setError stores the first error encountered (later errors are ignored).
func (el *eventList) setError(err error) {
el.mu.Lock()
defer el.mu.Unlock()
if el.err == nil {
el.err = err
}
}
// hasError reports whether an error has been recorded.
func (el *eventList) hasError() bool {
el.mu.Lock()
defer el.mu.Unlock()
return el.err != nil
}
// getError returns the stored error (or nil if none).
func (el *eventList) getError() error {
el.mu.Lock()
defer el.mu.Unlock()
return el.err
}
func (el *eventList) getOrCreate(path string) *eventEntry {
if el.entries == nil {
el.entries = make(map[string]*eventEntry)
}
if e, ok := el.entries[path]; ok {
return e
}
e := &eventEntry{}
el.entries[path] = e
return e
}
func (el *eventList) sequence() uint64 {
el.mu.Lock()
defer el.mu.Unlock()
return el.seq
}
func (el *eventList) nextSeqLocked() uint64 {
el.seq++
return el.seq
}
func (el *eventList) advanceSeqLocked(seq uint64) {
if seq > el.seq {
el.seq = seq
}
}
func (e *eventEntry) isDeleted() bool {
return e.deletedSeq > e.createdSeq && e.deletedSeq > e.updatedSeq
}
func (e *eventEntry) kindSince(startSeq uint64) (EventKind, bool) {
if e.deletedSeq > startSeq {
if e.createdSeq > startSeq && e.createdSeq < e.deletedSeq && e.updatedSeq < e.deletedSeq {
return 0, false
}
return EventDelete, true
}
seq := max(e.createdSeq, e.updatedSeq)
if seq > startSeq {
return EventUpdate, true
}
return 0, false
}