diff options
Diffstat (limited to 'src/hub.moon')
| -rw-r--r-- | src/hub.moon | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/src/hub.moon b/src/hub.moon new file mode 100644 index 0000000..48fcec3 --- /dev/null +++ b/src/hub.moon @@ -0,0 +1,184 @@ +-- Hub-and-spoke networking hub +-- Manages client connections and routes messages between them + +net = require "net" +log = require "log" +world = require "world" + +-- Register message types that clients sends to the hub +-- Client -> Hub: RequestEntities has no payload (empty table is fine) +net.register_message("RequestEntities", {}) + +-- Envelope used for all logical messages routed through the hub +net.register_message("routed_message", { + required: { + message_type: "string" + from: "string" + } + optional: { + data: "table" + } +}) + +net.register_message("Join", { + required: { + name: "string" + } +}) + +-- Hub -> Client: RespondEntities sends the current entities list (array) +net.register_message("RespondEntities", { + optional: { + entities: "table" + } +}) + +-- Hub -> Client: CreateEntity announces a new entity for a given peerid +net.register_message("CreateEntity", { + required: { + peerid: "string" + } +}) + +class Hub + new: => + @peer = nil + @clients = {} -- client_id -> connection + @client_names = {} -- client_id -> name (optional) + @initialized = false + @routes = {} + @on_connect_callbacks = {} + @on_disconnect_callbacks = {} + + initialize: => + if @initialized + return + --@attempts = 0 + --create_peer = () -> + --@peer = net.Peer! + --fail_peer_creation = () -> + --@attempts += 1 + --while @attempts < 4 + --xpcall(create_peer, fail_peer_creation) + --log.info("Creating peer attempt " .. @attempts .. " / 4", {"net","server"}) + --if @peer == nil + --error("Failed to create peer 4 times...") + @peer = net.Peer! + log.info("Hub peer created: #{@peer.id}", {"server", "net"}) + -- Listen for incoming connections + @peer\on "connection", (peer, message) -> + client_id = message.data.dest -- the other side of the connection? + log.info("Client connected: " .. tostring(peer) .. ":" .. tostring(message), {"server", "net"}) + conn = message.data + @clients[client_id] = conn + -- Set up connection handlers + -- When the connection receives data, the callback is invoked with + -- (connection, message_array), where message_array = { msgname, msgdata }. + -- We only care about the logical client id and the logical message array, + -- so forward just those to handle_message. This keeps the JS bridge + -- signature Hub.handle_message(from_client, msg) where msg is the + -- array { msg_type, msg_data }. + conn\on "data", (conn, message) -> + log.info("On data got arguments:" .. tostring(conn) .. "," .. tostring(message), {"server","net"}) + @handle_message(client_id, message) + + conn\on "close", () -> + @handle_disconnect(client_id) + + -- Notify connection callbacks + for callback in *@on_connect_callbacks + callback(client_id) + + @initialized = true + require("server.init") + + handle_message: (from_client, message) => + -- message is the array {msg_type, msg_data} that was sent over the wire + log.info("Hub received message: #{message} from #{from_client}", {"net", "server"}) + msg_type = message[1] + msg_data = message[2] + + -- Build routed_message-style envelope for validation + routed = { + message_type: msg_type + from: from_client + data: msg_data + } + + -- Validate envelope + ok, err = pcall(net.validate, "routed_message", routed) + if not ok + log.error("Invalid routed_message from " .. tostring(from_client) .. ": " .. tostring(routed), {"net", "server"}) + return + + -- Validate payload for the specific message type + ok, err = pcall(net.validate, msg_type, msg_data) + if not ok + log.error("Invalid " .. tostring(msg_type) .. " payload from " .. tostring(from_client) .. ": " .. tostring(err), {"net", "server"}) + return + + if msg_type == "Join" + log.info("Hub handling Join from " .. tostring(from_client) .. " with data=" .. tostring(msg_data), {"net", "server", "debug"}) + world.domain = "server" + if @routes[msg_type] + -- routes[msg_type] is a flat map id -> callback(from_client, data) + for _, callback in pairs(@routes[msg_type]) + callback(from_client, msg_data) + else + log.warn("No routes for " .. tostring(msg_type), {"net","server"}) + world.domain = "client" + + listen: (message_type, id, callback) => + @routes[message_type] = @routes[message_type] or {} + id = id or "default" + @routes[message_type][id] = callback + log.info("Hub listening for #{message_type}", {"net", "server"}) + id + + defen: (message_type, id) => + @routes[message_type][id] = nil + + send: (client_id, message_type, data) => + conn = @clients[client_id] + if not conn + log.warn("Cannot send to unknown client: #{client_id}", {"server", "net"}) + return + log.info("Hub sending #{message_type} to #{client_id}", {"net", "server"}) + conn\send({message_type, data}) + + broadcast: (message_type, data) => + nclients = 0 + for _, _ in pairs(@clients) + nclients += 1 + log.info("Hub broadcasting #{message_type} to " .. tostring(nclients) .. " clients", {"net", "server"}) + for clientid, conn in pairs(@clients) + conn\send({message_type, data}) + + handle_disconnect: (client_id) => + log.info("Client disconnected: #{client_id}", {"server", "net"}) + @clients[client_id] = nil + @client_names[client_id] = nil + + -- Notify disconnect callbacks + for callback in *@on_disconnect_callbacks + callback(client_id) + + on_connect: (callback) => + table.insert(@on_connect_callbacks, callback) + + on_disconnect: (callback) => + table.insert(@on_disconnect_callbacks, callback) + + get_clients: => + clients = {} + for id, conn in pairs @clients + table.insert(clients, { + id: id + name: @client_names[id] + }) + clients + + pump: => + net.pump! + +{:Hub} |
