local util = require('opencode.util')
local safe_call = util.safe_call
local Promise = require('opencode.promise')
local auth = require('opencode.auth')
local protocol_connection = require('opencode.protocols.connection')
--- @class OpencodeServer
---@field job vim.SystemObj|nil The vim.system job handle
---@field url string|nil The server URL once ready
---@field port number|nil The port this server is using (for custom servers)
---@field handle integer|nil Compatibility property for job.stop interface
---@field protocol? 'v1'|'v2' Protocol selected by authenticated health probe
---@field version? string Server version returned by the selected health endpoint
---@field server_identity? {version: string, pid: integer|nil} Identity facts returned by the probe or acquisition
---@field credential? {username: string, password?: string} Credential owned by this connection
---@field operations? OpencodeV1Operations|OpencodeV2Operations Protocol operations selected when the connection becomes ready
---@field observations table
---@field private _observe? fun(connection: OpencodeServer, ref: {id: string, location?: OpencodeLocation}): OpencodeObservation
---@field private _close_observations? fun(connection: OpencodeServer)
local OpencodeServer = {}
OpencodeServer.__index = OpencodeServer
local vim_leave_setup = false
local function ensure_vim_leave_autocmd()
if vim_leave_setup then
return
end
vim_leave_setup = true
vim.api.nvim_create_autocmd('VimLeavePre', {
group = vim.api.nvim_create_augroup('OpencodeVimLeavePre', { clear = true }),
callback = function()
local state = require('opencode.state')
if state.opencode_server then
state.opencode_server:close()
end
end,
})
end
--- Create a new ServerJob instance
--- @return OpencodeServer
function OpencodeServer.new()
ensure_vim_leave_autocmd()
return setmetatable({
job = nil,
url = nil,
port = nil,
handle = nil,
protocol = nil,
version = nil,
server_identity = nil,
credential = nil,
operations = nil,
observations = {},
shutdown_promise = Promise.new(),
_ready = false,
_shutdown_requested = false,
_release_process = nil,
_stream = nil,
_requests = {},
_observe = nil,
_close_observations = nil,
}, OpencodeServer)
end
--- Create a server instance that connects to a custom server
--- @param url string The custom server URL
--- @param port number|nil The port number (for PID tracking)
--- @return OpencodeServer
function OpencodeServer.from_custom(url, port)
local instance = OpencodeServer.new()
instance.url = url
instance.port = port
return instance
end
---@return boolean
function OpencodeServer:is_ready()
return self._ready
end
---@param release? fun()
function OpencodeServer:set_process_release(release)
if self._ready or self.shutdown_promise:is_resolved() then
error('cannot change release behavior of a ready connection')
end
self._release_process = release
end
---@return boolean
function OpencodeServer:can_release_process()
return self._release_process ~= nil
end
---@return boolean
function OpencodeServer:release_process()
local release = self._release_process
self._release_process = nil
if not release then
return false
end
release()
return true
end
---@param stream? {shutdown: fun(self: table)}
function OpencodeServer:set_stream(stream)
if stream and self._stream then
pcall(stream.shutdown, stream)
error('Connection already owns an SSE stream')
end
if stream and not self:is_ready() then
pcall(stream.shutdown, stream)
error('cannot attach SSE to a closed Connection')
end
self._stream = stream
end
---@param request {shutdown: fun(self: table)}
function OpencodeServer:_track_request(request)
if not self:is_ready() then
pcall(request.shutdown, request)
error('cannot attach HTTP request to a closed Connection')
end
self._requests[request] = true
end
---@param request table
function OpencodeServer:_untrack_request(request)
self._requests[request] = nil
end
--- Publish the connection only after its authenticated protocol probe succeeds.
---@return OpencodeServer
function OpencodeServer:mark_ready()
if self._ready then
return self
end
if self.shutdown_promise:is_resolved() then
error('cannot ready a closed Connection')
end
if type(self.url) ~= 'string' or self.url == '' then
error('ready connection requires url')
end
local runtime = self.protocol and protocol_connection.runtime(self.protocol)
if not runtime then
error('ready connection requires protocol')
end
if
type(self.server_identity) ~= 'table'
or type(self.server_identity.version) ~= 'string'
or self.server_identity.version == ''
then
error('ready connection requires server_identity')
end
if self.server_identity.pid ~= nil and type(self.server_identity.pid) ~= 'number' then
error('ready connection server_identity pid must be a number')
end
self.version = self.server_identity.version
if type(self.credential) ~= 'table' or type(self.credential.username) ~= 'string' then
error('ready connection requires credential')
end
if self.credential.password ~= nil and type(self.credential.password) ~= 'string' then
error('ready connection credential password must be a string')
end
self.operations = runtime.operations
local observation_protocol = runtime.observation
self._observe = observation_protocol.new
self._close_observations = observation_protocol.close
self._ready = true
return self
end
---Return the unique Observation for a session on this Connection.
---@param ref {id: string, location?: OpencodeLocation}
---@return OpencodeObservation
function OpencodeServer:observe(ref)
if not self:is_ready() or not self._observe then
error('cannot observe a session on a closed Connection')
end
if type(ref) ~= 'table' or type(ref.id) ~= 'string' or ref.id == '' then
error('observe requires a session id')
end
local existing = self.observations[ref.id]
if existing then
return existing
end
local observation = self._observe(self, ref)
self.observations[ref.id] = observation
return observation
end
--- Probe protocol using authenticated health endpoints.
---@param timeout_ms number|nil
---@return Promise<{protocol: 'v1'|'v2', response: table}>
function OpencodeServer:probe_connection(timeout_ms)
return protocol_connection.probe(self, timeout_ms)
end
---Check if the server is reachable via its health endpoint.
---@return Promise