watcher.ts 1.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. import { watch } from "node:fs"
  2. import type { WorkflowDef } from "./types"
  3. export class FileWatcher {
  4. private watchers: Map<string, ReturnType<typeof watch>> = new Map()
  5. private timers: Map<string, ReturnType<typeof setTimeout>> = new Map()
  6. watch(def: WorkflowDef, worktree: string, onTrigger: () => void): void {
  7. if (def.trigger !== "file_change" || !def.paths) return
  8. const key = def.name
  9. this.unwatch(key)
  10. const patterns = def.paths.map((p) => {
  11. let escaped = p
  12. .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex metachars
  13. .replace(/\*\*/g, "\x00") // globstar placeholder
  14. .replace(/\*/g, "[^/]*") // * → any non-slash
  15. .replace(/\x00/g, ".*") // globstar placeholder → .*
  16. .replace(/\?/g, "[^/]") // ? → single char
  17. return new RegExp("^" + escaped + "$")
  18. })
  19. const debounceMs = (def.debounce ?? 60) * 1000
  20. const watcher = watch(worktree, { recursive: true, persistent: false }, (_event, filename) => {
  21. if (!filename) return
  22. if (patterns.some((r) => r.test(filename))) {
  23. const existing = this.timers.get(key)
  24. if (existing) clearTimeout(existing)
  25. this.timers.set(key, setTimeout(onTrigger, debounceMs))
  26. }
  27. })
  28. this.watchers.set(key, watcher)
  29. }
  30. unwatch(key: string): void {
  31. const w = this.watchers.get(key)
  32. if (w) { w.close(); this.watchers.delete(key) }
  33. const t = this.timers.get(key)
  34. if (t) { clearTimeout(t); this.timers.delete(key) }
  35. }
  36. stopAll(): void {
  37. for (const [k] of this.watchers) this.unwatch(k)
  38. }
  39. }