Skip to content

Instantly share code, notes, and snippets.

@mathew-odwyer
Created December 20, 2025 13:47
Show Gist options
  • Select an option

  • Save mathew-odwyer/9c69f26dc5e5a721d47467c6bf6bab43 to your computer and use it in GitHub Desktop.

Select an option

Save mathew-odwyer/9c69f26dc5e5a721d47467c6bf6bab43 to your computer and use it in GitHub Desktop.
Simple NATS Client Protocol Implementation for GML (GameMaker Language)
/// @description Provides a NATS client-side protocol handler for a `Struct.Client`.
/// @param {Function} send The function used to send messages through the socket.
/// @param {Struct} options The NATS connection options.
function NatsClientProtocol(send, options = {}) constructor
{
/// @type {Struct.Logger}
/// @description The logger instance for logging protocol events.
static _logger = new Logger(nameof(NatsClientProtocol));
/// @type {Function}
/// @description The function used to send messages to the NATS server.
_send = send;
/// @type {Struct}
/// @description The NATS connection options (verbose, pedantic, name, version, etc.).
_options = options;
/// @type {String}
/// @description The NATS protocol line terminator.
_terminator = "\r\n";
/// @type {Struct}
/// @description Maps subject names to their subscriber callback functions.
_command_to_handler_map = {};
/// @description Subscribes to a subject and registers a callback to handle incoming messages.
/// @param {String} subject The subject to subscribe to.
/// @param {Function} callback The callback function to invoke when a message arrives on the subject. Callback receives (payload, reply_to).
subscribe = function(subject, callback)
{
_logger.log(log_type.trace, $"Subscribing to: '{subject}'");
var result = _send($"SUB {subject} {subject}{_terminator}");
if (!result)
{
_logger.log(log_type.error, $"Failed to subscribe to: '{subject}'");
return;
}
_command_to_handler_map[$ subject] = callback;
}
/// @description Publishes a message to the specified subject.
/// @param {String} subject The subject to publish to.
/// @param {Any} data The data to publish (will be JSON stringified).
publish = function(subject, data)
{
_logger.log(log_type.trace, $"Publishing to: '{subject}'");
var payload = json_stringify(data);
var size = string_byte_length(payload);
var result = _send($"PUB {subject} {size}{_terminator}{payload}{_terminator}");
if (!result)
{
_logger.log(log_type.error, $"Failed to publish to: '{subject}'");
}
}
/// @description Handles an incoming message from the NATS server.
/// @param {String} payload The raw payload received from the server.
handle_message = function(payload)
{
var parts = string_split(payload, _terminator, true);
if (array_length(parts) < 1)
{
return;
}
var control = string_trim(parts[0]);
var body = array_length(parts) > 1 ? parts[1] : "";
var parameters = string_split(control, " ", true);
if (array_length(parameters) == 0)
{
return;
}
var command = string_upper(parameters[0]);
switch (command)
{
case "INFO":
_handle_info_command(parameters);
break;
case "PING":
_handle_ping_command();
break;
case "MSG":
_handle_msg_command(parameters, body);
break;
}
}
/// @description Handles an INFO command from the server.
_handle_info_command = function(parameters)
{
var info = json_parse(parameters[1]);
_logger.log(log_type.information, $"Connecting to: '{info[$ "server_name"]}'...");
var connect = {
verbose: _options[$ "verbose"] ?? false,
pedantic: _options[$ "pedantic"] ?? false,
name: _options[$ "name"] ?? "GameMaker NATS Client",
lang: "GML",
version: _options[$ "version"] ?? GM_version,
};
_send($"CONNECT {json_stringify(connect)}{_terminator}");
}
/// @description Handles a PING command from the server by responding with PONG.
_handle_ping_command = function()
{
_send($"PONG{_terminator}");
}
/// @description Handles an incoming MSG command by routing to the appropriate subscriber callback.
/// @param {Array<String>} parameters The MSG command parameters parsed from the server message.
/// @param {String} body The message body/payload.
_handle_msg_command = function(parameters, body)
{
try
{
var subject = parameters[1];
var reply_to = array_length(parameters) > 3 ? parameters[3] : "";
var handler = _command_to_handler_map[$ subject];
if (is_undefined(handler))
{
_logger.log(log_type.warning, $"No handler registered for subject: '{subject}'");
return;
}
handler(json_parse(body), reply_to);
}
catch (ex)
{
_logger.log(log_type.error, $"Error handling NATS message: {ex}");
throw new NatsError($"Error handling NATS message.", ex);
}
}
}
/// @description Represents an error that is thrown when a NATS error occurs.
/// @param {String} message The message that describes the error.
/// @param {Struct|Undefined} inner_error The inner error that was thrown (if any).
function NatsError(message, inner_error = undefined)
: Error(message, inner_error) constructor
{
}
/// @description Enumerates the available message log types.
enum log_type
{
trace = 0,
debug = 1,
information = 2,
warning = 3,
error = 4,
critical = 5,
};
/// @description Represents a logger.
/// @param {String} name The name of the instance logging.
function Logger(name = "global") constructor
{
/// @type {Enum.log_type}
/// @description The logging level.
static LogLevel = log_type.debug;
/// @type {Function}
/// @description The function used when logging.
static LogFunction = show_debug_message;
/// @description Gets the log file path for all loggers.
/// @returns {String} Returns the log file path for all loggers.
static GetLogPathCallback = function()
{
var now = date_current_datetime();
var str = date_datetime_string(now);
str = string_replace_all(str, "/", "-");
str = string_replace_all(str, ":", "-");
var path = $"Logs/{str}.txt";
return path;
}
/// @type {String}
/// @description The log file path.
static _path = GetLogPathCallback();
/// @type {String}
/// @description The name of the instance logging.
_name = name;
/// @description Logs a message.
/// @argument {Enum.log_type} type The type of message to log.
/// @argument {String} text The message to log.
log = function(type, text)
{
if (type < LogLevel)
{
return;
}
static _type_to_name_map = [
[log_type.trace, "Trace"],
[log_type.debug, "Debug"],
[log_type.information, "Information"],
[log_type.warning, "Warning"],
[log_type.error, "Error"],
[log_type.critical, "Critical"],
];
var entry = $"[{_type_to_name_map[type][1]}] [{_name}]: {text}";
var file = file_text_open_append(_path);
LogFunction(entry);
file_text_write_string(file, entry);
file_text_write_string(file, "\n");
file_text_close(file);
}
}
new Logger();
@mathew-odwyer
Copy link
Author

And, example:

_client = new Client(network_socket_tcp);
_client.connect("winterhaven.com.au", 4222);

_protocol = new NatsClientProtocol(_client.send);

_protocol.subscribe("apple.cake", function(result, reply)
{
    show_debug_message(result[$ "data"]);
    _protocol.publish(reply, result);
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment