Class: Rigor::LanguageServer::Debouncer
- Inherits:
-
Object
- Object
- Rigor::LanguageServer::Debouncer
- Defined in:
- lib/rigor/language_server/debouncer.rb
Overview
Per-key debouncer. The LSP uses this to defer publishDiagnostics until the user stops typing (200ms
quiet-time floor per LSP UX conventions). Each schedule(uri, delay:) cancels the previous task for the
same uri and queues a new one — only the LAST task in a burst actually runs.
Threading model: each scheduled task runs in its own Thread so the dispatcher loop doesn't block.
Concurrent writes to STDOUT from the Debouncer's threads + the main dispatch loop are serialised by
SynchronizedWriter.
Cancellation is cooperative: each task carries a cancelled flag; new schedules flip the prior task's
flag and the prior thread skips the block on wake-up. This is safer than Thread#kill for in-flight
Ruby code and good enough for the "drop stale debounce" use case.
Defined Under Namespace
Classes: Task
Instance Method Summary collapse
-
#cancel_all ⇒ Object
Cancel every pending task (sets the flag; the threads exit without running the block).
-
#flush! ⇒ Object
Wait for every pending task to complete.
-
#initialize ⇒ Debouncer
constructor
A new instance of Debouncer.
-
#pending_size ⇒ Integer
Number of currently-pending tasks.
-
#schedule(key, delay:, &block) ⇒ Object
Schedule
blockto run afterdelayseconds, replacing any pending task for the samekey.
Constructor Details
#initialize ⇒ Debouncer
Returns a new instance of Debouncer.
19 20 21 22 |
# File 'lib/rigor/language_server/debouncer.rb', line 19 def initialize @tasks = {} @mutex = Mutex.new end |
Instance Method Details
#cancel_all ⇒ Object
Cancel every pending task (sets the flag; the threads exit without running the block). Called on
shutdown so in-flight publishes don't write to a closed STDOUT.
64 65 66 67 68 69 |
# File 'lib/rigor/language_server/debouncer.rb', line 64 def cancel_all @mutex.synchronize do @tasks.each_value { |t| t.cancelled = true } @tasks.clear end end |
#flush! ⇒ Object
Wait for every pending task to complete. Used by specs to synchronise with the async schedule; the
production shutdown path uses #cancel_all instead.
53 54 55 56 57 58 59 60 |
# File 'lib/rigor/language_server/debouncer.rb', line 53 def flush! threads = @mutex.synchronize { @tasks.values.map(&:thread) } threads.each do |thread| thread.join rescue StandardError # Threads can die from raised exceptions; ignore. end end |
#pending_size ⇒ Integer
Returns number of currently-pending tasks.
72 73 74 |
# File 'lib/rigor/language_server/debouncer.rb', line 72 def pending_size @mutex.synchronize { @tasks.size } end |
#schedule(key, delay:, &block) ⇒ Object
Schedule block to run after delay seconds, replacing any pending task for the same key.
delay: 0 makes the task fire immediately (still on its own thread); tests pair this with #flush!
for deterministic assertions.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/rigor/language_server/debouncer.rb', line 27 def schedule(key, delay:, &block) task = Task.new(nil, false) previous = @mutex.synchronize do prev = @tasks[key] @tasks[key] = task prev end previous&.cancelled = true task.thread = Thread.new do sleep(delay) if delay.positive? unless task.cancelled begin block.call rescue StandardError => e warn "Debouncer task #{key.inspect}: #{e.class}: #{e.}" end end @mutex.synchronize { @tasks.delete(key) if @tasks[key] == task } end nil end |