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.



25
26
27
28
# File 'lib/rigor/language_server/debouncer.rb', line 25

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.



73
74
75
76
77
78
# File 'lib/rigor/language_server/debouncer.rb', line 73

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.



61
62
63
64
65
66
67
68
# File 'lib/rigor/language_server/debouncer.rb', line 61

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.



81
82
83
# File 'lib/rigor/language_server/debouncer.rb', line 81

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.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/rigor/language_server/debouncer.rb', line 34

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