Skip to content

Commit 70e3c6f

Browse files
authored
fix(event_manager): prevent duplicate listeners on restart (#278)
1 parent a53056d commit 70e3c6f

2 files changed

Lines changed: 110 additions & 24 deletions

File tree

lua/opencode/event_manager.lua

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ local util = require('opencode.util')
154154
--- @class EventManager
155155
--- @field events table<string, function[]> Event listener registry
156156
--- @field server_subscription table|nil Subscription to server events
157+
--- @field state_server_listener function|nil Listener for state.opencode_server updates
157158
--- @field is_started boolean Whether the event manager is started
158159
--- @field captured_events table[] List of captured events for debugging
159160
--- @field throttling_emitter ThrottlingEmitter Throttle instance for batching events
@@ -166,6 +167,7 @@ function EventManager.new()
166167
local self = setmetatable({
167168
events = {},
168169
server_subscription = nil,
170+
state_server_listener = nil,
169171
is_started = false,
170172
captured_events = {},
171173
}, EventManager)
@@ -208,6 +210,13 @@ function EventManager:subscribe(event_name, callback)
208210
if not self.events[event_name] then
209211
self.events[event_name] = {}
210212
end
213+
214+
for _, cb in ipairs(self.events[event_name]) do
215+
if cb == callback then
216+
return
217+
end
218+
end
219+
211220
table.insert(self.events[event_name], callback)
212221
end
213222

@@ -243,10 +252,10 @@ function EventManager:unsubscribe(event_name, callback)
243252
return
244253
end
245254

246-
for i, cb in ipairs(listeners) do
255+
for i = #listeners, 1, -1 do
256+
local cb = listeners[i]
247257
if cb == callback then
248258
table.remove(listeners, i)
249-
break
250259
end
251260
end
252261
end
@@ -351,31 +360,34 @@ function EventManager:start()
351360

352361
self.is_started = true
353362

354-
state.subscribe(
355-
'opencode_server',
356-
--- @param key string
357-
--- @param current OpencodeServer|nil
358-
--- @param prev OpencodeServer|nil
359-
function(key, current, prev)
360-
if current and current:get_spawn_promise() then
361-
self:emit('custom.server_starting', { url = current.url })
362-
363-
current:get_spawn_promise():and_then(function(server)
364-
self:emit('custom.server_ready', { url = server.url })
365-
vim.defer_fn(function()
366-
self:_subscribe_to_server_events(server)
367-
end, 200)
368-
end)
369-
370-
current:get_shutdown_promise():and_then(function()
371-
self:emit('custom.server_stopped', {})
372-
self:_cleanup_server_subscription()
373-
end)
374-
elseif prev and not current then
363+
if self.state_server_listener then
364+
state.unsubscribe('opencode_server', self.state_server_listener)
365+
end
366+
367+
self.state_server_listener = function(key, current, prev)
368+
if current and current:get_spawn_promise() then
369+
self:emit('custom.server_starting', { url = current.url })
370+
371+
current:get_spawn_promise():and_then(function(server)
372+
self:emit('custom.server_ready', { url = server.url })
373+
vim.defer_fn(function()
374+
self:_subscribe_to_server_events(server)
375+
end, 200)
376+
end)
377+
378+
current:get_shutdown_promise():and_then(function()
375379
self:emit('custom.server_stopped', {})
376380
self:_cleanup_server_subscription()
377-
end
381+
end)
382+
elseif prev and not current then
383+
self:emit('custom.server_stopped', {})
384+
self:_cleanup_server_subscription()
378385
end
386+
end
387+
388+
state.subscribe(
389+
'opencode_server',
390+
self.state_server_listener
379391
)
380392
end
381393

@@ -385,6 +397,10 @@ function EventManager:stop()
385397
end
386398

387399
self.is_started = false
400+
if self.state_server_listener then
401+
state.unsubscribe('opencode_server', self.state_server_listener)
402+
self.state_server_listener = nil
403+
end
388404
self:_cleanup_server_subscription()
389405

390406
self.throttling_emitter:clear()

tests/unit/event_manager_spec.lua

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
local EventManager = require('opencode.event_manager')
2+
local Promise = require('opencode.promise')
3+
local state = require('opencode.state')
24

35
describe('EventManager', function()
46
local event_manager
@@ -75,6 +77,26 @@ describe('EventManager', function()
7577
assert.is_false(callback_called)
7678
end)
7779

80+
it('does not duplicate the same event callback', function()
81+
local callback_called = 0
82+
local callback = function()
83+
callback_called = callback_called + 1
84+
end
85+
86+
event_manager:subscribe('test_event', callback)
87+
event_manager:subscribe('test_event', callback)
88+
89+
assert.are.equal(1, event_manager:get_subscriber_count('test_event'))
90+
91+
event_manager:emit('test_event', {})
92+
93+
vim.wait(100, function()
94+
return callback_called > 0
95+
end)
96+
97+
assert.are.equal(1, callback_called)
98+
end)
99+
78100
it('should track subscriber count', function()
79101
local callback1 = function() end
80102
local callback2 = function() end
@@ -119,6 +141,54 @@ describe('EventManager', function()
119141
assert.are.equal(first_start, event_manager.is_started)
120142
end)
121143

144+
it('does not duplicate opencode_server listener across restart', function()
145+
local original_defer_fn = vim.defer_fn
146+
vim.defer_fn = function(fn, _)
147+
fn()
148+
end
149+
150+
local original_subscribe_to_server_events = event_manager._subscribe_to_server_events
151+
local subscribe_calls = 0
152+
153+
event_manager._subscribe_to_server_events = function()
154+
subscribe_calls = subscribe_calls + 1
155+
end
156+
157+
local function resolved(value)
158+
local p = Promise.new()
159+
p:resolve(value)
160+
return p
161+
end
162+
163+
local fake_server = {
164+
url = 'http://127.0.0.1:4000',
165+
get_spawn_promise = function(self)
166+
return resolved(self)
167+
end,
168+
get_shutdown_promise = function()
169+
return resolved(true)
170+
end,
171+
}
172+
173+
state.opencode_server = nil
174+
175+
event_manager:start()
176+
event_manager:stop()
177+
event_manager:start()
178+
179+
state.opencode_server = fake_server
180+
181+
vim.wait(200, function()
182+
return subscribe_calls > 0
183+
end)
184+
185+
assert.are.equal(1, subscribe_calls)
186+
187+
state.opencode_server = nil
188+
event_manager._subscribe_to_server_events = original_subscribe_to_server_events
189+
vim.defer_fn = original_defer_fn
190+
end)
191+
122192
describe('User autocmd events', function()
123193
it('should fire User autocmd when emitting events', function()
124194
local autocmd_called = false

0 commit comments

Comments
 (0)