added debugger for websockets, methods, created boilerplate production for websocket methods, added reactor

This commit is contained in:
Yessiest 2024-11-17 03:51:53 +04:00
parent ff8e103274
commit 7c34ab3751
3 changed files with 302 additions and 5 deletions

93
debug.html Normal file
View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debugger</title>
<style>
html {
font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
background-color: #EFEFE6;
min-height: 100vh;
}
.big_block {
min-height: 50px !important;
}
.block {
margin: 12px;
padding: 8px;
border: 1px solid #343434;
border-radius: 0.45em;
min-height: 12px;
}
.vflex {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.hflex {
display: flex;
flex-direction: row;
justify-content: stretch;
gap: 10px;
}
.button {
margin: 4px;
padding: 4px;
border: 1px solid #343434;
border-radius: 0.45em;
text-align: center;
text-justify: center;
}
.bg_primary {
background-color: #F9F9F9;
}
</style>
</head>
<body class="vflex">
<div class="block bg_primary vflex big_block">
<h1>Welcome to Heimdall Debugger</h1>
</div>
<div class="block bg_primary vflex big_block" id="content-box">
</div>
<div class="block bg_primary vflex big_block">
<div class="block bg_primary vflex">
<textarea id="description-box"></textarea>
</div>
<div class="block bg_primary">
<button onclick="send();">Add description</button>
</div>
</div>
</body>
<script>
secure = '';
if (location.protocol === 'https:') {
secure = 's';
};
var content_box = document.getElementById('content-box');
var description_box = document.getElementById('description-box');
var socket = new WebSocket(`ws${secure}://${location.host}/socket`);
socket.addEventListener('message', (message) => {
let content = JSON.parse(message.data);
console.log(content);
let element = document.createElement('div');
element.innerText = message.data;
element.classList.add("block", "bg_primary", "vflex", "big_block");
content_box.appendChild(element);
});
function send() {
if (socket.readyState != WebSocket.OPEN) {
return;
}
socket.send(description_box.value);
};
</script>
</html>

View File

@ -26,7 +26,7 @@ module Heimdall
# @return [self]
def new(*args, **params)
object = super(*args, **params)
@uuids[object.uuid] = object
@uuids[object.uuid] = WeakRef.new(object)
@last = object.uuid
add_foreign(object.foreign_ids, object)
object
@ -75,7 +75,7 @@ module Heimdall
# @return [void]
def add_foreign(foreign, obj)
foreign.each do |f_id|
@foreign_ids[check_foreign(f_id)] = obj
@foreign_ids[check_foreign(f_id)] = WeakRef.new(obj)
end
end
@ -137,7 +137,7 @@ module Heimdall
# Add a listener to the PubSub
# @param listener [#call]
# @return [void]
def listen(listener)
def listen(&listener)
@listeners.append(listener)
end
@ -159,8 +159,16 @@ module Heimdall
class Message < UUIDObject
def initialize(datahash, **params)
@id = datahash["id"]
@from = UUIDObject.get(datahash["from"])
@to = UUIDObject.get(datahash["to"])
@from = if datahash["from"]
UUIDObject.get(datahash["from"])
elsif datahash["from_foreign"]
UUIDObject.get_foreign(datahash["from_foreign"])
end
@to = if datahash["to"]
UUIDObject.get(datahash["to"])
elsif datahash["to_foreign"]
UUIDObject.get_foreign(datahash["to_foreign"])
end
@content = datahash["content"]
# @reply_to = datahash["reply_to"] # TODO: make this make sense
@attachments = datahash["attachments"]
@ -168,6 +176,21 @@ module Heimdall
super(**params)
end
# Convert message data to a JSON struct
# @return [String] JSON struct
def to_struct
JSON.dump({
"id" => @id,
"uuid" => @uuid,
"from" => @from.uuid,
"to" => @to.uuid,
"from_foreign" => @from.foreing_ids,
"to_foreign" => @to.foreign_ids,
"content" => @content,
"attachments" => @attachments
})
end
attr_reader :from, :to, :content, :reply_to, :attachments
end
@ -189,6 +212,7 @@ module Heimdall
def to_struct
JSON.dump({
"id" => @id,
"uuid" => @uuid,
"username" => @username,
"nickname" => @nickname,
"avatar" => @avatar
@ -201,6 +225,7 @@ module Heimdall
DEFAULT_AVATAR = ""
def initialize(datahash, **params)
@pubsub = PubSub.new
@id = datahash["id"]
@name = datahash["name"] or @id
@avatar = datahash["avatar"] || self.class::DEFAULT_AVATAR
@ -213,10 +238,13 @@ module Heimdall
def to_struct
JSON.dump({
"id" => @id,
"uuid" => @uuid,
"name" => @name,
"avatar" => @avatar
})
end
attr_reader :pubsub
end
VERSION = "1.0"

176
server.ru
View File

@ -3,8 +3,171 @@
require_relative '.env'
require_relative 'proto'
require 'landline'
require 'landline/extensions/websocket'
require 'json'
# Class for handling websocket connection requests
class Reactor
def initialize
@sockets = []
@events = {}
end
# Attach websocket to reactor
# @param socket [Landline::WebSocket::WSockWrapper]
def attach(socket)
@sockets.append(socket)
end
# Push data to all attached sockets
# @param data [Array,Hash]
def push(data)
@sockets.each { |x| x.write(data) }
end
# Handle websocket event
# @param event [String]
# @param block [#call]
def on(event, &block)
@events[event] ||= []
@events[event].append(block)
end
# Main loop
def listen_loop
loop do
readable = IO.select(@sockets, nil, nil, 5)&.fetch(0, nil)
readable&.each do |socket|
output = socket.read
if output
_respond(socket, output)
else
@sockets.delete(socket)
end
end
end
end
private
def _respond(socket, output)
data = JSON.parse(output.data)
event_name = data['event']
@events[event_name]&.each do |callback|
callback.call(socket,
*(data['args'] || []),
**(data['params'] || {}))
end
rescue JSON::ParserError => e
socket.write(JSON.dump({ error: e.message, code: 400 }))
end
end
# Class acting as session-persistent object storage
class ObjectStorage
def initialize
@channels = {}
@users = {}
end
# Add channel to object storage
# @param channel [Heimdall::Channel]
def add_channel(channel)
@channels[channel.uuid] = channel
end
# Add user to object storage
# @param user [Heimdall::User]
def add_user(user)
@users[user.uuid] = user
end
attr_reader :channels, :users
end
OBJECT_STORAGE = ObjectStorage.new
# Helper methods for building reactor listeners
module ReactorHelpers
# Create a block for foreign and uuid indexing of a class
# @param cls [Class]
# @param block [#call]
# @param reactor [Reactor]
def self.foreign_and_uuid(method_name, reactor, cls, &block)
reactor.on "#{cls.name.downcase}.#{method_name}.foreign" do |socket, f_id,
*args, **vars|
obj = cls.get_foreign(f_id)
block.call(socket, obj, *args, **vars)
end
reactor.on "#{cls.name.downcase}.#{method_name}.uuid" do |socket, uuid,
*args, **vars|
obj = cls.get_uuid(uuid)
block.call(socket, obj, *args, **vars)
end
end
# Boilerplate get/delete functions for a class
# @param reactor [Reactor]
# @param cls [Class]
def self.foreign_and_uuid_boilerplate(reactor, cls)
cname = cls.name.gsub(/\A.+::/, "").downcase
reactor.on "#{cname}.get.foreign" do |socket, f_id|
obj = cls.get_foreign(f_id)
socket.write(obj || JSON.dump({ error: "#{cname} not found", code: 400 }))
end
reactor.on "#{cname}.get.uuid" do |socket, uuid|
obj = cls.get_uuid(uuid)
socket.write(obj || JSON.dump({ error: "#{cname} not found", code: 400 }))
end
reactor.on "#{cname}.delete.foreign" do |socket, f_id|
status = cls.delete_foreign(f_id)
socket.write(JSON.dump(if status
{ code: 200 }
else
{ error: "#{cname} not found", code: 400 }
end))
end
reactor.on "#{cname}.delete.uuid" do |socket, f_id|
status = cls.delete_uuid(f_id)
socket.write(JSON.dump(if status
{ code: 200 }
else
{ error: "#{cname} not found", code: 400 }
end))
end
end
end
# WebSocket handling
WEBSOCKET_LISTENER = Reactor.new.tap do |react|
react.on "version" do |socket|
socket.write(JSON.dump({
"version" => Heimdall::VERSION
}))
end
# --- USER ---
ReactorHelpers.foreign_and_uuid_boilerplate(react, Heimdall::User)
# --- CHANNEL ---
ReactorHelpers.foreign_and_uuid_boilerplate(react, Heimdall::Channel)
ReactorHelpers.foreign_and_uuid("listen", react,
Heimdall::Channel) do |socket, channel|
channel.pubsub.listen do |msg|
socket.write(msg)
end
socket.write(JSON.dump({ code: 200 }))
end
ReactorHelpers.foreign_and_uuid("send", react,
Heimdall::Channel) do |socket, channel,
**message_args|
msg = Heimdall::Message.new(message_args)
channel.pubsub.push(msg.to_struct)
socket.write(JSON.dump({ code: 200,
msg_uuid: msg.uuid }))
end
end
WEBSOCKET_LISTENER_LOOP = Thread.new { WEBSOCKET_LISTENER.listen_loop }
# Primary server class
class HeimdallServer < Landline::App
before do
@ -48,6 +211,13 @@ class HeimdallServer < Landline::App
})
end
root __dir__
serve "*.html"
get "/debug" do
jump "/debug.html"
end
pipeline do |request, &output|
output.call(request)
rescue Heimdall::ProtoError => e
@ -94,6 +264,7 @@ class HeimdallServer < Landline::App
"nickname" => [String, NilClass],
"avatar" => [String, NilClass])
new_user = Heimdall::User.new(@data)
OBJECT_STORAGE.add_user(new_user)
JSON.dump({ "uuid": new_user.uuid })
end
@ -106,12 +277,17 @@ class HeimdallServer < Landline::App
"name" => String,
"avatar" => [String, NilClass])
new_channel = Heimdall::Channel.new(@data)
OBJECT_STORAGE.add_channel(new_channel)
JSON.dump({ "uuid": new_channel.uuid })
end
instance_exec(Heimdall::Channel, &read_delete)
end
websocket "/socket" do |socket|
WEBSOCKET_LISTENER.attach(socket)
end
handle do |status, backtrace: nil, **_|
backtrace ||= [Landline::Util::HTTP_STATUS[status]]
[status,