Skip to content

fix: back the file watcher with an mtime poll net (pollIntervalMs, default 5s) - #6

Merged
bytesnail merged 1 commit into
mainfrom
fix/watch-poll-fallback
Oct 4, 2026
Merged

bytesnail merged 1 commit into
mainfrom
fix/watch-poll-fallback

Conversation

@bytesnail

Copy link
Copy Markdown
Owner

Why

今天 CI 抖动调查的终点:热加载测试在 macOS 上以 15s 上限失败 4 次后,把上限提到 60s 依然超时(darwin, gave up after 60008 ms)。这不是延迟而是事件丢失——FSEvents 在负载下内核队列溢出时会直接丢事件,Node 的 fs.watch 不会把溢出翻译成文件事件。这是插件的产品级可靠性问题:真实用户的 macOS 机器高负载时,改 secrets.env 也可能永远等不到热加载。

What

事件监听保留为快速路径(约 1 秒生效的卖点不变),新增低频 mtime 轮询兜底(默认 5s,一次 stat,unref'd):签名(存在性+mtime)与上次成功应用的基线不一致就触发防抖 reload。丢事件的后果从永久错过直到重启降级为最多慢一个轮询间隔。

实现要点(建议 review 时重点看)

  • 签名在读之前取(fileSignature before readSecrets):读到一半发生写入会记录旧签名,下一次 tick 补一次多余的 diffed reload(无害),反向则会漏更新
  • tick 在防抖 pending 或 reload 进行中时直接跳过——否则轮询间隔 < 300ms 防抖时会永远重置防抖计时器(开发中实测:reload 被饿死 + 事件循环被钉住进程不退出)
  • 从未成功应用过(启动时读取失败)的场景:首次 tick 建立基线,此后每个新签名重试一次——不可读文件的 warn 每次变化最多一条,不刷屏
  • 新测试用 t.mock.module 把 fs.watch 桩成哑对象,隔离验证 poll 路径;为此单测入口加了 --experimental-test-module-mocks(22.3+ 可用,24 仍需 flag;CI 矩阵会验证 22.18)。踩到的坑:node:fs 命名空间有 getter-only 导出(constants),mock 时必须枚举用到的导出,不能整体展开
  • watch: false 语义不变(完全关闭实时更新);poll 随 watch 门控

Docs

  • 双 README:options 表新增 pollIntervalMs 行 + 热更新章节一句兜底说明
  • CHANGELOG [Unreleased] 新增 Fixed 条目

…fault 5s)

FSEvents can drop events under load — observed 2026-10-04 on macOS CI,
where no event arrived within 60 s and the hot reload was missed until
restart. The event watcher stays the fast path; an unref'd low-frequency
poll (one stat per interval) compares existence+mtime against the last
applied signature and schedules the debounced reload on divergence, so a
dropped event degrades to a few seconds' delay.

- New pollIntervalMs option (default 5000, 0 disables, gated by watch).
- Signature taken BEFORE the read: a mid-read write then records the
  older signature, yielding one extra diffed reload instead of a miss.
- The tick skips while a debounce is pending or a reload is in flight —
  re-arming would starve the 300 ms debounce (poll interval < debounce)
  and pin the event loop forever.
- Poll path is covered by a test that stubs fs.watch via t.mock.module;
  the unit runner now passes --experimental-test-module-mocks (available
  since node 22.3, still gated in 24). Mocking node:fs must enumerate the
  used exports — spreading the whole namespace dies on the getter-only
  'constants' export.
- READMEs document the option; CHANGELOG [Unreleased] updated.
@bytesnail
bytesnail merged commit 78134e8 into main Oct 4, 2026
15 checks passed
@bytesnail
bytesnail deleted the fix/watch-poll-fallback branch October 4, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant