Created
December 20, 2025 13:47
-
-
Save mathew-odwyer/9c69f26dc5e5a721d47467c6bf6bab43 to your computer and use it in GitHub Desktop.
Simple NATS Client Protocol Implementation for GML (GameMaker Language)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// @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(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
And, example: