diff --git a/meson.build b/meson.build index 4d51ced3..d34c99ba 100644 --- a/meson.build +++ b/meson.build @@ -230,7 +230,8 @@ copy_tests = custom_target( tests = [ 'spawn_actor', 'empty', - 'nota' + 'nota', + 'enet' ] foreach file : tests diff --git a/scripts/modules/enet.js b/scripts/modules/enet.js new file mode 100644 index 00000000..13603497 --- /dev/null +++ b/scripts/modules/enet.js @@ -0,0 +1,151 @@ +// enet.js doc (updated) +var enet = this; + +//------------------------------------------------ +// Top-level ENet functions +//------------------------------------------------ + +enet.initialize[prosperon.DOC] = ` +Initialize the ENet library. Must be called before using any ENet functionality. +Throws an error if initialization fails. + +:return: None +`; + +enet.deinitialize[prosperon.DOC] = ` +Deinitialize the ENet library, cleaning up all resources. Call this when you no longer +need any ENet functionality. + +:return: None +`; + +enet.create_host[prosperon.DOC] = ` +Create an ENet host for either a client-like unbound host or a server bound to a specific +address and port: + +- If no argument is provided, creates an unbound "client-like" host with default settings + (maximum 32 peers, 2 channels, unlimited bandwidth). +- If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to + that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth. + +Throws an error if host creation fails for any reason. + +:param address: (optional) A string in 'ip:port' format to bind the host (server), or + omit to create an unbound client-like host. +:return: An ENetHost object. +`; + +//------------------------------------------------ +// ENetHost methods +//------------------------------------------------ + +var enet_host = prosperon.c_types.enet_host; + +enet_host.service[prosperon.DOC] = ` +Poll for and process any available network events (connect, receive, disconnect, or none) +from this host, calling the provided callback for each event. This function loops until +no more events are available in the current timeframe. + +Event object properties: +- type: String, one of "connect", "receive", "disconnect", or "none". +- peer: (present if type = "connect") The ENetPeer object for the new connection. +- channelID: (present if type = "receive") The channel on which the data was received. +- data: (present if type = "receive") The received data as a *plain JavaScript object*. + If the JSON parse fails or the data isn't an object, a JavaScript error is thrown. + +:param callback: A function called once for each available event, receiving an event + object as its single argument. +:param timeout: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking). +:return: None +`; + +enet_host.connect[prosperon.DOC] = ` +Initiate a connection from this host to a remote server. Throws an error if the +connection cannot be started. + +:param host: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1"). +:param port: The port number to connect to. +:return: An ENetPeer object representing the connection. +`; + +enet_host.flush[prosperon.DOC] = ` +Flush all pending outgoing packets for this host immediately. + +:return: None +`; + +enet_host.broadcast[prosperon.DOC] = ` +Broadcast a JavaScript object to all connected peers on channel 0. The object is +serialized to JSON, and the packet is sent reliably. Throws an error if serialization fails. + +:param data: A JavaScript object to broadcast to all peers. +:return: None +`; + +//------------------------------------------------ +// ENetPeer methods +//------------------------------------------------ + +var enet_peer = prosperon.c_types.enet_peer; + +enet_peer.send[prosperon.DOC] = ` +Send a JavaScript object to this peer on channel 0. The object is serialized to JSON and +sent reliably. Throws an error if serialization fails. + +:param data: A JavaScript object to send. +:return: None +`; + +enet_peer.disconnect[prosperon.DOC] = ` +Request a graceful disconnection from this peer. The connection will close after +pending data is sent. + +:return: None +`; + +enet_peer.disconnect_now[prosperon.DOC] = ` +Immediately terminate the connection to this peer, discarding any pending data. + +:return: None +`; + +enet_peer.disconnect_later[prosperon.DOC] = ` +Request a disconnection from this peer after all queued packets are sent. + +:return: None +`; + +enet_peer.reset[prosperon.DOC] = ` +Reset this peer's connection, immediately dropping it and clearing its internal state. + +:return: None +`; + +enet_peer.ping[prosperon.DOC] = ` +Send a ping request to this peer to measure latency. + +:return: None +`; + +enet_peer.throttle_configure[prosperon.DOC] = ` +Configure the throttling behavior for this peer, controlling how ENet adjusts its sending +rate based on packet loss or congestion. + +:param interval: The interval (ms) between throttle adjustments. +:param acceleration: The factor to increase sending speed when conditions improve. +:param deceleration: The factor to decrease sending speed when conditions worsen. +:return: None +`; + +enet_peer.timeout[prosperon.DOC] = ` +Set timeout parameters for this peer, determining how long ENet waits before considering +the connection lost. + +:param timeout_limit: The total time (ms) before the peer is disconnected. +:param timeout_min: The minimum timeout (ms) used for each timeout attempt. +:param timeout_max: The maximum timeout (ms) used for each timeout attempt. +:return: None +`; + +// Return the enet object. +return enet; diff --git a/source/qjs_enet.c b/source/qjs_enet.c index 3c11f17c..f75a21aa 100644 --- a/source/qjs_enet.c +++ b/source/qjs_enet.c @@ -1,4 +1,4 @@ -// enet_qjs.c +// qjs_enet.c #include "quickjs.h" #include #include @@ -9,6 +9,7 @@ static JSClassID enet_host_id; static JSClassID enet_peer_class_id; +/* Finalizers */ static void js_enet_host_finalizer(JSRuntime *rt, JSValue val) { ENetHost *host = JS_GetOpaque(val, enet_host_id); if (host) { @@ -18,278 +19,394 @@ static void js_enet_host_finalizer(JSRuntime *rt, JSValue val) { static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val) { ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id); - if (peer) { - // No explicit cleanup needed for ENetPeer - } + // No explicit cleanup needed for ENetPeer itself + (void)peer; } -static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +/* ENet init/deinit */ +static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { if (enet_initialize() != 0) { - return JS_ThrowInternalError(ctx, "An error occurred while initializing ENet."); + return JS_ThrowInternalError(ctx, "Error initializing ENet."); } return JS_UNDEFINED; } -static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { enet_deinitialize(); return JS_UNDEFINED; } -static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +/* Host creation */ +static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetHost *host; ENetAddress address; JSValue obj; + if (argc < 1) { + // Create client-like host, unbound host = enet_host_create(NULL, 32, 2, 0, 0); + if (!host) { + return JS_ThrowInternalError(ctx, "Failed to create ENet host (null address)."); + } goto RET; } + // If arg is provided, interpret as "ip:port" for server const char *address_str = JS_ToCString(ctx, argv[0]); - if (!address_str) - return JS_EXCEPTION; - + if (!address_str) { + return JS_EXCEPTION; // memory or conversion error + } char ip[64]; int port; if (sscanf(address_str, "%63[^:]:%d", ip, &port) != 2) { JS_FreeCString(ctx, address_str); - return JS_ThrowTypeError(ctx, "Invalid address format. Expected format: 'ip:port'"); + return JS_ThrowTypeError(ctx, "Invalid address format. Expected 'ip:port'."); } - JS_FreeCString(ctx, address_str); - int err; - if ((err = enet_address_set_host_ip(&address, ip)) != 0) { + int err = enet_address_set_host_ip(&address, ip); + if (err != 0) { return JS_ThrowInternalError(ctx, "Failed to set host IP from %s. Error %d.", ip, err); } address.port = port; - host = enet_host_create(&address, 32, 2, 0, 0); // server host with max 32 clients + // Create server host with max 32 clients, 2 channels + host = enet_host_create(&address, 32, 2, 0, 0); if (!host) { return JS_ThrowInternalError(ctx, "Failed to create ENet host."); } - RET: +RET: obj = JS_NewObjectClass(ctx, enet_host_id); if (JS_IsException(obj)) { enet_host_destroy(host); return obj; } - JS_SetOpaque(obj, host); return obj; } -static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +/* Host service: poll for events */ +static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetHost *host = JS_GetOpaque(this_val, enet_host_id); - if (!host) + if (!host) { return JS_EXCEPTION; - - if (argc < 1 || !JS_IsFunction(ctx, argv[0])) { - return JS_ThrowTypeError(ctx, "Expected a callback function as the first argument"); } + // Expect a callback function + if (argc < 1 || !JS_IsFunction(ctx, argv[0])) { + return JS_ThrowTypeError(ctx, "Expected a callback function as first argument."); + } JSValue callback = argv[0]; JS_DupValue(ctx, callback); - ENetEvent event; - int timeout = 1000; // 1 second timeout by default - + // Optional timeout + int timeout = 0; if (argc > 1) { JS_ToInt32(ctx, &timeout, argv[1]); } + ENetEvent event; while (enet_host_service(host, &event, timeout) > 0) { JSValue event_obj = JS_NewObject(ctx); + switch (event.type) { - case ENET_EVENT_TYPE_CONNECT: { - JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect")); - JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id); - if (JS_IsException(peer_obj)) - return peer_obj; - JS_SetOpaque(peer_obj, event.peer); - JS_SetPropertyStr(ctx, event_obj, "peer", peer_obj); - break; - } - case ENET_EVENT_TYPE_RECEIVE: { - JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive")); - JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID)); - JSValue packet_data = JS_ParseJSON(ctx, (const char *)event.packet->data, event.packet->dataLength, "packet"); - if (JS_IsException(packet_data)) { - packet_data = JS_NULL; - } - JS_SetPropertyStr(ctx, event_obj, "data", packet_data); - enet_packet_destroy(event.packet); - break; - } - case ENET_EVENT_TYPE_DISCONNECT: { - JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect")); - break; + case ENET_EVENT_TYPE_CONNECT: { + JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect")); + JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id); + if (JS_IsException(peer_obj)) { + JS_FreeValue(ctx, event_obj); + JS_FreeValue(ctx, callback); + return peer_obj; } + JS_SetOpaque(peer_obj, event.peer); + JS_SetPropertyStr(ctx, event_obj, "peer", peer_obj); + break; } + case ENET_EVENT_TYPE_RECEIVE: { + JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive")); + JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID)); + char *tmp = js_mallocz(ctx, event.packet->dataLength+1); + memcpy(tmp, event.packet->data, event.packet->dataLength); + tmp[event.packet->dataLength] = '\0'; + + // We expect strictly a JSON object + JSValue packet_data = JS_ParseJSON(ctx, + tmp, + event.packet->dataLength, + ""); + + js_free(ctx,tmp); + + if (JS_IsException(packet_data)) { + // Malformed JSON -> throw error, abort + printf("INVALID JSON!\n"); + enet_packet_destroy(event.packet); + JS_FreeValue(ctx, event_obj); + JS_FreeValue(ctx, callback); + return JS_ThrowTypeError(ctx, "Received invalid JSON (parse error)."); + } + + if (!JS_IsObject(packet_data)) { + // It might be a string/number/array/... -> we want only a plain object + JS_FreeValue(ctx, event_obj); + JS_FreeValue(ctx, callback); + JS_FreeValue(ctx, packet_data); + enet_packet_destroy(event.packet); + return JS_ThrowTypeError(ctx, + "Received data is not an object (must send a plain object)."); + } + + JS_SetPropertyStr(ctx, event_obj, "data", packet_data); + enet_packet_destroy(event.packet); + break; + } + case ENET_EVENT_TYPE_DISCONNECT: + JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect")); + break; + case ENET_EVENT_TYPE_NONE: + JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "none")); + break; + } + + // Invoke callback JS_Call(ctx, callback, JS_UNDEFINED, 1, &event_obj); JS_FreeValue(ctx, event_obj); } JS_FreeValue(ctx, callback); return JS_UNDEFINED; -} +} -static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +/* Host connect: client -> connect to server */ +static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetHost *host = JS_GetOpaque(this_val, enet_host_id); - if (!host) + if (!host) { return JS_EXCEPTION; + } - if (argc < 2) - return JS_ThrowTypeError(ctx, "Expected at least 2 arguments (host, port)"); + if (argc < 2) { + return JS_ThrowTypeError(ctx, "Expected 2 arguments: hostname, port."); + } - const char *host_name = JS_ToCString(ctx, argv[0]); + const char *hostname = JS_ToCString(ctx, argv[0]); + if (!hostname) { + return JS_EXCEPTION; // out of memory or conversion error + } int port; JS_ToInt32(ctx, &port, argv[1]); ENetAddress address; - enet_address_set_host(&address, host_name); + enet_address_set_host(&address, hostname); + JS_FreeCString(ctx, hostname); address.port = port; - JS_FreeCString(ctx, host_name); ENetPeer *peer = enet_host_connect(host, &address, 2, 0); - if (!peer) + if (!peer) { return JS_ThrowInternalError(ctx, "Failed to initiate connection."); + } JSValue peer_obj = JS_NewObjectClass(ctx, enet_peer_class_id); - if (JS_IsException(peer_obj)) + if (JS_IsException(peer_obj)) { return peer_obj; + } JS_SetOpaque(peer_obj, peer); return peer_obj; } -static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +/* Flush queued packets */ +static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetHost *host = JS_GetOpaque(this_val, enet_host_id); - if (!host) + if (!host) { return JS_EXCEPTION; - + } enet_host_flush(host); return JS_UNDEFINED; } -static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +/* Broadcast a plain object */ +static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetHost *host = JS_GetOpaque(this_val, enet_host_id); - if (!host) + if (!host) { return JS_EXCEPTION; + } - if (argc < 1) - return JS_ThrowTypeError(ctx, "Expected at least 1 argument (data)"); + if (argc < 1) { + return JS_ThrowTypeError(ctx, "Expected an object to broadcast."); + } + // Must be a JavaScript object + if (!JS_IsObject(argv[0])) { + return JS_ThrowTypeError(ctx, + "broadcast() only accepts a plain JS object, not strings/numbers."); + } + + // JSON.stringify the object + JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL); + if (JS_IsException(json_data)) { + return JS_ThrowTypeError(ctx, + "Failed to stringify object (circular ref or non-serializable)."); + } size_t data_len; - const char *data = JS_ToCStringLen(ctx, &data_len, argv[0]); - if (!data) - return JS_EXCEPTION; + const char *data_str = JS_ToCStringLen(ctx, &data_len, json_data); + JS_FreeValue(ctx, json_data); - ENetPacket *packet = enet_packet_create(data, data_len, ENET_PACKET_FLAG_RELIABLE); - JS_FreeCString(ctx, data); + if (!data_str) { + return JS_EXCEPTION; // out of memory + } + ENetPacket *packet = enet_packet_create(data_str, data_len, ENET_PACKET_FLAG_RELIABLE); + JS_FreeCString(ctx, data_str); + + if (!packet) { + return JS_ThrowInternalError(ctx, "Failed to create ENet packet."); + } enet_host_broadcast(host, 0, packet); return JS_UNDEFINED; } -static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +/* Peer-level operations */ +static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); - if (!peer) + if (!peer) { return JS_EXCEPTION; - + } enet_peer_disconnect(peer, 0); return JS_UNDEFINED; } -static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +/* Peer send must only accept an object */ +static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); - if (!peer) + if (!peer) { return JS_EXCEPTION; - - if (argc < 1 || !JS_IsObject(argv[0])) - return JS_ThrowTypeError(ctx, "Expected at least 1 argument (object)"); - - JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL); - if (JS_IsException(json_data)) - return JS_EXCEPTION; - - const char *data = JS_ToCString(ctx, json_data); - if (!data) - return JS_EXCEPTION; - - ENetPacket *packet = enet_packet_create(data, strlen(data) + 1, ENET_PACKET_FLAG_RELIABLE); - JS_FreeCString(ctx, data); - JS_FreeValue(ctx, json_data); - - if (enet_peer_send(peer, 0, packet) < 0) { - return JS_ThrowInternalError(ctx, "Failed to send packet."); } + if (argc < 1) { + return JS_ThrowTypeError(ctx, "Expected an object to send."); + } + if (!JS_IsObject(argv[0])) { + return JS_ThrowTypeError(ctx, + "peer.send() only accepts a plain JS object, not strings/numbers."); + } + + JSValue json_data = JS_JSONStringify(ctx, argv[0], JS_NULL, JS_NULL); + if (JS_IsException(json_data)) { + return JS_ThrowTypeError(ctx, + "Failed to stringify object (circular ref or non-serializable)."); + } + + size_t data_len; + const char *data_str = JS_ToCStringLen(ctx, &data_len, json_data); + JS_FreeValue(ctx, json_data); + if (!data_str) { + return JS_EXCEPTION; + } + + // Create packet + ENetPacket *packet = enet_packet_create(data_str, data_len, ENET_PACKET_FLAG_RELIABLE); + JS_FreeCString(ctx, data_str); + if (!packet) { + return JS_ThrowInternalError(ctx, "Failed to create ENet packet."); + } + + if (enet_peer_send(peer, 0, packet) < 0) { + return JS_ThrowInternalError(ctx, "enet_peer_send returned error."); + } return JS_UNDEFINED; } -static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); - if (!peer) + if (!peer) { return JS_EXCEPTION; - + } enet_peer_disconnect_now(peer, 0); return JS_UNDEFINED; } -static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); - if (!peer) + if (!peer) { return JS_EXCEPTION; - + } enet_peer_disconnect_later(peer, 0); return JS_UNDEFINED; } -static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); - if (!peer) + if (!peer) { return JS_EXCEPTION; - + } enet_peer_reset(peer); return JS_UNDEFINED; } -static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); - if (!peer) + if (!peer) { return JS_EXCEPTION; - + } enet_peer_ping(peer); return JS_UNDEFINED; } -static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); - if (!peer) + if (!peer) { return JS_EXCEPTION; + } int interval, acceleration, deceleration; - if (argc < 3 || JS_ToInt32(ctx, &interval, argv[0]) || JS_ToInt32(ctx, &acceleration, argv[1]) || JS_ToInt32(ctx, &deceleration, argv[2])) - return JS_ThrowTypeError(ctx, "Expected 3 integer arguments (interval, acceleration, deceleration)"); + if (argc < 3 || + JS_ToInt32(ctx, &interval, argv[0]) || + JS_ToInt32(ctx, &acceleration, argv[1]) || + JS_ToInt32(ctx, &deceleration, argv[2])) { + return JS_ThrowTypeError(ctx, + "Expected 3 int arguments: interval, acceleration, deceleration"); + } enet_peer_throttle_configure(peer, interval, acceleration, deceleration); return JS_UNDEFINED; } -static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { +static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id); - if (!peer) + if (!peer) { return JS_EXCEPTION; + } int timeout_limit, timeout_min, timeout_max; - if (argc < 3 || JS_ToInt32(ctx, &timeout_limit, argv[0]) || JS_ToInt32(ctx, &timeout_min, argv[1]) || JS_ToInt32(ctx, &timeout_max, argv[2])) - return JS_ThrowTypeError(ctx, "Expected 3 integer arguments (timeout_limit, timeout_min, timeout_max)"); + if (argc < 3 || + JS_ToInt32(ctx, &timeout_limit, argv[0]) || + JS_ToInt32(ctx, &timeout_min, argv[1]) || + JS_ToInt32(ctx, &timeout_max, argv[2])) { + return JS_ThrowTypeError(ctx, + "Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max"); + } enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max); return JS_UNDEFINED; } +/* Class definitions */ static JSClassDef enet_host = { "ENetHost", .finalizer = js_enet_host_finalizer, @@ -300,51 +417,70 @@ static JSClassDef enet_peer_class = { .finalizer = js_enet_peer_finalizer, }; +/* Function lists */ static const JSCFunctionListEntry js_enet_funcs[] = { - JS_CFUNC_DEF("initialize", 0, js_enet_initialize), - JS_CFUNC_DEF("deinitialize", 0, js_enet_deinitialize), - JS_CFUNC_DEF("create_host", 1, js_enet_host_create), + JS_CFUNC_DEF("initialize", 0, js_enet_initialize), + JS_CFUNC_DEF("deinitialize", 0, js_enet_deinitialize), + JS_CFUNC_DEF("create_host", 1, js_enet_host_create), }; static const JSCFunctionListEntry js_enet_host_funcs[] = { - JS_CFUNC_DEF("service", 1, js_enet_host_service), - JS_CFUNC_DEF("connect", 2, js_enet_host_connect), - JS_CFUNC_DEF("flush", 0, js_enet_host_flush), - JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast), + JS_CFUNC_DEF("service", 2, js_enet_host_service), + JS_CFUNC_DEF("connect", 2, js_enet_host_connect), + JS_CFUNC_DEF("flush", 0, js_enet_host_flush), + JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast), }; static const JSCFunctionListEntry js_enet_peer_funcs[] = { - JS_CFUNC_DEF("send", 1, js_enet_peer_send), - JS_CFUNC_DEF("disconnect", 0, js_enet_peer_disconnect), - JS_CFUNC_DEF("disconnect_now", 0, js_enet_peer_disconnect_now), - JS_CFUNC_DEF("disconnect_later", 0, js_enet_peer_disconnect_later), - JS_CFUNC_DEF("reset", 0, js_enet_peer_reset), - JS_CFUNC_DEF("ping", 0, js_enet_peer_ping), - JS_CFUNC_DEF("throttle_configure", 3, js_enet_peer_throttle_configure), - JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout), + JS_CFUNC_DEF("send", 1, js_enet_peer_send), + JS_CFUNC_DEF("disconnect", 0, js_enet_peer_disconnect), + JS_CFUNC_DEF("disconnect_now", 0, js_enet_peer_disconnect_now), + JS_CFUNC_DEF("disconnect_later", 0, js_enet_peer_disconnect_later), + JS_CFUNC_DEF("reset", 0, js_enet_peer_reset), + JS_CFUNC_DEF("ping", 0, js_enet_peer_ping), + JS_CFUNC_DEF("throttle_configure",3, js_enet_peer_throttle_configure), + JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout), }; -JSValue js_enet_use(JSContext *js) -{ - JS_NewClassID(&enet_host_id); - JS_NewClass(JS_GetRuntime(js), enet_host_id, &enet_host); - JSValue host_proto = JS_NewObject(js); - JS_SetPropertyFunctionList(js, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs)); - JS_SetClassProto(js, enet_host_id, host_proto); +/* Module entry point */ +static int js_enet_init(JSContext *ctx, JSModuleDef *m); - JS_NewClassID(&enet_peer_class_id); - JS_NewClass(JS_GetRuntime(js), enet_peer_class_id, &enet_peer_class); - JSValue peer_proto = JS_NewObject(js); - JS_SetPropertyFunctionList(js, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs)); - JS_SetClassProto(js, enet_peer_class_id, peer_proto); +/* This function returns the default export object */ +JSValue js_enet_use(JSContext *ctx) { + // Register ENetHost class + JS_NewClassID(&enet_host_id); + JS_NewClass(JS_GetRuntime(ctx), enet_host_id, &enet_host); + JSValue host_proto = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs)); + JS_SetClassProto(ctx, enet_host_id, host_proto); - JSValue export = JS_NewObject(js); - JS_SetPropertyFunctionList(js, export, js_enet_funcs, sizeof(js_enet_funcs)/sizeof(JSCFunctionListEntry)); - return export; + // Register ENetPeer class + JS_NewClassID(&enet_peer_class_id); + JS_NewClass(JS_GetRuntime(ctx), enet_peer_class_id, &enet_peer_class); + JSValue peer_proto = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs)); + JS_SetClassProto(ctx, enet_peer_class_id, peer_proto); + + // Optional: store references in a "prosperon.c_types" for your environment + JSValue global = JS_GetGlobalObject(ctx); + JSValue prosp = JS_GetPropertyStr(ctx, global, "prosperon"); + JSValue c_types = JS_GetPropertyStr(ctx, prosp, "c_types"); + + JS_SetPropertyStr(ctx, c_types, "enet_host", JS_DupValue(ctx, host_proto)); + JS_SetPropertyStr(ctx, c_types, "enet_peer", JS_DupValue(ctx, peer_proto)); + + JS_FreeValue(ctx, c_types); + JS_FreeValue(ctx, prosp); + JS_FreeValue(ctx, global); + + // Create the default export object with top-level ENet functions + JSValue export_obj = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, export_obj, js_enet_funcs, countof(js_enet_funcs)); + return export_obj; } -static int js_enet_init(JSContext *js, JSModuleDef *m) { - return JS_SetModuleExport(js, m, "default", js_enet_use(js)); +static int js_enet_init(JSContext *ctx, JSModuleDef *m) { + return JS_SetModuleExport(ctx, m, "default", js_enet_use(ctx)); } #ifdef JS_SHARED_LIBRARY @@ -353,10 +489,12 @@ static int js_enet_init(JSContext *js, JSModuleDef *m) { #define JS_INIT_MODULE js_init_module_enet #endif -JSModuleDef *JS_INIT_MODULE(JSContext *js, const char *module_name) { - JSModuleDef *m = JS_NewCModule(js, module_name, js_enet_init); - if (!m) +/* Module definition */ +JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { + JSModuleDef *m = JS_NewCModule(ctx, module_name, js_enet_init); + if (!m) { return NULL; - JS_AddModuleExport(js, m, "default"); + } + JS_AddModuleExport(ctx, m, "default"); return m; } diff --git a/tests/enet.js b/tests/enet.js new file mode 100644 index 00000000..176d7ad1 --- /dev/null +++ b/tests/enet.js @@ -0,0 +1,191 @@ +var os = use('os'); +var enet = use('enet'); +var json = use('json'); // Some QuickJS environments need this import for JSON + +//////////////////////////////////////////////////////////////////////////////// +// 1. Initialization +//////////////////////////////////////////////////////////////////////////////// + +// Make sure ENet is initialized before we do anything else +enet.initialize(); + +//////////////////////////////////////////////////////////////////////////////// +// 2. "Framework": test runner & polling helper +//////////////////////////////////////////////////////////////////////////////// + +let results = []; + +/** + * Simple test runner. Each test is given a name and a function. + * If the function throws an Error, the test is marked failed. + */ +function runTest(testName, testFunc) { + console.log(`=== Running Test: ${testName} ===`); + try { + testFunc(); + results.push({ testName, passed: true }); + } catch (err) { + console.log(`Test "${testName}" failed: ${err}`); + results.push({ testName, passed: false, error: err }); + } +} + +/** + * runSteps polls both client and server for `steps` iterations, + * each iteration calling service() with a small timeout. Any events + * are captured and returned in arrays for further inspection. + * + * @param {Object} client - the client ENet host + * @param {Object} server - the server ENet host + * @param {number} steps - how many iterations to process + * @param {boolean} printEvents - whether to log events to console + * @param {number} timeout - milliseconds to pass to service() + * @returns { clientEvents, serverEvents } arrays of all events captured + */ +function runSteps(client, server, steps = 5, printEvents = false, timeout = 10) { + let clientEvents = []; + let serverEvents = []; + + for (let i = 0; i < steps; i++) { + // Poll the client + client.service((evt) => { + if (evt) { + clientEvents.push(evt); + if (printEvents) { + console.log("client:" + json.encode(evt)); + } + } + }, timeout); + + // Poll the server + server.service((evt) => { + if (evt) { + serverEvents.push(evt); + if (printEvents) { + console.log("server:" + json.encode(evt)); + } + } + }, timeout); + } + + return { clientEvents, serverEvents }; +} + +//////////////////////////////////////////////////////////////////////////////// +// 3. Actual Tests +//////////////////////////////////////////////////////////////////////////////// + +let serverHost = null; +let clientHost = null; +let clientPeer = null; + +runTest("Create Server", () => { + // Bind on 127.0.0.1:12345 + serverHost = enet.create_host("127.0.0.1:12345"); + if (!serverHost) { + throw new Error("Failed to create server host"); + } +}); + +runTest("Create Client", () => { + clientHost = enet.create_host(); + if (!clientHost) { + throw new Error("Failed to create client host"); + } +}); + +runTest("Connect Client to Server", () => { + clientPeer = clientHost.connect("127.0.0.1", 12345); + if (!clientPeer) { + throw new Error("Failed to create client->server peer"); + } + + // Poll both sides for a few steps so the connection can succeed + const { clientEvents, serverEvents } = runSteps(clientHost, serverHost, 5, true); + + // Verify the server got a 'connect' event + let connectEvent = serverEvents.find(evt => evt.type === "connect"); + if (!connectEvent) { + throw new Error("Server did not receive a connect event"); + } +}); + +runTest("Send Data from Client to Server", () => { + // Send some JSON object from client -> server + clientPeer.send({ hello: "HELLO" }); + + // Process for a few steps so the data actually arrives + const { clientEvents, serverEvents } = runSteps(clientHost, serverHost, 5, true); + + // The server should get a 'receive' event + let receiveEvent = serverEvents.find(evt => evt.type === "receive"); + if (!receiveEvent) { + throw new Error("Server did not receive data from the client"); + } + + // Check the payload + if (!receiveEvent.data || receiveEvent.data.hello !== "HELLO") { + throw new Error(`Server got unexpected data: ${JSON.stringify(receiveEvent.data)}`); + } +}); + +runTest("Broadcast from Server to Client", () => { + // The server broadcasts a JSON string + serverHost.broadcast({ broadcast: "HelloAll" }); + + // Let data flow + const { clientEvents, serverEvents } = runSteps(clientHost, serverHost, 5, true); + + // Client should get a 'receive' event with broadcast data + let broadcastEvent = clientEvents.find(evt => evt.type === "receive"); + if (!broadcastEvent) { + throw new Error("Client did not receive broadcast data"); + } + + // The broadcastEvent.data should be an object with broadcast="HelloAll" + if (!broadcastEvent.data || broadcastEvent.data.broadcast !== "HelloAll") { + throw new Error(`Client received unexpected broadcast: ${JSON.stringify(broadcastEvent.data)}`); + } +}); + +runTest("Disconnect Client", () => { + // Disconnect from the client side + clientPeer.disconnect(); + + // Let both sides see the disconnect + const { clientEvents, serverEvents } = runSteps(clientHost, serverHost, 5, true); + + // The server should eventually get a "disconnect" + let disconnectEvent = serverEvents.find(evt => evt.type === "disconnect"); + if (!disconnectEvent) { + throw new Error("Server never received a disconnect event"); + } +}); + +runTest("Deinitialize ENet", () => { + enet.deinitialize(); +}); + +//////////////////////////////////////////////////////////////////////////////// +// 4. Print Summary & Exit +//////////////////////////////////////////////////////////////////////////////// + +console.log("\n=== Test Summary ==="); +let passedCount = 0; +results.forEach(({ testName, passed, error }, idx) => { + console.log(`Test ${idx + 1}: ${testName} - ${passed ? "PASSED" : "FAILED"}`); + if (!passed && error) { + console.log(" Reason:", error); + } + if (passed) passedCount++; +}); + +console.log(`\nResult: ${passedCount}/${results.length} tests passed`); + +if (passedCount < results.length) { + console.log("Overall: FAILED"); + os.exit(1); +} else { + console.log("Overall: PASSED"); + os.exit(0); +}