79131878

Date: 2024-10-28 02:02:46
Score: 2
Natty:
Report link

I think I've come up with my own solution using spawn. Here's lib.js, the file containing my own "library code" (I'm only showing one example function here, but this is easily extensible):

// ============================================ IMPORTS ============================================

import events from "node:events";
import { spawn } from "node:child_process";

// ============================================ GLOBALS ============================================

const emitter = new events.EventEmitter();
let childProcess;
let id = 0;

//  ======================================== INITIALIZATION ========================================

function initParentProcess() {
  // Create a child process of this file using spawn:
  childProcess = spawn("node", ["."], {
    stdio: ["pipe", "pipe", "pipe", "ipc"],
    env: { IS_CHILD: "true" },
  });

  // Listen for stdout/stderr output from the child process and forward it to the importing file;
  // Note that ".slice(0, -1)" removes ending newlines; you may or may not want this:
  childProcess.stdout?.on("data", (data) => {
    emitter.emit("stdout", data.toString().slice(0, -1));
  });
  childProcess.stderr?.on("data", (data) => {
    emitter.emit("stderr", data.toString().slice(0, -1));
  });

  // Once the child process is ready, emit the "ready" event:
  childProcess.once("message", (state) => {
    if (state === "ready") emitter.emit("ready");
  });
}

function initChildProcess() {
  // Satisfy the ESLint overlords:
  if (!process.send) return;

  // Handle incoming requests from the parent process:
  process.on("message", async ({ id, func, args: args }) => {
    // Satisfy the ESLint overlords:
    if (!process.send) return;

    // Handle each function and their arguments:
    switch (func) {
      case "example":
        process.send({ id, res: await _example(...args) });
        break;
      // Other functions should be handled here
    }
  });

  // Notify the parent process that the child process is ready:
  process.send("ready");
}

if (!process.env.IS_CHILD) {
  // If not a child process, this module must have been imported from another file:
  initParentProcess();
} else {
  // Otherwise, this instance is the child process:
  initChildProcess();
}

// ======================================= ABSTRACTION LAYER =======================================

// The handle function will forward work to the child process, where it will be completed.
// Upon completion, the handle function's Promise will resolve with the result of the work.
function handle(func, ...args) {
  // Handle setup:
  const handleID = id++;
  const listener = (resolve, id, res) => {
    // Only handle the message if the ID matches the handle ID:
    if (id !== handleID) return;

    // Remove the listener and resolve with the result:
    process.removeListener("message", listener);
    resolve(res);
  };

  // Return a Promise that resolves once we recieve a response from the child process:
  return new Promise((resolve) => {
    childProcess.send({ id: handleID, func, args });
    childProcess.on("message", ({ id, res }) => listener(resolve, id, res));
  });
}

// ================================= CHILD PROCESS IMPLEMENTATIONS =================================

// This is the actual implementation of "example":
async function _example(sampleArg) {
  return `Example output; sampleArg=${sampleArg}`;
}

//  ================================ PARENT PROCESS IMPLEMENTATIONS ================================

// This is the module export for the importing file to use:
export async function example(sampleArg) {
  return await handle("example", sampleArg);
}

//  ====================================== SUPPORTING EXPORTS ======================================

// Event emitter exports so the importing file knows when this module is ready:
export function on(event, callback) {
  emitter.on(event, callback);
}
export function once(event, callback) {
  emitter.once(event, callback);
}

export function close() {
  childProcess.kill();
}

And here's main.js:

import { once, example, close } from "./lib.js";

once("ready", async () => {
  console.log("Ready!");
  const result1 = await example("test");
  const result2 = await example("hello world");
  console.log("Result 1:", result1);
  console.log("Result 2:", result2);
  close();
});

Running node main.js will print:

Ready!
Result 1: Example output; sampleArg=test
Result 2: Example output; sampleArg=hello world

Additionally, I can now listen for stdout and stderr from main.js as follows:

import { on } from "./lib.js";

on("stdout", (data) => {
  console.log(data);
});
on("stderr", (data) => {
  console.log(data);
});

The way this works is broken into the following steps:

It seems somewhat complicated for how simple of a problem I'm trying to solve, but if it works, it works. Please let me know if there's any way to simplify this without making it harder to use the module from main.js.

Reasons:
  • RegEx Blacklisted phrase (2.5): Please let me know
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Low reputation (0.5):
Posted by: simplexshotz