From 216ada5568c1896a1c73e5929e0fc79ce5a73b02 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 21 May 2025 15:40:22 -0500 Subject: [PATCH] attempt fix local network --- examples/chess/main.js | 86 +++++--- scripts/core/engine.js | 482 +++++++++++++++++++++++++++-------------- 2 files changed, 375 insertions(+), 193 deletions(-) diff --git a/examples/chess/main.js b/examples/chess/main.js index ffe80095..eea208d0 100644 --- a/examples/chess/main.js +++ b/examples/chess/main.js @@ -285,12 +285,16 @@ function startServer() { isMyTurn = true; updateTitle(); + console.log("Starting server with actor:", json.encode($_)); + $_.portal(e => { - console.log("Portal received contact message"); - // Reply with this actor to establish connection - console.log (json.encode($_)) - $_.send(e, $_); - console.log("Portal replied with server actor"); + console.log("Portal received contact message:", json.encode(e)); + + // The proper Misty pattern: Portal should only reply with an actor reference + // Use a clean actor object, not application data + $_.send(e, { id: $_.id }); // Send a clean actor reference + + console.log("Portal replied with server actor reference"); }, 5678); } @@ -298,17 +302,36 @@ function joinServer() { gameState = 'searching'; updateTitle(); + console.log("Client attempting to join server with client actor:", json.encode($_)); + function contact_fn(actor, reason) { - console.log("CONTACTED!", actor ? "SUCCESS" : "FAILED", reason); + console.log("Contact callback received:", actor ? "SUCCESS" : "FAILED", reason); + if (actor) { - opponent = actor; - console.log("Connection established with server, sending join request"); - - // Send a greet message with our actor object - $_.send(opponent, { - type: 'greet', - client_actor: $_ - }); + // Ensure we have a clean actor reference with just the id + if (typeof actor === 'object' && actor.id) { + // Store server actor reference for ongoing communication + opponent = { id: actor.id }; // Clean actor reference + console.log("Connection established with server actor:", json.encode(opponent)); + + // Now that we have the server actor reference, send application message + // This follows the two-phase Misty pattern: + // 1. First establish actor connection (done with contact) + // 2. Then send application messages + console.log("Sending greet message to server"); + $_.send(opponent, { + type: 'greet', + client_actor: { id: $_.id } // Send clean actor reference + }); + + // Update game state now that we're connected + gameState = 'connected'; + updateTitle(); + } else { + console.log("Received invalid actor reference:", json.encode(actor)); + gameState = 'waiting'; + updateTitle(); + } } else { console.log(`Failed to connect: ${json.encode(reason)}`); gameState = 'waiting'; @@ -316,6 +339,7 @@ function joinServer() { } } + // Initial contact phase - get actor reference only $_.contact(contact_fn, { address: "localhost", port: 5678 @@ -325,11 +349,7 @@ function joinServer() { var os = use('os') // Set up IO actor subscription -var ioguy = { - __ACTORDATA__: { - id: os.ioactor() - } -}; +var ioguy = { id: os.ioactor() }; $_.send(ioguy, { type: "subscribe", @@ -344,18 +364,22 @@ $_.receiver(e => { if (e.type === 'greet') { console.log("Server received greet from client"); // Store the client's actor object for ongoing communication - opponent = e.client_actor; - console.log("Stored client actor:", json.encode(opponent)); - gameState = 'connected'; - updateTitle(); - - // Send game_start to the client - console.log("Sending game_start to client"); - $_.send(opponent, { - type: 'game_start', - your_color: 'black' - }); - console.log("game_start message sent to client"); + if (e.client_actor && e.client_actor.id) { + opponent = { id: e.client_actor.id }; // Clean actor reference + console.log("Stored client actor:", json.encode(opponent)); + gameState = 'connected'; + updateTitle(); + + // Send game_start to the client + console.log("Sending game_start to client"); + $_.send(opponent, { + type: 'game_start', + your_color: 'black' + }); + console.log("game_start message sent to client"); + } else { + console.log("Invalid client actor in greet message:", json.encode(e)); + } } else if (e.type === 'game_start') { console.log("Game starting, I am:", e.your_color); diff --git a/scripts/core/engine.js b/scripts/core/engine.js index 6005b886..a38207b2 100644 --- a/scripts/core/engine.js +++ b/scripts/core/engine.js @@ -574,8 +574,6 @@ actor.delay = function(fn, seconds) { } actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.` - - var act = use('actor') actor[UNDERLINGS] = new Set() @@ -619,36 +617,10 @@ var nota = use('nota') var dying = false var HEADER = Symbol() +var ACTORDATA = Symbol() -var $actor = { - toString: print_actor -} - -function print_actor() { - return json.encode(this.__ACTORDATA__, 1) -} - -function create_actor(data = {}) { - var newactor = Object.create($actor) - - // Store actual address/port values on the data object - data._address = data._address || local_address - data._port = data._port || local_port - - Object.defineProperty(data, 'address', { - get: function() { return this._address || local_address }, - set: function(x) { this._address = x }, - enumerable: true - }) - - Object.defineProperty(data, 'port', { - get: function() { return this._port || local_port }, - set: function(x) { this._port = x }, - enumerable: true - }) - - newactor.__ACTORDATA__ = data - return newactor +function create_actor(id = util.guid()) { + return {id} } var $_ = create_actor() @@ -669,7 +641,7 @@ var receive_fn = undefined var greeters = {} $_.is_actor = function(actor) { - return actor.__ACTORDATA__ + return "id" in actor || ACTORDATA in actor } function peer_connection(peer) { @@ -696,12 +668,12 @@ function peer_connection(peer) { } $_.connection = function(callback, actor, config) { - var peer = peers[actor.__ACTORDATA__.id] + var peer = peers[actor.id] if (peer) { callback(peer_connection(peer)) return } - if (os.mailbox_exist(actor.__ACTORDATA__.id)) { + if (os.mailbox_exist(actor.id)) { callback({type:"local"}) return } @@ -709,15 +681,22 @@ $_.connection = function(callback, actor, config) { } $_.connection[prosperon.DOC] = "takes a callback function, an actor object, and a configuration record..." -var peers = {} -var id_address = {} -var peer_queue = new WeakMap() -var portal = undefined -var portal_fn = undefined -var local_address = undefined -var local_port = undefined +// 1) id → peer (live ENet connection) +const peer_by_id = Object.create(null) +// 2) id → {address,port} (last seen endpoint) +const id_address = Object.create(null) +// 3) address:port → peer (fast lookup for incoming events) +const peers = Object.create(null) -var service_delay = 0.01 +var peer_queue = new WeakMap() +var portal, portal_fn +var local_address, local_port + +var service_delay = 0.01 // how often to ping enet + +function route_set(id, peer){ peer_by_id[id] = peer } +function route_hint(id, address, port){ id_address[id] = {address,port} } +function route_peerString(peer){ return `${peer.address}:${peer.port}` } $_.portal = function(fn, port) { if (portal) throw new Error(`Already started a portal listening on ${portal.port}`) @@ -727,70 +706,138 @@ $_.portal = function(fn, port) { local_address = 'localhost' local_port = port portal_fn = fn - console.log(`I am now ${$_}`) + console.log(`Portal initialized with actor ID: ${$_.id}`) } $_.portal[prosperon.DOC] = "starts a public address that performs introduction services..." function handle_host(e) { switch (e.type) { case "connect": - console.log(`connected a new peer: ${e.peer.address}:${e.peer.port}`) - peers[`${e.peer.address}:${e.peer.port}`] = e.peer + // Store peer information for future routing + var key = route_peerString(e.peer) + peers[key] = e.peer + console.log(`Connected to peer: ${e.peer.address}:${e.peer.port}`) + + // Check if we have queued messages for this peer var queue = peer_queue.get(e.peer) - if (queue) { - for (var msg of queue) e.peer.send(nota.encode(msg)) - console.log(`sent ${json.encode(msg)} out of queue`) + if (queue && queue.length > 0) { + console.log(`Sending ${queue.length} queued messages to newly connected peer`) + + for (var msg of queue) { + try { + e.peer.send(nota.encode(msg)) + console.log(`Sent queued message: ${json.encode(msg)}`) + } catch (err) { + console.error(`Failed to send queued message: ${err.message}`) + } + } + + // Clear the queue after sending peer_queue.delete(e.peer) } break + case "disconnect": + // Clean up peer references + var key = route_peerString(e.peer) + console.log(`Peer disconnected: ${key}`) + + // Remove from peers map + delete peers[key] + + // Remove from queue peer_queue.delete(e.peer) - for (var id in peers) if (peers[id] === e.peer) delete peers[id] - console.log('portal got disconnect') + + // Remove from any ID-based routing + for (var id in peer_by_id) { + if (peer_by_id[id] === e.peer) { + console.log(`Removing route for actor ${id}`) + delete peer_by_id[id] + } + } break + case "receive": - var data = nota.decode(e.data) - if (data.replycc && !data.replycc.__ACTORDATA__.address) { - data.replycc.__ACTORDATA__.address = e.peer.address - data.replycc.__ACTORDATA__.port = e.peer.port - // Store in global lookup for future routing - id_address[data.replycc.__ACTORDATA__.id] = { - address: e.peer.address, - port: e.peer.port - } - } - // Also populate address/port for any actor objects in the message data - function populate_actor_addresses(obj) { - if (typeof obj !== 'object' || obj === null) return - if (obj.__ACTORDATA__ && !obj.__ACTORDATA__.address) { - obj.__ACTORDATA__.address = e.peer.address - obj.__ACTORDATA__.port = e.peer.port - // Store in global lookup for future routing - id_address[obj.__ACTORDATA__.id] = { - address: e.peer.address, - port: e.peer.port - } - } - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - populate_actor_addresses(obj[key]) + // Decode the message + try { + var msg = nota.decode(e.data) + console.log(`Received message from ${e.peer.address}:${e.peer.port}:`, json.encode(msg)) + + // Update routing information based on message contents + function touch(obj){ + if (!obj || typeof obj !== 'object') return + + if (typeof obj.id === 'string'){ + // Set up routing for this actor ID + route_set(obj.id, e.peer) + + // Add address hint if we don't have one + if (!id_address[obj.id]) { + route_hint(obj.id, e.peer.address, e.peer.port) + console.log(`Added route hint for ${obj.id}: ${e.peer.address}:${e.peer.port}`) + } + } + + // Recursively touch all properties + for (const k in obj) { + if (obj.hasOwnProperty(k)) touch(obj[k]) } } + + // Extract routing information + touch(msg) + + // Process the message + handle_message(msg) + } catch (err) { + console.error(`Failed to decode or process received message: ${err.message}`) } - if (data.data) populate_actor_addresses(data.data) - handle_message(data) break } } var contactor = undefined $_.contact = function(callback, record) { - $_.send({ - __ACTORDATA__: { - address: record.address, - port: record.port + console.log("Contact function called with:", json.encode(record)); + + // Create a pseudo-actor for the initial contact + var sendto = {} + sendto[ACTORDATA] = record + + // Ensure we send a properly formatted contact message + var contactMsg = { + type: "contact", + data: record + }; + + // Wrap the original callback to handle the actor reference properly + function wrappedCallback(response) { + console.log("Contact wrapped callback received:", json.encode(response)); + + // First param should be the actor, second param is reason/error + if (response && typeof response === 'object') { + if ('id' in response) { + // This is an actor reference, pass it directly + console.log("Identified actor reference in response"); + callback(response, null); + } else if (response.data && typeof response.data === 'object' && 'id' in response.data) { + // The actor reference is in the data field + console.log("Identified actor reference in response.data"); + callback(response.data, null); + } else { + // No actor reference found, must be an error + console.log("No actor reference found in response"); + callback(null, response); + } + } else { + // Error case or unexpected response format + console.log("Unexpected response format"); + callback(null, response); } - }, record, callback) + } + + // Send the contact message with callback for response + $_.send(sendto, contactMsg, wrappedCallback); } $_.contact[prosperon.DOC] = "The contact function sends a message to a portal..." @@ -822,7 +869,7 @@ $_.stop = function(actor) { } if (!$_.is_actor(actor)) throw new Error('Can only call stop on an actor.') - if (!underlings.has(actor.__ACTORDATA__.id)) + if (!underlings.has(actor.id)) throw new Error('Can only call stop on an underling or self.') actor_prep(actor, {type:"stop", id: prosperon.id}) @@ -842,8 +889,8 @@ $_.delay[prosperon.DOC] = "used to schedule the invocation of a function..." var couplings = new Set() $_.couple = function(actor) { - console.log(`coupled to ${actor.__ACTORDATA__.id}`) - couplings.add(actor.__ACTORDATA__.id) + console.log(`coupled to ${actor.id}`) + couplings.add(actor.id) } $_.couple[prosperon.DOC] = "causes this actor to stop when another actor stops." @@ -851,41 +898,91 @@ function actor_prep(actor, send) { message_queue.push({actor,send}); } -function actor_send(actor, message) { - if (!$_.is_actor(actor)) throw new Error(`Must send to an actor object. Attempted send to ${json.encode(actor)}`) - if (typeof message !== 'object') throw new Error('Must send an object record.') - - if (actor.__ACTORDATA__.id === prosperon.id) { - if (receive_fn) receive_fn(message.data) - return - } - if (actor.__ACTORDATA__.id && os.mailbox_exist(actor.__ACTORDATA__.id)) { - os.mailbox_push(actor.__ACTORDATA__.id, message) - return +function ensure_route(actor){ + // Extract the actor ID + const id = actor.id + + // First check if we already have a peer for this actor + if (peer_by_id[id]) { + console.log(`Found existing peer for actor ${id}`) + return peer_by_id[id] } - // Use fallback address lookup if actor doesn't have address info - if (!actor.__ACTORDATA__.address && id_address[actor.__ACTORDATA__.id]) { - Object.assign(actor.__ACTORDATA__, id_address[actor.__ACTORDATA__.id]) - } - - if (actor.__ACTORDATA__.address) { - if (actor.__ACTORDATA__.id) message.target = actor.__ACTORDATA__.id - else message.type = "contact" - var peer = peers[actor.__ACTORDATA__.address + ":" + actor.__ACTORDATA__.port] - if (!peer) { - if (!contactor && !portal) { - console.log(`creating a contactor ...`) - contactor = enet.create_host() - } - peer = (contactor || portal).connect(actor.__ACTORDATA__.address, actor.__ACTORDATA__.port) - peer_queue.set(peer, [message]) - } else { - peer.send(nota.encode(message)) + // Check if we have a hint for this actor's address + const hint = id_address[id] + if (hint) { + console.log(`Found address hint for ${id}: ${hint.address}:${hint.port}`) + + // Create host if needed + if (!contactor && !portal) { + console.log("Creating new contactor host") + contactor = enet.create_host() } + + // Connect to the peer + const host = contactor || portal + console.log(`Connecting to ${hint.address}:${hint.port} via ${contactor ? 'contactor' : 'portal'}`) + const p = host.connect(hint.address, hint.port) + + // Initialize queue for this peer + peer_queue.set(p, []) + return p + } + + // Check if we have connection data for this actor + var cnn = actor[ACTORDATA] + if (cnn) { + console.log(`Using ACTORDATA for connection: ${cnn.address}:${cnn.port}`) + + // Create host if needed + if (!contactor && !portal) { + console.log("Creating new contactor host") + contactor = enet.create_host() + } + + // Connect to the peer + const host = contactor || portal + console.log(`Connecting to ${cnn.address}:${cnn.port} via ${contactor ? 'contactor' : 'portal'}`) + var p = host.connect(cnn.address, cnn.port) + + // Initialize queue for this peer + peer_queue.set(p, []) + return p + } + + console.log(`No route found for actor ${id}`) + return null +} + +function actor_send(actor, send){ + if (!$_.is_actor(actor)) throw Error('bad actor: ' + json.encode(actor)) + if (actor.id===prosperon.id) { // message to self + console.log("actor_send: message to self") + if(receive_fn) + receive_fn(send.data); + return + } + + if (os.mailbox_exist(actor.id)){ // message to local mailbox + os.mailbox_push(actor.id, send); return } - throw new Error(`Unable to send message to actor ${json.encode(actor)}`) + + const peer = ensure_route(actor) + if (peer){ + console.log("actor_send: sending via peer route", peer.address + ":" + peer.port) + peer_queue.get(peer)?.push(send) || peer.send(nota.encode(send)) + return + } + + // fallback – forward via parent header if present + if (actor[HEADER] && actor[HEADER].replycc){ + console.log("actor_send: forwarding via parent header") + const fwd = {type:'forward', forward_to:actor.id, payload:send} + actor_send(actor[HEADER].replycc, fwd); return + } + console.error("actor_send: no route to actor", actor.id) + throw Error(`no route to actor ${actor.id}`) } // Holds all messages queued during the current turn. @@ -894,19 +991,28 @@ var message_queue = [] function send_messages() { // Attempt to flush the queued messages. If one fails, keep going anyway. var errors = [] + + // Process all queued messages while (message_queue.length > 0) { - var item = message_queue.shift() - var actor = item.actor - var send = item.send + var {actor,send} = message_queue.shift() + +// console.log("Processing queued message:", json.encode(send)) + try { actor_send(actor, send) +// console.log("Message sent successfully") } catch (err) { + console.error("Failed to send message:", err.message) errors.push(err) } } - if (errors.length > 0) { - console.error("Some messages failed to send:", errors) - for (var i of errors) console.error(i) + + // Report any send errors + if (errors.length) { + console.error(`${errors.length} messages failed to send`) + for (var i = 0; i < Math.min(errors.length, 3); i++) { + console.error(`Error ${i+1}: ${errors[i].message}`) + } } } @@ -915,15 +1021,30 @@ var replies = {} $_.send = function(actor, message, reply) { if (typeof message !== 'object') throw new Error('Message must be an object') - var send = {type:"user", data: message} + + // If message already has type, respect it, otherwise wrap it as user message + var send = message.type ? message : {type:"user", data: message} + + // Make sure contact messages have 'data' property + if (message.type === 'contact' && !send.data) { + console.log("Fixing contact message structure") + send.data = message.data || {} + } - if (actor[HEADER] && actor[HEADER].replycc) { + if (actor[HEADER] && actor[HEADER].replycc) { // in this case, it's not a true actor, but a message we're responding to + console.log("Send: responding to message with header", json.encode(actor[HEADER])) var header = actor[HEADER] if (!header.replycc || !$_.is_actor(header.replycc)) throw new Error(`Supplied actor had a return, but it's not a valid actor! ${json.encode(actor[HEADER])}`) actor = header.replycc send.return = header.reply + + // When replying to a contact message, ensure proper data structure + if (header.type === "contact" && send.type === "user") { + // Make sure we're sending the actor ID in a proper format for contact responses + send.data = send.data || $_; + } } if (reply) { @@ -931,10 +1052,12 @@ $_.send = function(actor, message, reply) { replies[id] = reply send.reply = id send.replycc = $_ + console.log("Send: added reply callback with id", id) } // Instead of sending immediately, queue it - actor_prep(actor,send); +// console.log("Send: queuing message", json.encode(send)) + actor_prep(actor, send); } $_.send[prosperon.DOC] = "sends a message to another actor..." @@ -956,9 +1079,10 @@ os.register_actor(prosperon.id, function(msg) { } }, prosperon.args.main) -$_.__ACTORDATA__.id = prosperon.id +$_.id = prosperon.id + +if (prosperon.args.overling) overling = create_actor(prosperon.args.overling) -if (prosperon.args.overling) overling = {__ACTORDATA__: {id: prosperon.args.overling}} if (prosperon.args.root) root = json.decode(prosperon.args.root) else root = $_ @@ -977,12 +1101,8 @@ actor.spawn(prosperon.args.program) function destroyself() { console.log(`Got the message to destroy self.`) dying = true - for (var i of underlings) { - var act = { - __ACTORDATA__: {id: i} - } - $_.stop(act); - } + for (var i of underlings) + $_.stop(create_actor(id)); os.destroy() } @@ -998,52 +1118,90 @@ function handle_actor_disconnect(id) { } function handle_message(msg) { +// console.log("Handling message:", json.encode(msg)); + if (msg.target) { if (msg.target !== prosperon.id) { - os.mailbox_push(msg.target, msg) - return + console.log(`Forwarding message to ${msg.target}`); + os.mailbox_push(msg.target, msg); + return; } } + switch (msg.type) { case "user": - var letter = msg.data - delete msg.data - letter[HEADER] = msg + var letter = msg.data; + delete msg.data; + letter[HEADER] = msg; + if (msg.return) { - console.log(`Received a message for the return id ${msg.return}`) - var fn = replies[msg.return] - if (!fn) throw new Error(`Could not find return function for message ${msg.return}`) - fn(letter) - delete replies[msg.return] - return + console.log(`Received a message for the return id ${msg.return}`); + var fn = replies[msg.return]; + if (!fn) { + console.error(`Could not find return function for message ${msg.return}`); + return; + } + + // Handle contact response specially - first parameter is the actor reference + if (letter && letter[HEADER] && letter[HEADER].type === "user" && letter[HEADER].return) { + // For contact responses, we need to extract the actor reference + // letter.data should contain the actor object from the portal + if (letter.data && typeof letter.data === 'object' && letter.data.id) { + console.log("Processing contact response with actor data:", json.encode(letter.data)); + fn(letter.data); // This is the actor reference + } else { + console.log("Processing contact response with letter as actor:", json.encode(letter)); + fn(letter); // Fallback to the whole message + } + } else { + fn(letter); + } + + delete replies[msg.return]; + return; } - if (receive_fn) receive_fn(letter) - break + + if (receive_fn) receive_fn(letter); + break; + case "stop": - if (msg.id !== overling.__ACTORDATA__.id) - throw new Error(`Got a message from an actor ${msg.id} to stop...`) - destroyself() - break + if (overling && msg.id !== overling.id) + throw new Error(`Got a message from an actor ${msg.id} to stop...`); + destroyself(); + break; + case "contact": if (portal_fn) { - var letter2 = msg.data - letter2[HEADER] = msg - delete msg.data - portal_fn(letter2) - } else throw new Error('Got a contact message, but no portal is established.') - break - case "stopped": - handle_actor_disconnect(msg.id) - break - case "greet": - var greeter = greeters[msg.id] - if (greeter) greeter({type: "actor_started", actor: create_actor(msg)}) + console.log("Portal received contact message"); + var letter2 = msg.data; + letter2[HEADER] = msg; + delete msg.data; + console.log("Portal handling contact message:", json.encode(letter2)); + portal_fn(letter2); + } else { + console.error('Got a contact message, but no portal is established.'); + } break; + + case "stopped": + handle_actor_disconnect(msg.id); + break; + + case "greet": + var greeter = greeters[msg.id]; + if (greeter) { + console.log("Greeting actor with id:", msg.id); + greeter({type: "actor_started", actor: create_actor(msg)}); + } + break; + case "ping": // Keep-alive ping, no action needed break; + default: - if (receive_fn) receive_fn(msg) +// console.log("Default message handler for type:", msg.type); + if (receive_fn) receive_fn(msg); break; } };