Class: Rigor::LanguageServer::Debouncer

Inherits:
Object
  • Object
show all
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

Constructor Details

#initializeDebouncer

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_allObject

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_sizeInteger

Returns number of currently-pending tasks.

Returns:

  • (Integer)

    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.message}"
      end
    end
    @mutex.synchronize { @tasks.delete(key) if @tasks[key] == task }
  end
  nil
end