Source: executrix/socketsClient.js

/**
 * @file socketsClient.js
 * @module socketsClient
 * @description Creates a socket client to connect and send messages through the server to other processes 
 * @requires @requires {@link https://www.npmjs.com/package/@haystacks/constants|@haystacks/constants}
 * @requires {@link https://www.npmjs.com/package/path|path}
 * @author Seth Hollingsead
 * @NOTE: Original author Karl-Edward FP Jean-Mehu
 * @date 2025-01-02 - Originally 2023/12/29
 * @copyright Copyright © 2025-… by Seth Hollingsead. All rights reserved
 */

// Internal imports
import ruleBroker from '../brokers/ruleBroker.js';
// External imports
import { Socket } from 'net';
import process from 'process'
import { promisify } from 'util';
import hayConst from '@haystacks/constants';
import path from 'path';

const {bas, biz, gen, msg, sys, wrd} = hayConst;
const baseFileName = path.basename(import.meta.url, path.extname(import.meta.url));
// framework.executrix.loggers.
// eslint-disable-next-line no-unused-vars
const namespacePrefix =  wrd.cframework + bas.cDot + wrd.cexecutrix + bas.cDot + baseFileName + bas.cDot;

// Host and port to which the socket
// connection will be listening to
const SOCKET = {
  host: '127.0.0.1',
  port: 3000
}

// Get current development mode
const isDevMode =
  !process.env[sys.cNODE_ENV] ||
  process.env[sys.cNODE_ENV].toLowerCase() !== wrd.cproduction;
const messageDelimiter = bas.cHash.repeat(2) + wrd.cEND + bas.cHash.repeat(2); // ##END##
const chunkDelimiter = bas.cPipe + bas.cTilde + bas.cPipe; // |~|
const maxMessageLength = 840;

/**
 * @name socketsClient
 * @description Creates a socket client instance and handles communication to the server 
 * @author Karl-Edward FP Jean-Mehu
 * @date 2023/12/29
 */
export default async function socketsClient() {
  try {
    const socket = Socket({});
    const writeAsync = promisify(socket.write.bind(socket));

    // Keep connection live
    socket.setKeepAlive(true);

    // Connect to server
    let isConnected = false;

    // Connections has been refused or connection is in use.
    let connectionRefusedOrInUse = false;

    if (isConnected == false) {
      socket.connect(SOCKET.port, SOCKET.host);
      socket.setNoDelay(true);  // Disable Nagle's algorithm to avoid packet batching
      isConnected = true;
    }

    // Error handler
    socket.on(wrd.cerror, (error) => {
      if (error.code === gen.cECONNREFUSED || error.code === gen.cEADDRINUSE){
        connectionRefusedOrInUse = true;
        // Server is offline! Connection is already in use or has been denied.
        // console.log(bas.cCarriageReturn + bas.cNewLine + msg.csocketsClientServerOffline + bas.cCarriageReturn + bas.cNewLine);
      }else{
        console.log(bas.cCarriageReturn + bas.cNewLine + msg.cErrorOnClient + error.message);
      }
    });

    // Handle connection being closed
    socket.on(wrd.cclose, async () => {
      // Connection closed!
      if (!connectionRefusedOrInUse) console.log(msg.cConnectionClosed);
    });

    // Handles actions to take when the server establishes a connection
    // Client connection established!
    socket.on(wrd.cconnect, async () => console.log(msg.cClientConnectionEstablished));

    // Handle receiving data
    socket.on(wrd.cdata, async (chunk) => {
      // console.log(`Received data, ${JSON.parse(chunk)}`);
    });

    /**
     * Sends data through to the socket server.
     * Parses and sends test result as well.
     * @param { string } message - Message to be logged.
     */
    socket.send = async (message) => {
      if (isConnected) {
        // Create timestamp
        const timestamp = await ruleBroker.processRules([gen.cYYYYMMDD_HHmmss_SSS, ''], [biz.cgetNowMoment]);
        let broadcastMessage = JSON.stringify({ message: `${message}`, timestamp }) + messageDelimiter;
        
        // Write data to socket
        await writeAsync(broadcastMessage, async (error) => {

          // Assuming app is called locally / directly. Display only debug logs on failed
          // stream writes
          if (error) {
            if (error.code === gen.ERR_STREAM_DESTROYED) {
              console.log(bas.cCarriageReturn + bas.cNewLine + msg.cConnectionClosedFailedSendingChunk + broadcastMessage +
                bas.cCarriageReturn + bas.cNewLine);
            } else {
              console.log(namespacePrefix + wrd.csend + bas.cSpace + wrd.cchunk + bas.cColon + bas.cSpace + broadcastMessage +
                bas.cCarriageReturn + bas.cNewLine);
              console.log(namespacePrefix + wrd.csend + bas.cSpace + wrd.cerror + bas.cColon + bas.cSpace + error.message +
                bas.cCarriageReturn + bas.cNewLine);
            }
          } // End-if (error)
        });

        // Parse broadcastMessage for and return test results
        const REGEX = /((TestResultsLog)\s:\s(Test_\w+)\s:)\s(\w)+/g;

        const validResponses = [wrd.cPass, wrd.cFail, wrd.cWarning];
        const matchResult = broadcastMessage.match(REGEX);

        if  (matchResult) {
          // Testing for match
          console.log(msg.cTestingForMatch + matchResult[0]);
          const testResultsMatch = matchResult[0].split(':').map(v => v.trim())[2];

          if (!testResultsMatch) {
            console.log({testResultsMatch});
            return;
          }

          console.log({ testResultsMatch });
          const testResult = validResponses.includes(( testResultsMatch || "").toLowerCase()) ? testResultsMatch[2] : wrd.cFail

          await writeAsync(JSON.stringify({ testResult }));
        }
      } else {
        return;
      }
     };

    // Return connection status
    socket.isConnected = async () => {
      await new Promise(resolve => {
        resolve(isConnected);
      });
    };

    // Closes the connection with the server
    socket.close = async () => {
      isConnected = false;
      await promisify(socket.end.bind(socket))();
      // await promisify(socket.exit.bind(socket))();
      await promisify(socket.destroy.bind(socket))();
    };

    return socket;
  } catch (error) {
    if (error.code === gen.cECONNREFUSED || error.code === gen.cEADDRINUSE){
      // Socket server offline!
      console.log(bas.cCarriageReturn + bas.cNewLine + msg.cSocketServerOffline + bas.cCarriageReturn + bas.cNewLine);
    } else {
      // Socket client failed:
      console.log(bas.cCarriageReturn + bas.cNewLine + msg.cSocketClientFailed + error.message);
    }
  }
}