fix enet; add enet testing
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled

This commit is contained in:
2025-02-24 13:39:47 -06:00
parent b7da920f31
commit e9519484cc
4 changed files with 632 additions and 151 deletions

View File

@@ -230,7 +230,8 @@ copy_tests = custom_target(
tests = [
'spawn_actor',
'empty',
'nota'
'nota',
'enet'
]
foreach file : tests

151
scripts/modules/enet.js Normal file
View File

@@ -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;

View File

@@ -1,4 +1,4 @@
// enet_qjs.c
// qjs_enet.c
#include "quickjs.h"
#include <enet/enet.h>
#include <stdio.h>
@@ -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,53 +19,63 @@ 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.");
}
@@ -75,38 +86,44 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
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))
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;
@@ -114,19 +131,50 @@ static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int a
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");
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,
"<enet-packet>");
js_free(ctx,tmp);
if (JS_IsException(packet_data)) {
packet_data = JS_NULL;
// 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: {
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);
}
@@ -135,161 +183,230 @@ static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int a
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,6 +417,7 @@ 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),
@@ -307,7 +425,7 @@ static const JSCFunctionListEntry js_enet_funcs[] = {
};
static const JSCFunctionListEntry js_enet_host_funcs[] = {
JS_CFUNC_DEF("service", 1, js_enet_host_service),
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),
@@ -324,27 +442,45 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = {
JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout),
};
JSValue js_enet_use(JSContext *js)
{
/* Module entry point */
static int js_enet_init(JSContext *ctx, JSModuleDef *m);
/* 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(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);
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);
// Register ENetPeer class
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);
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);
JSValue export = JS_NewObject(js);
JS_SetPropertyFunctionList(js, export, js_enet_funcs, sizeof(js_enet_funcs)/sizeof(JSCFunctionListEntry));
return export;
// 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;
}

191
tests/enet.js Normal file
View File

@@ -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);
}