refactor(util): reuse timer handles in Debouncer

This commit is contained in:
2026-04-15 21:16:48 +02:00
parent 9c6130d3d2
commit 227900d81c
3 changed files with 63 additions and 45 deletions
+3 -12
View File
@@ -1,4 +1,3 @@
local log = require("log")
local util = require("util") local util = require("util")
local HIGHLIGHTS = { local HIGHLIGHTS = {
@@ -97,21 +96,13 @@ local Repo = {}
Repo.__index = Repo Repo.__index = Repo
function Repo:start_watcher() function Repo:start_watcher()
local watcher, err_msg, err_name = vim.uv.new_fs_event() local watcher = assert(vim.uv.new_fs_event())
if not watcher then assert(watcher:start(self.gitdir, {}, function(err, filename)
log.error(
"Failed to create fs event watcher: %s (%s)",
err_msg,
err_name
)
return
end
watcher:start(self.gitdir, {}, function(err, filename)
if err or (filename ~= "index" and filename ~= "HEAD") then if err or (filename ~= "index" and filename ~= "HEAD") then
return return
end end
self.refresh:call() self.refresh:call()
end) end))
self.watcher = watcher self.watcher = watcher
end end
+3 -13
View File
@@ -187,17 +187,7 @@ function M.watch()
local plugins_dir = vim.fs.joinpath(config_dir, "plugins") local plugins_dir = vim.fs.joinpath(config_dir, "plugins")
local err_msg, err_name watcher = assert(vim.uv.new_fs_event())
watcher, err_msg, err_name = vim.uv.new_fs_event()
if not watcher then
log.error(
"Failed to create fs event watcher: %s (%s)",
err_msg,
err_name
)
return
end
reload = util.debounce(function(filename) reload = util.debounce(function(filename)
local path = vim.fs.joinpath(plugins_dir, filename) local path = vim.fs.joinpath(plugins_dir, filename)
if not vim.uv.fs_stat(path) then if not vim.uv.fs_stat(path) then
@@ -211,7 +201,7 @@ function M.watch()
end end
end, 100) end, 100)
watcher:start(plugins_dir, {}, function(err, filename) assert(watcher:start(plugins_dir, {}, function(err, filename)
if err then if err then
log.error("Watch error: %s", err) log.error("Watch error: %s", err)
return return
@@ -220,7 +210,7 @@ function M.watch()
return return
end end
reload:call(filename) reload:call(filename)
end) end))
end end
function M.unwatch() function M.unwatch()
+57 -20
View File
@@ -295,10 +295,16 @@ end
local NIL_KEY = {} local NIL_KEY = {}
---@class ow.Util.Debouncer.Slot
---@field timer uv.uv_timer_t
---@field cb function
---@field id any
---@field args table
---@class ow.Util.Debouncer ---@class ow.Util.Debouncer
---@field private _fn fun(id: any, ...) ---@field private _fn fun(id: any, ...)
---@field private _delay integer ---@field private _delay integer
---@field private _timers table<any, uv.uv_timer_t> ---@field private _slots table<any, ow.Util.Debouncer.Slot>
local Debouncer = {} local Debouncer = {}
Debouncer.__index = Debouncer Debouncer.__index = Debouncer
@@ -306,42 +312,73 @@ Debouncer.__index = Debouncer
---@param delay integer ---@param delay integer
---@return ow.Util.Debouncer ---@return ow.Util.Debouncer
function Debouncer.new(fn, delay) function Debouncer.new(fn, delay)
return setmetatable({ _fn = fn, _delay = delay, _timers = {} }, Debouncer) return setmetatable({ _fn = fn, _delay = delay, _slots = {} }, Debouncer)
end
---@param slot ow.Util.Debouncer.Slot
local function dispose(slot)
if not slot.timer:is_closing() then
slot.timer:stop()
slot.timer:close()
end
end end
---@param id? any ---@param id? any
---@param ... any ---@param ... any
function Debouncer:call(id, ...) function Debouncer:call(id, ...)
local key = id == nil and NIL_KEY or id local key = id == nil and NIL_KEY or id
local slot = self._slots[key]
local args = vim.F.pack_len(...) local args = vim.F.pack_len(...)
self:cancel(id) if not slot then
self._timers[key] = vim.defer_fn(function() -- cb lives on the slot so restart (the else branch below) can hand
self._timers[key] = nil -- the same closure back to timer:start, avoiding a fresh
self._fn(id, vim.F.unpack_len(args)) -- schedule_wrap allocation per call. It reads id/args from the
end, self._delay) -- slot (not upvalues) so restarts pick up the latest args.
end local timer = assert(vim.uv.new_timer())
---@type ow.Util.Debouncer.Slot
---@param timer uv.uv_timer_t local new_slot
local function dispose(timer) new_slot = {
timer:stop() timer = timer,
timer:close() id = id,
args = args,
cb = vim.schedule_wrap(function()
-- Slot may have been cancelled between libuv firing and
-- this scheduled callback running. Identity-check before
-- firing.
if self._slots[key] ~= new_slot then
return
end
self._slots[key] = nil
dispose(new_slot)
self._fn(new_slot.id, vim.F.unpack_len(new_slot.args))
end),
}
self._slots[key] = new_slot
slot = new_slot
else
slot.id = id
slot.args = args
end
-- uv_timer_start on an already-active timer restarts it with the new
-- timeout, reusing the same handle (no per-call allocation).
assert(slot.timer:start(self._delay, 0, slot.cb))
end end
---@param id? any ---@param id? any
function Debouncer:cancel(id) function Debouncer:cancel(id)
local key = id == nil and NIL_KEY or id local key = id == nil and NIL_KEY or id
local timer = self._timers[key] local slot = self._slots[key]
if timer then if slot then
dispose(timer) self._slots[key] = nil
self._timers[key] = nil dispose(slot)
end end
end end
function Debouncer:cancel_all() function Debouncer:cancel_all()
for _, t in pairs(self._timers) do for _, slot in pairs(self._slots) do
dispose(t) dispose(slot)
end end
self._timers = {} self._slots = {}
end end
Util.Debouncer = Debouncer Util.Debouncer = Debouncer