talkpal-frontend/src/libs/socketio_client.gd
DarkSlein c008e89412 Added WebSockets
Fixed HTTP request
2025-02-22 21:15:18 +03:00

248 lines
7.3 KiB
GDScript

class_name SocketIOClient extends Node
enum EngineIOPacketType {
open = 0,
close = 1,
ping = 2,
pong = 3,
message = 4,
upgrade = 5,
noop = 6,
}
enum SocketIOPacketType {
CONNECT = 0,
DISCONNECT = 1,
EVENT = 2,
ACK = 3,
CONNECT_ERROR = 4,
BINARY_EVENT = 5,
BINARY_ACK = 6,
}
enum ConnectionState {
DISCONNECTED,
CONNECTED,
RECONNECTING,
}
var _url: String
var _client: WebSocketPeer = WebSocketPeer.new()
var _sid: String
var _pingTimeout: int = 0
var _pingInterval: int = 0
var _auth: Variant = null
var _connection_state: ConnectionState = ConnectionState.DISCONNECTED
var _reconnect_timer: Timer = null
# triggered when engine.io connection is established
signal on_engine_connected(sid: String)
# triggered when engine.io connection is closed
signal on_engine_disconnected(code: int, reason: String)
# triggered when engine.io message is received
signal on_engine_message(payload: String)
# triggered when socket.io connection is established
signal on_connect(payload: Variant, name_space: String, error: bool)
# triggered when socket.io connection is closed
signal on_disconnect(name_space: String)
# triggered when socket.io event is received
signal on_event(event_name: String, payload: Variant, name_space: String)
# triggered when lost connection in not-clean way (i.e. service shut down)
# and now trying to reconnect. When re-connects successfully,
# the on_reconnected signal is emitted, instead of on_connect
signal on_connection_lost
# triggered when connects again after losing connection
# it's alternative to on_connect signal, but only emitted
# after automatically re-connecting with socket.io server
signal on_reconnected(payload: Variant, name_space: String, error: bool)
func _init(url: String, auth: Variant=null):
_auth = auth
url = _preprocess_url(url)
_url = "%s?EIO=4&transport=websocket" % url
func _preprocess_url(url: String) -> String:
if not url.ends_with("/"):
url = url + "/"
if url.begins_with("https"):
url = "wss" + url.erase(0, len("https"))
elif url.begins_with("http"):
url = "ws" + url.erase(0, len("http"))
return url
func _ready():
_client.connect_to_url(_url)
func _process(_delta):
_client.poll()
var state = _client.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
while _client.get_available_packet_count():
var packet = _client.get_packet()
var packetString = packet.get_string_from_utf8()
if len(packetString) > 0:
_engineio_decode_packet(packetString)
# TODO: handle binary data?
elif state == WebSocketPeer.STATE_CLOSED:
set_process(false)
var code = _client.get_close_code()
var reason = _client.get_close_reason()
if code == -1:
# -1 is not-clean disconnect (i.e. Service shut down)
# we should try to reconnect
_connection_state = ConnectionState.RECONNECTING
_reconnect_timer = Timer.new()
_reconnect_timer.wait_time = 1.0
_reconnect_timer.timeout.connect(_on_reconnect_timer_timeout)
_reconnect_timer.autostart = true
add_child(_reconnect_timer)
on_connection_lost.emit()
else:
_connection_state = ConnectionState.DISCONNECTED
on_engine_disconnected.emit(code, reason)
func _on_reconnect_timer_timeout():
_client.poll()
var state = _client.get_ready_state()
if state == WebSocketPeer.STATE_CLOSED:
_client.connect_to_url(_url)
else:
set_process(true)
_reconnect_timer.queue_free()
func _exit_tree():
if _connection_state == ConnectionState.CONNECTED:
_engineio_send_packet(EngineIOPacketType.close)
_client.close()
func _engineio_decode_packet(packet: String):
var packetType = int(packet.substr(0, 1))
var packetPayload = packet.substr(1)
match packetType:
EngineIOPacketType.open:
var json = JSON.new()
json.parse(packetPayload)
_sid = json.data["sid"]
_pingTimeout = int(json.data["pingTimeout"])
_pingInterval = int(json.data["pingInterval"])
on_engine_connected.emit(_sid)
EngineIOPacketType.ping:
_engineio_send_packet(EngineIOPacketType.pong)
EngineIOPacketType.message:
_socketio_parse_packet(packetPayload)
on_engine_message.emit(packetPayload)
func _engineio_send_packet(type: EngineIOPacketType, payload: String=""):
if len(payload) == 0:
_client.send_text("%d" % type)
else:
_client.send_text("%d%s" % [type, payload])
func _socketio_parse_packet(payload: String):
var packetType = int(payload.substr(0, 1))
payload = payload.substr(1)
var regex = RegEx.new()
regex.compile("(\\d+)-")
var regexMatch = regex.search(payload)
if regexMatch and regexMatch.get_start() == 0:
payload = payload.substr(regexMatch.get_end())
push_error("Binary data payload not supported!")
var name_space = "/"
regex.compile("(\\w),")
regexMatch = regex.search(payload)
if regexMatch and regexMatch.get_start() == 0:
payload = payload.substr(regexMatch.get_end())
name_space = regexMatch.get_string(1)
# var ack_id = null
regex.compile("(\\d+)")
regexMatch = regex.search(payload)
if regexMatch and regexMatch.get_start() == 0:
payload = payload.substr(regexMatch.get_end())
push_warning("Ignoring acknowledge ID!")
var data = null
if len(payload) > 0:
var json = JSON.new()
if json.parse(payload) == OK:
data = json.data
match packetType:
SocketIOPacketType.CONNECT:
if _connection_state == ConnectionState.RECONNECTING:
_connection_state = ConnectionState.CONNECTED
on_reconnected.emit(data, name_space, false)
else:
_connection_state = ConnectionState.CONNECTED
on_connect.emit(data, name_space, false)
SocketIOPacketType.CONNECT_ERROR:
if _connection_state == ConnectionState.RECONNECTING:
_connection_state = ConnectionState.CONNECTED
on_reconnected.emit(data, name_space, true)
else:
_connection_state = ConnectionState.CONNECTED
on_connect.emit(data, name_space, true)
SocketIOPacketType.EVENT:
if typeof(data) != TYPE_ARRAY:
push_error("Invalid socketio event format!")
var eventName = data[0]
var eventData = data[1] if len(data) > 1 else null
on_event.emit(eventName, eventData, name_space)
func _socketio_send_packet(type: SocketIOPacketType, name_space: String, data: Variant=null, binaryData: Array[PackedByteArray]=[], ack_id: Variant=null):
var payload = "%d" % type
if binaryData.size() > 0:
payload += "%d-" % binaryData.size()
if name_space != "/":
payload += "%s," % name_space
if ack_id != null:
payload += "%d" % ack_id
if data != null:
payload += "%s" % JSON.stringify(data)
_engineio_send_packet(EngineIOPacketType.message, payload)
for binary in binaryData:
_client.put_packet(binary)
# connect to socket.io server by namespace
func socketio_connect(name_space: String="/"):
_socketio_send_packet(SocketIOPacketType.CONNECT, name_space, _auth)
# disconnect from socket.io server by namespace
func socketio_disconnect(name_space: String="/"):
if _connection_state == ConnectionState.CONNECTED:
# We should ONLY send disconnect packet when we're connected
_socketio_send_packet(SocketIOPacketType.DISCONNECT, name_space)
on_disconnect.emit(name_space)
# send event to socket.io server by namespace
func socketio_send(event_name: String, payload: Variant=null, name_space: String="/"):
if payload == null:
_socketio_send_packet(SocketIOPacketType.EVENT, name_space, [event_name])
else:
_socketio_send_packet(SocketIOPacketType.EVENT, name_space, [event_name, payload])
func get_connection_state() -> ConnectionState:
return _connection_state