Class: Pikuri::Lsp::Servers
- Inherits:
-
Object
- Object
- Pikuri::Lsp::Servers
- Defined in:
- lib/pikuri/lsp/servers.rb
Overview
Every language server this agent may use: one ClientWrapper per Registry entry, and the routing that picks which one answers for a file. Nothing is spawned until a call needs it — construction is pure, and each of the two routes starts what it needs:
servers = Servers.new(registry: registry, root: filesystem.project_root)
servers.client_for('lib/pikuri/agent.rb') # the claiming server, spawned now
servers.client_for('README.md') # => nil, and nothing spawned
servers.active # every project-relevant server, spawned
servers.close
Arm teardown straight after construction, not after the first call: a
server that started before a later one failed is registered here precisely
so an already-armed close still sweeps it.
Implementation details
Because a start is lazy, a misconfigured command is discovered by the
call that needed it — a ClientWrapper::ServerDied naming the id and the
argv, which Navigator relays as an "Error: …" observation. That is the
same path a server dying after a successful spawn already took.
Sharing: P_one_agent, accepted for today — @clients / @active are
unguarded, #close kills children another caller may be mid-request on,
and a per-agent Agent::Control::Cancellable is threaded into
every handshake. A shared server pool is buildable and simply isn't built,
so N agents over one project means N sets of language servers.
Instance Attribute Summary collapse
-
#registry ⇒ Registry
readonly
The configuration these clients run.
Instance Method Summary collapse
-
#active ⇒ Array<ClientWrapper>
The fan-out set: every project-relevant server, each spawned here if it was not running.
-
#active_entries ⇒ Array<Registry::StdioEntry>
The registered entries this project has files for — the relevance question answered without spawning anything.
-
#client_for(path) ⇒ ClientWrapper?
The server that answers for
path, started if it was not running. -
#close ⇒ void
Shut every started server down.
-
#empty? ⇒ Boolean
Whether there is nothing to ask — a registry question, not a liveness one: a configured server counts whether or not it has started, or this would answer "none" to the very first call.
-
#initialize(registry:, root:, cancellable: nil, spawn: nil) ⇒ Servers
constructor
A new instance of Servers.
-
#started_ids ⇒ Array<String>
Ids of the servers actually running, in start order — nothing at all until a call needs one.
Constructor Details
#initialize(registry:, root:, cancellable: nil, spawn: nil) ⇒ Servers
Returns a new instance of Servers.
49 50 51 52 53 54 55 56 57 58 |
# File 'lib/pikuri/lsp/servers.rb', line 49 def initialize(registry:, root:, cancellable: nil, spawn: nil) @registry = registry @root = root @cancellable = cancellable @spawn = spawn || lambda { |entry| ClientWrapper.spawn(entry, root: @root, cancellable: @cancellable) } @clients = {} @active = nil end |
Instance Attribute Details
#registry ⇒ Registry (readonly)
Returns the configuration these clients run.
37 38 39 |
# File 'lib/pikuri/lsp/servers.rb', line 37 def registry @registry end |
Instance Method Details
#active ⇒ Array<ClientWrapper>
The fan-out set: every project-relevant server, each spawned here if it was not running.
Relevant rather than registered because a fan-out cannot choose between servers without starting them — Navigator gates on what each advertised in this workspace, and only a live handshake carries that.
71 72 73 |
# File 'lib/pikuri/lsp/servers.rb', line 71 def active active_entries.map { |entry| client(entry) } end |
#active_entries ⇒ Array<Registry::StdioEntry>
The registered entries this project has files for — the relevance question answered without spawning anything.
Rescanned while the answer is incomplete, memoized once every
registered entry is accounted for. A project gains its first .java
file mid-session — the agent wrote it — and a scan memoized at the first
call would leave jdtls out of every later fan-out, which the model reads
as "no such symbol" rather than "nobody asked". A walk costs ~4 ms on
this repo against a path that is about to block on a whole index, so
paying it until the answer stops changing is free; once every entry is
relevant nothing can invalidate it and the walk stops.
The first entry claiming a basename wins, exactly as Registry#entry_for decides, so an entry shadowed by an earlier one is never activated by files it would not be asked about.
92 93 94 95 96 |
# File 'lib/pikuri/lsp/servers.rb', line 92 def active_entries return @active if @active && @active.size == @registry.entries.size @active = relevant_entries end |
#client_for(path) ⇒ ClientWrapper?
The server that answers for path, started if it was not running.
Cheap: the path is its own evidence of relevance, so this route never walks the project the way #active_entries must.
107 108 109 110 |
# File 'lib/pikuri/lsp/servers.rb', line 107 def client_for(path) entry = @registry.entry_for(path) entry && client(entry) end |
#close ⇒ void
This method returns an undefined value.
Shut every started server down. Idempotent, and never raises — it runs
from an on_close at exit, where one bad teardown must not take the rest
of the sweep with it (ClientWrapper#close already swallows its own).
130 131 132 133 134 135 |
# File 'lib/pikuri/lsp/servers.rb', line 130 def close @clients.each_value(&:close) @clients.clear LOGGER.debug('closed') nil end |
#empty? ⇒ Boolean
Returns whether there is nothing to ask — a registry question, not a liveness one: a configured server counts whether or not it has started, or this would answer "none" to the very first call.
121 122 123 |
# File 'lib/pikuri/lsp/servers.rb', line 121 def empty? @registry.empty? end |
#started_ids ⇒ Array<String>
Returns ids of the servers actually running, in start order — nothing at all until a call needs one.
114 115 116 |
# File 'lib/pikuri/lsp/servers.rb', line 114 def started_ids @clients.keys end |