class_name KeetClient
extends Node
## Core HTTP client for keet-daemon (localhost:3117).
## Provides JSON-RPC 2.0 calls and connection management.
signal connected
signal disconnected
signal error_occurred(message: String)
signal status_updated(status: Dictionary)
@export var host: String = "127.0.0.1"
@export var port: int = 3117
@export var auto_connect: bool = true
@export_range(1.0, 30.0) var health_interval: float = 10.0
var is_connected: bool = false
var _base_url: String
var _health_timer: Timer
var _pending_requests: int = 0
func _ready() -> void:
_base_url = "http://%s:%d" % [host, port]
_health_timer = Timer.new()
_health_timer.wait_time = health_interval
_health_timer.timeout.connect(_check_health)
add_child(_health_timer)
if auto_connect:
check_connection()
_health_timer.start()
## Make a JSON-RPC call to keet-daemon.
## Returns result via callback(result, error).
func rpc(method: String, params: Dictionary = {}, callback: Callable = Callable()) -> void:
var req := HTTPRequest.new()
req.timeout = 10.0
add_child(req)
var body := JSON.stringify({"method": method, "params": params})
req.request_completed.connect(_on_rpc_response.bind(callback, req))
_pending_requests += 1
var err := req.request(
_base_url + "/rpc",
["Content-Type: application/json"],
HTTPClient.METHOD_POST,
body
)
if err != OK:
_pending_requests -= 1
req.queue_free()
if callback.is_valid():
callback.call(null, "Request failed: %d" % err)
## Convenience: async RPC via await. Returns {"result": ..., "error": ...}.
func rpc_async(method: String, params: Dictionary = {}) -> Dictionary:
var response := {"result": null, "error": null}
var done := false
rpc(method, params, func(result, err):
response.result = result
response.error = err
done = true
)
while not done:
await get_tree().process_frame
return response
## GET a convenience endpoint (e.g. "/rpc/rooms", "/rpc/identity").
func get_endpoint(path: String, callback: Callable = Callable()) -> void:
var req := HTTPRequest.new()
req.timeout = 10.0
add_child(req)
req.request_completed.connect(_on_get_response.bind(callback, req))
var err := req.request(_base_url + path)
if err != OK:
req.queue_free()
if callback.is_valid():
callback.call(null, "GET failed: %d" % err)
## Check if keet-daemon is reachable.
func check_connection() -> void:
rpc("status", {}, func(result, err):
var was_connected := is_connected
if err != null:
is_connected = false
if was_connected:
disconnected.emit()
else:
is_connected = true
if not was_connected:
connected.emit()
status_updated.emit(result if result is Dictionary else {})
)
func _on_rpc_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray, callback: Callable, req: HTTPRequest) -> void:
_pending_requests -= 1
req.queue_free()
if result != HTTPRequest.RESULT_SUCCESS:
_handle_disconnect()
if callback.is_valid():
callback.call(null, "HTTP error: %d" % result)
return
if response_code < 200 or response_code >= 300:
if callback.is_valid():
callback.call(null, "HTTP %d" % response_code)
return
var text := body.get_string_from_utf8()
var data = JSON.parse_string(text)
if data == null:
if callback.is_valid():
callback.call(null, "Invalid JSON")
return
if not is_connected:
is_connected = true
connected.emit()
if callback.is_valid():
callback.call(data.get("result"), data.get("error"))
func _on_get_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray, callback: Callable, req: HTTPRequest) -> void:
req.queue_free()
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
if callback.is_valid():
callback.call(null, "GET failed")
return
var text := body.get_string_from_utf8()
var data = JSON.parse_string(text)
if callback.is_valid():
callback.call(data, null)
func _check_health() -> void:
check_connection()
func _handle_disconnect() -> void:
if is_connected:
is_connected = false
disconnected.emit()