npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@david.uhlir/vm-machine

v0.1.16

Published

Helper to evaluate code in a virtual machine environment.

Readme

VM-machine

npm version License: ISC

A powerful and secure library for executing JavaScript code in isolated virtual machines using Node.js isolated-vm. Perfect for running untrusted code, creating sandboxed execution environments, and building extensible applications with plugin systems.

Features

  • Secure Execution: Run JavaScript in completely isolated V8 contexts
  • Memory Management: Configurable memory limits and automatic cleanup
  • Modular Library System: Extensible plugin architecture with built-in libraries
  • IPC Communication: Safe communication between host and VM contexts
  • Error Handling: Comprehensive error handling with timeout support
  • TypeScript Support: Full TypeScript definitions included
  • Production Ready: Battle-tested with comprehensive test suite

Installation

npm install @david.uhlir/vm-machine

Quick Start

import { VM, VMConsoleLibrary, VMIpcLibrary, VMIpcRxLibrary } from '@david.uhlir/vm-machine'

// Create libraries
const consoleLib = new VMConsoleLibrary()
const ipcRxLib = new VMIpcRxLibrary({
  calculateSum: async (a: number, b: number) => a + b
})
const ipcTxLib = new VMIpcLibrary()

// Initialize VM
const vm = new VM([consoleLib, ipcRxLib, ipcTxLib])
await vm.initialize()

// Execute code
const code = `
  console.log('Hello from VM!');

  async function main() {
    const result = await calculateSum(5, 10);
    return { message: 'Calculation done', result };
  }

  return { main };
`

await vm.run(code)

// Call VM function
const result = await ipcTxLib.callIpc('main', [])
console.log(result) // { message: 'Calculation done', result: 15 }

// Cleanup
vm.dispose()

Complete Example: JavaScript Project Runner

Here's a comprehensive example showing how to run a complete JavaScript project with filesystem access, modules, and monitoring:

import {
  VM,
  VMConsoleLibrary,
  VMCodeBaseLibrary,
  VMMeasureLibrary,
  VMIpcRxLibrary,
  VMIpcLibrary,
  VMTimingLibrary
} from '@david.uhlir/vm-machine'

async function runJavaScriptProject() {
  // Setup filesystem and module system
  const codebaseLib = new VMCodeBaseLibrary({
    maxMemfsSize: 10 * 1024 * 1024, // 10MB
    maxFileCount: 1000,
    moduleResolver: async (name) => {
      // Custom module resolver for external libraries
      if (name === 'axios') {
        return `module.exports = { get: async (url) => ({ data: 'mocked response' }) };`
      }
      return undefined
    }
  })

  // Setup performance monitoring
  const measureLib = new VMMeasureLibrary({
    responseTimeLimit: 5000 // 5 second limit
  })

  // Setup host functions
  const ipcRxLib = new VMIpcRxLibrary({
    saveToDatabase: async (data: any) => {
      console.log('Saving to database:', data)
      return { id: Date.now(), success: true }
    },
    fetchApiData: async (endpoint: string) => {
      console.log('Fetching from API:', endpoint)
      return { data: 'API response data', timestamp: Date.now() }
    }
  })

  const ipcTxLib = new VMIpcLibrary({ timeout: 10000 })
  const consoleLib = new VMConsoleLibrary()
  const timingLib = new VMTimingLibrary({ limitCount: 50 })

  // Create VM with all libraries
  const vm = new VM([
    consoleLib,
    codebaseLib,
    measureLib,
    ipcRxLib,
    ipcTxLib,
    timingLib
  ], {
    memoryLimit: 128, // 128MB
    onError: (error) => console.error('VM Error:', error),
    onCatastrophicError: (error) => {
      console.error('CRITICAL VM ERROR:', error)
      process.exit(1)
    }
  })

  try {
    // Get virtual filesystem
    const fs = codebaseLib.getFs()

    // Create project structure
    await fs.promises.mkdir('/src', { recursive: true })
    await fs.promises.mkdir('/config', { recursive: true })

    // Add configuration
    await fs.promises.writeFile('/config/app.json', JSON.stringify({
      apiUrl: 'https://api.example.com',
      retries: 3,
      timeout: 5000
    }))

    // Add utility module
    await fs.promises.writeFile('/src/utils.js', `
      const fs = require('fs/promises');

      async function loadConfig() {
        const configData = await fs.readFile('/config/app.json', 'utf-8');
        return JSON.parse(configData);
      }

      function processData(data, multiplier = 1) {
        return data.map(item => ({
          ...item,
          value: item.value * multiplier,
          processed: true,
          timestamp: new Date().toISOString()
        }));
      }

      module.exports = { loadConfig, processData };
    `)

    // Add main application
    await fs.promises.writeFile('/src/app.js', `
      const utils = require('./utils');
      const axios = require('axios'); // Resolved by custom resolver

      async function processApiData() {
        console.log('Starting data processing...');

        // Load configuration
        const config = await utils.loadConfig();
        console.log('Loaded config:', config);

        // Fetch data from API (via host function)
        const response = await fetchApiData('/users');
        console.log('API Response:', response);

        // Simulate data processing
        const mockData = [
          { id: 1, value: 10 },
          { id: 2, value: 20 },
          { id: 3, value: 30 }
        ];

        const processed = utils.processData(mockData, 2);
        console.log('Processed data:', processed);

        // Save to database (via host function)
        const saveResult = await saveToDatabase({
          type: 'processed_data',
          data: processed,
          config: config
        });

        console.log('Save result:', saveResult);

        // Use timer for delayed operation
        await new Promise(resolve => {
          setTimeout(() => {
            console.log('Delayed operation completed');
            resolve();
          }, 1000);
        });

        return {
          success: true,
          processedCount: processed.length,
          saveId: saveResult.id
        };
      }

      module.exports = { processApiData };
    `)

    // Set entry point
    codebaseLib.setEntryFile('/src/app.js')

    // Initialize and run VM
    await vm.initialize()

    // Start performance monitoring
    const monitorInterval = setInterval(async () => {
      measureLib.ping()
      const stats = await measureLib.getUsageStats()
      console.log(`VM Stats - Memory: ${(stats.usedMemory / 1024 / 1024).toFixed(2)}MB (${stats.memoryUsedPercentage.toFixed(1)}%), Response: ${stats.responseTime}ms`)
    }, 2000)

    // Run the project
    await vm.run() // This will automatically execute /src/app.js

    // Call the main function
    const result = await ipcTxLib.callIpc('processApiData', [])
    console.log('Final result:', result)

    // Read any files created by the VM
    try {
      const files = await fs.promises.readdir('/output')
      console.log('Output files created:', files)
    } catch (e) {
      console.log('No output directory created')
    }

    clearInterval(monitorInterval)
    return result

  } finally {
    vm.dispose()
  }
}

// Usage
runJavaScriptProject()
  .then(result => console.log('Project completed:', result))
  .catch(error => console.error('Project failed:', error))

This example demonstrates:

  • Virtual filesystem with project structure
  • Module system with custom resolvers
  • Host-VM communication for API calls and database operations
  • Performance monitoring with real-time stats
  • Timer support for delayed operations
  • Error handling and resource management
  • Resource limits and security boundaries

Complete Multi-Threading Example

Here's an advanced example showcasing the full power of VM-machine with threading capabilities:

import {
  VM,
  VMConsoleLibrary,
  VMCodeBaseLibrary,
  VMThreadLibrary,
  VMMeasureMasterLibrary,
  VMTimingLibrary,
  VMIpcRxLibrary
} from '@david.uhlir/vm-machine'

async function runMultiThreadedApplication() {
  const codebaseLib = new VMCodeBaseLibrary({
    maxMemfsSize: 20 * 1024 * 1024, // 20MB
    maxFileCount: 2000
  })

  const measureMaster = new VMMeasureMasterLibrary()
  const fs = codebaseLib.getFs()

  // Create worker thread for CPU-intensive tasks
  await fs.promises.writeFile('/cpu-worker.js', `
    console.log('CPU Worker thread started');

    let taskCount = 0;

    exposeCallables({
      calculatePrimes: async (max) => {
        taskCount++;
        console.log(\`Starting prime calculation up to \${max} (task #\${taskCount})\`);

        const primes = [];
        for (let num = 2; num <= max; num++) {
          let isPrime = true;
          for (let i = 2; i <= Math.sqrt(num); i++) {
            if (num % i === 0) {
              isPrime = false;
              break;
            }
          }
          if (isPrime) primes.push(num);

          // Report progress for large calculations
          if (num % 1000 === 0) {
            await host.reportProgress(\`Checked \${num}/\${max} numbers\`);
          }
        }

        await host.logResult(\`Found \${primes.length} primes up to \${max}\`);
        return { primes: primes.slice(-10), count: primes.length, taskId: taskCount };
      },

      getWorkerStats: async () => ({
        taskCount,
        workerId: 'cpu-worker',
        isAlive: true
      })
    });
  `)

  // Create I/O worker thread for database operations
  await fs.promises.writeFile('/io-worker.js', `
    console.log('I/O Worker thread started');

    let operationCount = 0;

    exposeCallables({
      processDataBatch: async (batch) => {
        operationCount++;
        console.log(\`Processing batch of \${batch.length} items (op #\${operationCount})\`);

        const results = [];
        for (const item of batch) {
          // Simulate database operation
          await new Promise(resolve => setTimeout(resolve, 50));

          const processed = {
            ...item,
            processed: true,
            processedAt: new Date().toISOString(),
            workerId: 'io-worker',
            operationId: operationCount
          };

          // Save to host database
          const saved = await host.saveItem(processed);
          results.push({ ...processed, savedId: saved.id });
        }

        return { results, totalProcessed: results.length };
      },

      getWorkerStats: async () => ({
        operationCount,
        workerId: 'io-worker',
        isAlive: true
      })
    });
  `)

  // Create main coordinator application
  await fs.promises.writeFile('/coordinator.js', `
    console.log('Main coordinator started');

    let cpuWorker, ioWorker;
    const results = [];

    async function initializeWorkers() {
      console.log('Initializing worker threads...');

      cpuWorker = await startThread('/cpu-worker');
      ioWorker = await startThread('/io-worker');

      console.log('Workers initialized:', {
        cpu: cpuWorker.threadId,
        io: ioWorker.threadId
      });
    }

    async function runParallelTasks() {
      console.log('Starting parallel task execution...');

      // Start CPU-intensive task
      const primeTask = cpuWorker.calculatePrimes(10000);

      // Start I/O tasks in parallel
      const batches = [
        [{ id: 1, data: 'batch1' }, { id: 2, data: 'batch1' }],
        [{ id: 3, data: 'batch2' }, { id: 4, data: 'batch2' }],
        [{ id: 5, data: 'batch3' }, { id: 6, data: 'batch3' }]
      ];

      const ioTasks = batches.map(batch => ioWorker.processDataBatch(batch));

      // Wait for all tasks to complete
      const [primeResult, ...ioResults] = await Promise.all([primeTask, ...ioTasks]);

      console.log('All tasks completed!');
      console.log('Prime calculation result:', {
        count: primeResult.count,
        lastPrimes: primeResult.primes
      });

      console.log('I/O processing results:', {
        totalBatches: ioResults.length,
        totalItems: ioResults.reduce((sum, r) => sum + r.totalProcessed, 0)
      });

      return { primeResult, ioResults };
    }

    async function monitorWorkers() {
      const [cpuStats, ioStats] = await Promise.all([
        cpuWorker.getWorkerStats(),
        ioWorker.getWorkerStats()
      ]);

      console.log('Worker statistics:', { cpuStats, ioStats });
      return { cpuStats, ioStats };
    }

    async function cleanup() {
      console.log('Cleaning up workers...');
      if (cpuWorker && !cpuWorker.isDisposed) {
        cpuWorker.dispose();
      }
      if (ioWorker && !ioWorker.isDisposed) {
        ioWorker.dispose();
      }
      console.log('Cleanup complete');
    }

    async function main() {
      try {
        await initializeWorkers();
        const results = await runParallelTasks();
        await monitorWorkers();

        // Schedule cleanup
        setTimeout(cleanup, 2000);

        return {
          success: true,
          results
        };
      } catch (error) {
        console.error('Error in main execution:', error);
        await cleanup();
        throw error;
      }
    }

    // Export the main function for external calling
    exposeCallables({ main });
  `)

  // Setup thread library with monitoring
  const threadLib = new VMThreadLibrary({
    threadMemoryLimit: 64, // 64MB per thread
    threadsCountLimit: 10,
    getThreadLibraries: (threadId) => [
      new VMConsoleLibrary((level, ...args) =>
        console.log(`[THREAD-${threadId}]`, ...args)
      ),
      new VMTimingLibrary({ limitCount: 20 }),
      measureMaster.getNewLibrary(`thread-${threadId}`),
      new VMIpcRxLibrary({
        reportProgress: async (message) => {
          console.log(`[PROGRESS-${threadId}]`, message);
          return { acknowledged: true };
        },
        logResult: async (message) => {
          console.log(`[RESULT-${threadId}]`, message);
          return { logged: true };
        },
        saveItem: async (item) => {
          console.log(`[SAVE-${threadId}]`, 'Saving item:', item.id);
          return {
            id: Date.now() + Math.random(),
            saved: true,
            timestamp: new Date().toISOString()
          };
        }
      })
    ]
  })

  // Setup main VM with all capabilities
  const vm = new VM([
    new VMConsoleLibrary((level, ...args) => console.log('[MAIN]', ...args)),
    codebaseLib,
    threadLib,
    measureMaster,
    new VMTimingLibrary(),
    new VMIpcRxLibrary({
      getSystemInfo: async () => ({
        activeThreads: threadLib.getThreads().size,
        mainVmMemory: 'simulated-memory-info',
        timestamp: Date.now()
      })
    })
  ], {
    memoryLimit: 128, // 128MB for main VM
    onError: (error) => console.error('[MAIN-ERROR]', error),
    onCatastrophicError: (error) => {
      console.error('[CRITICAL-ERROR]', error);
      process.exit(1);
    }
  })

  try {
    // Set entry point and initialize
    codebaseLib.setEntryFile('/coordinator.js')
    await vm.initialize()

    // Start performance monitoring
    const monitorInterval = setInterval(async () => {
      await measureMaster.pingAll()
      const stats = await measureMaster.getCombinedStats()

      console.log('[MONITOR]', 'System Performance:', {
        totalMemory: `${(stats.usedMemory / 1024 / 1024).toFixed(2)} MB`,
        memoryPercentage: `${stats.memoryUsedPercentage.toFixed(1)}%`,
        avgResponseTime: `${stats.responseAverageTime.toFixed(2)}ms`,
        maxResponseTime: `${stats.responseTime}ms`,
        activeInstances: stats.managedCount,
        activeThreads: threadLib.getThreads().size
      })
    }, 3000)

    // Run the application
    await vm.run() // This auto-executes coordinator.js

    // Let it run for a while to see the threading in action
    await new Promise(resolve => setTimeout(resolve, 10000))

    clearInterval(monitorInterval)
    console.log('[MAIN]', 'Application completed successfully')

  } finally {
    vm.dispose()
  }
}

// Run the multi-threaded application
runMultiThreadedApplication()
  .then(() => console.log('Multi-threaded application finished'))
  .catch(error => console.error('Application failed:', error))

This advanced example demonstrates:

  • Multi-threading with specialized worker threads for CPU and I/O tasks
  • Parallel processing with coordinated task execution
  • Inter-thread communication via host functions
  • Resource monitoring across all VM instances and threads
  • Thread lifecycle management with automatic cleanup
  • Error handling with graceful degradation
  • Performance monitoring with real-time metrics
  • Complex coordination between main VM and worker threads

Core API

VM Class

The main class for creating and managing virtual machines.

class VM {
  constructor(libraries: VMLibrary[], options?: VMOptions)
  async initialize(): Promise<void>
  async run(code: string): Promise<void>
  dispose(): void
}

VMOptions

interface VMOptions {
  memoryLimit?: number;           // Memory limit in MB (default: 64)
  onError?: (error: Error) => void;
  onCatastrophicError?: (error: string) => void;
  onEnd?: () => void;
  onDispose?: () => void;
}

Example with Options

const vm = new VM([consoleLib, ipcRxLib, ipcTxLib], {
  memoryLimit: 128, // 128MB limit
  onError: (error) => console.error('VM Error:', error),
  onCatastrophicError: (error) => console.error('Catastrophic:', error),
  onEnd: () => console.log('VM execution completed'),
  onDispose: () => console.log('VM disposed')
})

Library Exports

The package exports the following main classes:

import {
  // Core VM class
  VM,

  // Built-in libraries
  VMConsoleLibrary,      // Console logging support
  VMInjectLibrary,       // Variable/function injection
  VMIpcRxLibrary,        // VM calls host functions
  VMIpcLibrary,          // Host calls VM functions (also known as VMIpcTxLibrary)
  VMTimingLibrary,       // setTimeout/setInterval support
  VMCodeBaseLibrary,     // Virtual filesystem with memfs and module system
  VMMeasureLibrary,      // Performance monitoring and resource usage
  VMThreadLibrary,       // Thread management utilities

  // Base classes for custom libraries
  VMLibrary,
  VMLibraryGroup
} from '@david.uhlir/vm-machine'

Note: VMIpcLibrary handles host-to-VM communication (calling VM functions from host), while VMIpcRxLibrary handles VM-to-host communication (VM calling host functions).

Built-in Libraries

VMConsoleLibrary

Provides console logging functionality to the VM.

class VMConsoleLibrary extends VMLibrary {
  constructor(writeLog?: (level: string, ...args: any[]) => void)
}

Usage

// Default console output
const consoleLib = new VMConsoleLibrary()

// Custom logger
const consoleLib = new VMConsoleLibrary((level, ...args) => {
  console.log(`[VM-${level.toUpperCase()}]`, ...args)
})

const code = `
  console.log('Info message');
  console.error('Error message');
  console.warn('Warning message');
`

VMIpcRxLibrary & VMIpcLibrary (IPC Communication)

IPC (Inter-Process Communication) is split into two complementary libraries for clear separation of concerns:

  • VMIpcRxLibrary: Provides functions that the VM can call on the host (VM receives/calls host functions)
  • VMIpcLibrary: Allows the host to call functions defined in the VM with timeout support (host transmits calls to VM)

VMIpcRxLibrary

Provides callable functions from VM to host environment.

class VMIpcRxLibrary extends VMLibrary {
  constructor(callables: { [name: string]: (...args: any[]) => Promise<any> })
}

VMIpcLibrary

Allows host to call VM functions with timeout support.

class VMIpcLibrary extends VMLibrary {
  constructor(options?: { timeout?: number })
  async callIpc(name: string, args: any[]): Promise<any>
}

Complete IPC Example

// Host functions callable from VM
const ipcRxLib = new VMIpcRxLibrary({
  fetchUser: async (id: number) => {
    return await database.users.findById(id)
  },
  saveData: async (data: any) => {
    return await database.save(data)
  },
  delay: async (ms: number) => {
    await new Promise(resolve => setTimeout(resolve, ms))
  }
})

// VM function call handler with timeout
const ipcTxLib = new VMIpcLibrary({
  timeout: 10000 // 10 second timeout for VM function calls
})

const vm = new VM([ipcRxLib, ipcTxLib])
await vm.initialize()

const code = `
  // VM can call host functions (via VMIpcRxLibrary)
  async function processUser(userId) {
    const user = await fetchUser(userId); // Calls host function
    await delay(100); // Calls host function

    const processed = {
      ...user,
      processedAt: new Date().toISOString()
    };

    await saveData(processed); // Calls host function
    return processed;
  }

  // Return functions that host can call
  return { processUser };
`

await vm.run(code)

// Host calls VM function (via VMIpcLibrary)
const result = await ipcTxLib.callIpc('processUser', [123])

Understanding the IPC Architecture

The IPC system is designed with clear directional responsibilities:

VMIpcRxLibrary (VM Receives/Calls Host Functions)

  • VM can call functions defined on the host
  • Functions are injected into VM global scope
  • Host provides the callable implementations
  • Async execution with error propagation

VMIpcLibrary (Host Transmits Calls to VM Functions)

  • Host can call functions returned by VM code
  • Functions must be returned in VM's return object
  • Supports timeout configuration for long-running VM functions
  • Bidirectional error handling

Important Notes:

  • VM functions called by host (via VMIpcLibrary) must be async
  • Host functions called by VM (via VMIpcRxLibrary) must return promises
  • Both libraries can be used together for full bidirectional communication
  • Each library handles its own error states and timeouts

VMInjectLibrary

Injects variables and functions into the VM global scope.

class VMInjectLibrary extends VMLibrary {
  constructor(injectables: { [name: string]: any })
}

Usage

const injectLib = new VMInjectLibrary({
  API_URL: 'https://api.example.com',
  version: '1.0.0',
  config: {
    retries: 3,
    timeout: 5000
  },
  helperFunction: (x: number) => x * 2
})

const code = `
  console.log('API URL:', API_URL);
  console.log('Version:', version);
  console.log('Config:', config);

  const doubled = helperFunction(21); // 42
  return { doubled, config };
`

VMTimingLibrary

Provides setTimeout and setInterval functionality with resource limits.

class VMTimingLibrary extends VMLibrary {
  constructor(options?: VMTimingLibraryOptions)
}

interface VMTimingLibraryOptions {
  limitCount?: number // Maximum number of concurrent timers (default: 100)
}

Usage

const timingLib = new VMTimingLibrary({
  limitCount: 50 // Limit to 50 concurrent timers
})

const code = `
  let counter = 0;

  const intervalId = setInterval(() => {
    console.log('Counter:', ++counter);
    if (counter >= 3) {
      clearInterval(intervalId);
    }
  }, 1000);

  setTimeout(() => {
    console.log('Delayed execution');
  }, 2500);

  return { started: true };
`

VMCodeBaseLibrary

Provides a virtual filesystem using memfs and a complete module system for running JavaScript projects inside the VM. This is essential for running complex applications that need file system access and module imports.

class VMCodeBaseLibrary extends VMLibraryGroup {
  constructor(options?: VMCodeBaseModulesLibraryOptions)
  setEntryFile(entryFile: string): void
  getFs(): IFs
  addModule(module: BaseCodebaseModule): void
  addModule(name: string, source: string): void
}

interface VMCodeBaseModulesLibraryOptions {
  mockModulesList?: string[]     // List of Node.js modules to mock (default: builtin modules)
  moduleResolver?: (name: string) => Promise<string | undefined>
  maxMemfsSize?: number          // Maximum filesystem size in bytes (default: 10MB)
  maxFileCount?: number          // Maximum number of files (default: 1000)
}

Key Features

  • Virtual Filesystem: Complete filesystem implementation using memfs
  • Module System: Support for require() and ES modules with relative/absolute imports
  • Resource Limits: Configurable memory and file count limits for security
  • Mock Modules: Automatic mocking of Node.js built-in modules
  • File Operations: Full fs/promises API support within the VM

Usage

const codebaseLib = new VMCodeBaseLibrary({
  maxMemfsSize: 5 * 1024 * 1024, // 5MB limit
  maxFileCount: 500,
  mockModulesList: ['fs', 'path', 'crypto'] // Mock these modules
})

// Add files to the virtual filesystem
const fs = codebaseLib.getFs()
await fs.promises.writeFile('/src/utils.js', `
  module.exports = {
    add: (a, b) => a + b,
    multiply: (a, b) => a * b
  };
`)

await fs.promises.writeFile('/src/main.js', `
  const utils = require('./utils');
  const fs = require('fs/promises');

  async function main() {
    console.log('Sum:', utils.add(5, 3));

    // Write result to virtual filesystem
    await fs.writeFile('/output.txt', 'Result: ' + utils.multiply(4, 7));

    return { success: true };
  }

  module.exports = { main };
`)

// Set entry file to auto-execute
codebaseLib.setEntryFile('/src/main.js')

const vm = new VM([codebaseLib, new VMConsoleLibrary()])
await vm.initialize()
await vm.run() // Will automatically execute main.js

// Access files created by VM code
const output = await fs.promises.readFile('/output.txt', 'utf-8')
console.log(output) // "Result: 28"

Advanced Module System Usage

// Custom module resolver
const codebaseLib = new VMCodeBaseLibrary({
  moduleResolver: async (name) => {
    if (name === 'lodash') {
      return `module.exports = { map: (arr, fn) => arr.map(fn) };`
    }
    if (name.startsWith('@mycompany/')) {
      // Load from external source
      return await loadFromDatabase(name)
    }
    return undefined
  }
})

// Add modules programmatically
codebaseLib.addModule('config', `
  module.exports = {
    apiUrl: 'https://api.example.com',
    timeout: 5000
  };
`)

const code = `
  const config = require('config');
  const _ = require('lodash'); // Resolved by custom resolver
  const myLib = require('@mycompany/utils'); // Also resolved by custom resolver

  const numbers = [1, 2, 3, 4, 5];
  const doubled = _.map(numbers, x => x * 2);

  return { doubled, config };
`

VMThreadLibrary

Provides advanced multi-threading capabilities with isolated VM contexts for each thread. Each thread runs in its own VM with independent memory limits and can communicate bidirectionally with the main VM and host.

class VMThreadLibrary extends VMLibraryGroup {
  constructor(options: VMThreadLibraryOptions)
  getThreads(): Map<string, ThreadInfo>
}

interface VMThreadLibraryOptions {
  threadMemoryLimit?: number                    // Memory limit per thread in MB (default: 16)
  threadsCountLimit?: number                    // Maximum number of concurrent threads
  getThreadLibraries: (threadId: string) => VMLibrary[]  // Libraries for each thread
}

interface ThreadInfo {
  vm: VM
  tx: VMIpcTxLibrary  // For calling thread functions
  rx: VMIpcRxLibrary  // For thread calling host functions
}

Key Features

  • Isolated Threads: Each thread runs in its own V8 isolate with independent memory
  • Bidirectional Communication: Threads can call host functions and host can call thread functions
  • Automatic Management: Built-in watchdog for thread health monitoring
  • Resource Limits: Configurable memory and thread count limits
  • Dynamic Loading: Threads can be created from filesystem or inline code
  • Lifecycle Management: Automatic cleanup and disposal

Basic Usage

const codebaseLib = new VMCodeBaseLibrary()
const fs = codebaseLib.getFs()

// Create thread file
await fs.promises.writeFile('/worker.js', `
  console.log('Worker thread started!');

  // Expose functions that main VM can call
  exposeCallables({
    processData: async (data) => {
      console.log('Processing data in thread:', data);

      // Thread can call host functions
      const result = await host.saveToDatabase({
        threadId: global.isThread,
        processedData: data.map(x => x * 2)
      });

      return { success: true, result };
    },

    heavyComputation: async (numbers) => {
      // CPU-intensive work in isolated thread
      return numbers.reduce((sum, n) => sum + Math.sqrt(n * n), 0);
    }
  });
`)

// Create main application
await fs.promises.writeFile('/main.js', `
  console.log('Main application started');

  async function main() {
    // Start a worker thread
    const worker = await startThread('/worker');
    console.log('Worker thread ID:', worker.threadId);

    // Call thread functions
    const result1 = await worker.processData([1, 2, 3, 4, 5]);
    console.log('Processing result:', result1);

    const result2 = await worker.heavyComputation([100, 200, 300]);
    console.log('Computation result:', result2);

    // Clean up thread
    setTimeout(() => {
      console.log('Disposing worker thread');
      worker.dispose();
    }, 5000);
  }

  main();
`)

const threadLib = new VMThreadLibrary({
  threadMemoryLimit: 32, // 32MB per thread
  threadsCountLimit: 10, // Max 10 concurrent threads
  getThreadLibraries: (threadId) => [
    new VMConsoleLibrary((level, ...args) =>
      console.log(`[THREAD-${threadId}]`, ...args)
    ),
    new VMTimingLibrary(),
    // Add other libraries as needed
  ]
})

const vm = new VM([
  new VMConsoleLibrary(),
  codebaseLib,
  threadLib,
  new VMIpcRxLibrary({
    saveToDatabase: async (data) => {
      console.log('Host saving data:', data);
      return { id: Date.now(), saved: true };
    }
  })
])

codebaseLib.setEntryFile('/main.js')
await vm.initialize()
await vm.run()

Advanced Thread Communication

const threadLib = new VMThreadLibrary({
  getThreadLibraries: (threadId) => [
    new VMConsoleLibrary(),
    new VMIpcRxLibrary({
      // Host functions available to threads
      getDatabaseConnection: async () => ({ connected: true }),
      logMessage: async (message) => console.log(`Thread ${threadId}:`, message),
      notifyComplete: async (result) => {
        console.log(`Thread ${threadId} completed:`, result);
        return { acknowledged: true };
      }
    })
  ]
})

// Complex worker thread
await fs.promises.writeFile('/data-processor.js', `
  let processingCount = 0;

  exposeCallables({
    startProcessing: async (config) => {
      const db = await host.getDatabaseConnection();
      await host.logMessage('Starting data processing with config: ' + JSON.stringify(config));

      // Simulate processing
      const results = [];
      for (let i = 0; i < config.itemCount; i++) {
        processingCount++;

        // Simulate work
        await new Promise(resolve => setTimeout(resolve, config.delay || 100));

        results.push({
          id: i,
          processed: true,
          timestamp: Date.now(),
          threadCount: processingCount
        });

        // Progress updates
        if (i % 10 === 0) {
          await host.logMessage(\`Progress: \${i}/\${config.itemCount}\`);
        }
      }

      await host.notifyComplete({ totalProcessed: results.length });
      return results;
    },

    getStatus: async () => ({
      processingCount,
      isAlive: true,
      memoryUsage: 'simulated-memory-info'
    })
  });
`)

// Main coordination logic
await fs.promises.writeFile('/coordinator.js', `
  const workers = [];
  const results = [];

  async function spawnWorkers(count = 3) {
    for (let i = 0; i < count; i++) {
      const worker = await startThread('/data-processor');
      workers.push(worker);
      console.log(\`Spawned worker \${i + 1}/\${count}: \${worker.threadId}\`);
    }
  }

  async function distributeWork() {
    const tasks = workers.map((worker, index) =>
      worker.startProcessing({
        itemCount: 50,
        delay: 50 + (index * 20), // Stagger processing
        workerId: index
      })
    );

    const workerResults = await Promise.all(tasks);
    results.push(...workerResults.flat());

    console.log(\`All workers completed. Total items processed: \${results.length}\`);
  }

  async function monitorWorkers() {
    const statuses = await Promise.all(
      workers.map(worker => worker.getStatus())
    );

    console.log('Worker statuses:', statuses);
  }

  async function cleanup() {
    console.log('Cleaning up workers...');
    workers.forEach(worker => worker.dispose());
  }

  async function main() {
    await spawnWorkers(3);
    await distributeWork();
    await monitorWorkers();

    setTimeout(cleanup, 1000);
  }

  main();
`)

Thread Lifecycle and Health Monitoring

// The VMThreadLibrary automatically monitors thread health
const threadLib = new VMThreadLibrary({
  threadMemoryLimit: 64,
  threadsCountLimit: 5,
  getThreadLibraries: (threadId) => [
    new VMConsoleLibrary(),
    new VMMeasureLibrary({
      responseTimeLimit: 3000 // Kill thread if response > 3s
    })
  ]
})

// Monitor thread health
setInterval(() => {
  const threads = threadLib.getThreads()
  console.log(`Active threads: ${threads.size}`)

  threads.forEach((thread, threadId) => {
    if (thread.vm.isDisposed) {
      console.log(`Thread ${threadId} is disposed`)
    } else {
      console.log(`Thread ${threadId} is running`)
    }
  })
}, 2000)

// Thread with error handling
await fs.promises.writeFile('/error-prone-worker.js', `
  let operationCount = 0;

  exposeCallables({
    riskyOperation: async (shouldFail = false) => {
      operationCount++;

      if (shouldFail && operationCount > 3) {
        throw new Error('Simulated thread error');
      }

      // Simulate long-running operation
      await new Promise(resolve => setTimeout(resolve, 1000));

      return {
        success: true,
        operationCount,
        timestamp: Date.now()
      };
    }
  });
`)

Performance and Resource Management

// Resource monitoring with thread library
const measureMaster = new VMMeasureMasterLibrary()

const threadLib = new VMThreadLibrary({
  threadMemoryLimit: 32,
  getThreadLibraries: (threadId) => [
    new VMConsoleLibrary(),
    measureMaster.getNewLibrary(`thread-${threadId}`) // Monitor each thread
  ]
})

// Monitor overall performance
setInterval(async () => {
  await measureMaster.pingAll()
  const stats = await measureMaster.getCombinedStats()

  console.log('System Performance:', {
    totalMemoryUsed: `${(stats.usedMemory / 1024 / 1024).toFixed(2)} MB`,
    memoryPercentage: `${stats.memoryUsedPercentage.toFixed(1)}%`,
    averageResponseTime: `${stats.responseAverageTime.toFixed(2)}ms`,
    slowestResponseTime: `${stats.responseTime}ms`,
    managedInstances: stats.managedCount
  })
}, 2000)

// Thread pool pattern
await fs.promises.writeFile('/thread-pool.js', `
  const pool = [];
  const maxPoolSize = 3;

  async function getWorker() {
    if (pool.length > 0) {
      return pool.pop();
    }

    if (pool.length < maxPoolSize) {
      const worker = await startThread('/data-processor');
      return worker;
    }

    throw new Error('Thread pool exhausted');
  }

  function releaseWorker(worker) {
    if (!worker.isDisposed && pool.length < maxPoolSize) {
      pool.push(worker);
    } else {
      worker.dispose();
    }
  }

  async function processWithPool(data) {
    const worker = await getWorker();

    try {
      const result = await worker.processData(data);
      releaseWorker(worker);
      return result;
    } catch (error) {
      worker.dispose(); // Don't return failed workers to pool
      throw error;
    }
  }

  // Process multiple items using thread pool
  const tasks = Array.from({length: 10}, (_, i) =>
    processWithPool(\`data-item-\${i}\`)
  );

  const results = await Promise.all(tasks);
  console.log('Pool processing complete:', results.length);
`)

VMMeasureLibrary

Provides performance monitoring, resource usage tracking, and response time measurement.

class VMMeasureLibrary extends VMLibrary {
  constructor(options?: VMMeasureLibraryOptions, onDispose?: () => void)
  async getUsageStats(): Promise<UsageStats>
  ping(): void
  getResponseTime(): number
}

interface VMMeasureLibraryOptions {
  responseTimeLimit?: number // Maximum allowed response time in milliseconds
}

interface UsageStats {
  usedMemory: number
  totalMemory: number
  memoryUsedPercentage: number
  responseTime: number
}

Usage

const measureLib = new VMMeasureLibrary({
  responseTimeLimit: 1000 // Panic stop if response time > 1000ms
})

const vm = new VM([measureLib, new VMConsoleLibrary()])
await vm.initialize()

// Monitor VM performance
setInterval(async () => {
  measureLib.ping() // Send ping to measure response time

  const stats = await measureLib.getUsageStats()
  console.log('VM Stats:', {
    memoryUsed: `${(stats.usedMemory / 1024 / 1024).toFixed(2)} MB`,
    memoryPercentage: `${stats.memoryUsedPercentage.toFixed(2)}%`,
    responseTime: `${stats.responseTime}ms`
  })
}, 1000)

const code = `
  // VM code can also trigger pings
  global.__ping(); // Manually trigger ping from VM

  // Simulate work
  const data = new Array(1000000).fill(0).map((_, i) => i * i);

  return { processed: data.length };
`

Advanced Monitoring with VMMeasureMasterLibrary

const masterLib = new VMMeasureMasterLibrary({
  responseTimeLimit: 2000
})

// Create multiple monitored VMs
const vm1Lib = masterLib.getNewLibrary('worker-1')
const vm2Lib = masterLib.getNewLibrary('worker-2')

const vm1 = new VM([vm1Lib], { memoryLimit: 32 })
const vm2 = new VM([vm2Lib], { memoryLimit: 64 })

// Monitor all VMs together
const overallStats = await masterLib.getOverallUsageStats()
console.log('Overall VM Performance:', {
  totalMemory: overallStats.usedMemory,
  individual: overallStats.nested
})

const combinedStats = await masterLib.getCombinedStats()
console.log('Combined Stats:', {
  totalMemoryUsed: combinedStats.usedMemory,
  averageResponseTime: combinedStats.responseAverageTime,
  slowestResponseTime: combinedStats.responseTime,
  managedVMs: combinedStats.managedCount
})

Creating Custom Libraries

You can create custom libraries in several ways depending on your needs:

1. Extending VMLibrary Base Class

For complete control over library functionality:

import { VMLibrary } from '@david.uhlir/vm-machine'

class CustomMathLibrary extends VMLibrary {
  getReferences() {
    return {
      __advancedMath: (operation: string, ...args: number[]) => {
        try {
          switch (operation) {
            case 'fibonacci':
              return this.fibonacci(args[0])
            case 'factorial':
              return this.factorial(args[0])
            default:
              throw new Error(`Unknown operation: ${operation}`)
          }
        } catch (error) {
          this.onError?.(error as Error)
          return null
        }
      }
    }
  }

  getVmCode(): string {
    return `
      global.fibonacci = (n) => __advancedMath('fibonacci', n);
      global.factorial = (n) => __advancedMath('factorial', n);
    `
  }

  dispose(): void {
    // Cleanup resources
  }

  private fibonacci(n: number): number {
    if (n <= 1) return n
    return this.fibonacci(n - 1) + this.fibonacci(n - 2)
  }

  private factorial(n: number): number {
    if (n <= 1) return 1
    return n * this.factorial(n - 1)
  }
}

2. Extending VMIpcRxLibrary for Async Functions

When you need to provide async functions to the VM, extend VMIpcRxLibrary. This is the most common and recommended approach:

import { VMIpcRxLibrary } from '@david.uhlir/vm-machine'
import crypto from 'crypto'
import fs from 'fs/promises'

class CustomCryptoLibrary extends VMIpcRxLibrary {
  constructor() {
    super({
      // All functions must return Promise<any>
      randomHash: async (): Promise<string> => {
        return crypto.randomBytes(16).toString('hex')
      },

      sha256: async (data: string | Buffer): Promise<string> => {
        return crypto.createHash('sha256').update(data).digest('hex')
      },

      encrypt: async (text: string, secret: string): Promise<string> => {
        const key = crypto.createHash('sha256').update(secret).digest()
        const iv = crypto.randomBytes(16)
        const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
        let encrypted = cipher.update(text, 'utf8', 'base64')
        encrypted += cipher.final('base64')
        return iv.toString('base64') + ':' + encrypted
      },

      decrypt: async (encrypted: string, secret: string): Promise<string> => {
        const key = crypto.createHash('sha256').update(secret).digest()
        const [ivBase64, encryptedData] = encrypted.split(':')
        const iv = Buffer.from(ivBase64, 'base64')
        const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
        let decrypted = decipher.update(encryptedData, 'base64', 'utf8')
        decrypted += decipher.final('utf8')
        return decrypted
      }
    })
  }

  // Override getVmCode to provide clean API
  public getVmCode(): string {
    const originalCode = super.getVmCode()
    return `
      ${originalCode};
      // Create clean crypto API
      global.crypto = {
        randomHash: global.randomHash,
        sha256: global.sha256,
        encrypt: global.encrypt,
        decrypt: global.decrypt
      };
    `
  }
}

// Usage in VM code:
const code = `
  const hash = await crypto.randomHash();
  const digest = await crypto.sha256('hello world');
  const encrypted = await crypto.encrypt('secret data', 'password');
  const decrypted = await crypto.decrypt(encrypted, 'password');

  return { hash, digest, encrypted, decrypted };
`

3. Advanced Example: File System Library

import { VMIpcRxLibrary } from '@david.uhlir/vm-machine'
import fs from 'fs/promises'
import path from 'path'

class CustomFileSystemLibrary extends VMIpcRxLibrary {
  private allowedPaths: string[]

  constructor(allowedPaths: string[] = []) {
    super({
      readFile: async (filePath: string): Promise<string> => {
        this.validatePath(filePath)
        return await fs.readFile(filePath, 'utf-8')
      },

      writeFile: async (filePath: string, content: string): Promise<void> => {
        this.validatePath(filePath)
        await fs.writeFile(filePath, content, 'utf-8')
      },

      listDirectory: async (dirPath: string): Promise<string[]> => {
        this.validatePath(dirPath)
        return await fs.readdir(dirPath)
      },

      getStats: async (filePath: string): Promise<any> => {
        this.validatePath(filePath)
        const stats = await fs.stat(filePath)
        return {
          size: stats.size,
          isFile: stats.isFile(),
          isDirectory: stats.isDirectory(),
          modifiedTime: stats.mtime,
          createdTime: stats.birthtime
        }
      }
    })

    this.allowedPaths = allowedPaths.map(p => path.resolve(p))
  }

  private validatePath(filePath: string): void {
    const resolvedPath = path.resolve(filePath)
    const isAllowed = this.allowedPaths.some(allowedPath =>
      resolvedPath.startsWith(allowedPath)
    )

    if (!isAllowed) {
      throw new Error(`Access denied: ${filePath} is outside allowed paths`)
    }
  }

  public getVmCode(): string {
    const originalCode = super.getVmCode()
    return `
      ${originalCode};
      global.fs = {
        readFile: global.readFile,
        writeFile: global.writeFile,
        listDirectory: global.listDirectory,
        getStats: global.getStats
      };
    `
  }
}

// Usage
const fsLib = new CustomFileSystemLibrary(['/tmp', '/var/data'])
const vm = new VM([fsLib])

const code = `
  const files = await fs.listDirectory('/tmp');
  const content = await fs.readFile('/tmp/test.txt');
  await fs.writeFile('/tmp/output.txt', 'Hello World');

  return { files, content };
`

4. Database Access Library

import { VMIpcRxLibrary } from '@david.uhlir/vm-machine'

interface DatabaseConfig {
  host: string
  database: string
  // ... other config
}

class CustomDatabaseLibrary extends VMIpcRxLibrary {
  private db: any // Your database connection

  constructor(config: DatabaseConfig) {
    super({
      query: async (sql: string, params: any[] = []): Promise<any[]> => {
        // Validate SQL for security
        this.validateSQL(sql)
        return await this.db.query(sql, params)
      },

      insert: async (table: string, data: Record<string, any>): Promise<number> => {
        this.validateTableName(table)
        const columns = Object.keys(data).join(', ')
        const placeholders = Object.keys(data).map(() => '?').join(', ')
        const values = Object.values(data)

        const result = await this.db.query(
          `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`,
          values
        )
        return result.insertId
      },

      select: async (table: string, where: Record<string, any> = {}): Promise<any[]> => {
        this.validateTableName(table)
        const whereClause = Object.keys(where).length > 0
          ? 'WHERE ' + Object.keys(where).map(key => `${key} = ?`).join(' AND ')
          : ''

        return await this.db.query(
          `SELECT * FROM ${table} ${whereClause}`,
          Object.values(where)
        )
      },

      update: async (table: string, data: Record<string, any>, where: Record<string, any>): Promise<number> => {
        this.validateTableName(table)
        const setClause = Object.keys(data).map(key => `${key} = ?`).join(', ')
        const whereClause = Object.keys(where).map(key => `${key} = ?`).join(' AND ')

        const result = await this.db.query(
          `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`,
          [...Object.values(data), ...Object.values(where)]
        )
        return result.affectedRows
      }
    })

    // Initialize database connection
    this.initDatabase(config)
  }

  private validateSQL(sql: string): void {
    // Basic SQL injection protection
    const forbidden = ['DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE']
    const upperSQL = sql.toUpperCase()

    if (forbidden.some(keyword => upperSQL.includes(keyword))) {
      throw new Error('Forbidden SQL operation')
    }
  }

  private validateTableName(table: string): void {
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
      throw new Error('Invalid table name')
    }
  }

  private async initDatabase(config: DatabaseConfig): Promise<void> {
    // Initialize your database connection here
  }

  public getVmCode(): string {
    const originalCode = super.getVmCode()
    return `
      ${originalCode};
      global.db = {
        query: global.query,
        insert: global.insert,
        select: global.select,
        update: global.update
      };
    `
  }

  public dispose(): void {
    super.dispose()
    if (this.db) {
      this.db.close()
    }
  }
}

// Usage
const dbLib = new CustomDatabaseLibrary({
  host: 'localhost',
  database: 'myapp'
})

const code = `
  // Insert user
  const userId = await db.insert('users', {
    name: 'John Doe',
    email: '[email protected]'
  });

  // Select users
  const users = await db.select('users', { active: 1 });

  // Update user
  await db.update('users', { last_login: new Date() }, { id: userId });

  return { userId, users };
`

Key Principles for Custom Libraries

  1. Extend VMIpcRxLibrary for async functions - this is the most common pattern
  2. All callable functions must return Promise when extending VMIpcRxLibrary
  3. Override getVmCode() to provide clean APIs in the VM
  4. Add security validation for any external access (files, database, network)
  5. Implement proper disposal to clean up resources
  6. Handle errors gracefully with try-catch blocks
  7. Use meaningful function names that will be available in VM global scope

Security Considerations

Memory Limits

Always set appropriate memory limits to prevent resource exhaustion:

const vm = new VM([...libraries], {
  memoryLimit: 64, // MB
  onCatastrophicError: (error) => {
    console.error('Memory or security violation:', error)
    // Handle critical errors
  }
})

Input Validation

Validate all data passed between host and VM:

const ipcRxLib = new VMIpcRxLibrary({
  processData: async (data: unknown) => {
    // Validate input
    if (typeof data !== 'object' || data === null) {
      throw new Error('Invalid input: expected object')
    }

    // Process safely
    return { processed: true, data }
  }
})

Error Boundaries

Implement comprehensive error handling:

const vm = new VM([...libraries], {
  onError: (error) => {
    console.error('VM Runtime Error:', error.message)
    // Log, report, or handle gracefully
  },
  onCatastrophicError: (error) => {
    console.error('Critical VM Error:', error)
    // Emergency cleanup, restart, or shutdown
  }
})

Architecture & Internal Mechanisms

Core VM Architecture

The VM-machine is built on top of Node.js isolated-vm which provides true V8 isolates for secure code execution. Here's how the system works internally:

Isolated V8 Context

// Each VM instance creates its own V8 isolate
const isolate = new ivm.Isolate({
  memoryLimit: 64, // MB
  onCatastrophicError: (error) => {
    // Handle memory violations, infinite loops, etc.
  }
})

// Context provides the execution environment
const context = await isolate.createContext()

Library System

Libraries extend functionality by injecting host functions and VM code:

  1. Host References: Functions callable from VM (via getReferences())
  2. VM Code: JavaScript code injected into VM context (via getVmCode())
  3. Lifecycle Hooks: attach(), start(), afterStart(), dispose()

Communication Bridge

The system uses several mechanisms for host-VM communication:

Synchronous Communication:

  • Direct function calls via ivm.Reference
  • Immediate data transfer with copy semantics

Asynchronous Communication:

  • Promise-based function calls
  • Callback systems for timers and events

Shared Memory:

  • SharedArrayBuffer for blocking operations (used in module system)
  • Atomics for synchronization between async/sync boundaries

Module System Deep Dive

The VMCodeBaseLibrary implements a sophisticated module system that bridges the gap between Node.js-style require() and VM isolation:

Async-to-Sync Bridge

// Problem: require() is synchronous, but we need async module loading
// Solution: SharedArrayBuffer + Atomics for blocking operations

// 1. VM calls __requireSource(name) - starts async operation
global.__requireSource(name);

// 2. VM blocks using Atomics.wait until host loads module
const res = Atomics.wait(__codebaseSABReference, 0, 0);

// 3. Host loads module asynchronously and signals completion
this.synchronizationArray[0] = 1;
Atomics.notify(this.synchronizationArray, 0, 1);

// 4. VM continues and retrieves loaded code
const code = global.__requireSourceGetLastResult();

Module Resolution Algorithm

  1. Custom Resolver: Check if options.moduleResolver can handle the module
  2. Added Modules: Check modules added via addModule()
  3. Filesystem: Look for files in the virtual filesystem (memfs)
  4. Path Normalization: Handle relative/absolute paths and file extensions

Virtual Filesystem Security

  • Memory Limits: Configurable max size and file count
  • Read-only Protection: Prevent modification of core files
  • Access Control: Whitelist of allowed filesystem operations

Performance Monitoring System

Response Time Measurement

// Host sends ping timestamp
measureLib.ping() // Sets this.lastPingTime = Date.now()

// VM responds with pong
global.__ping() // Calls global.__pong()

// Host calculates round-trip time
this.lastResponseTime = Date.now() - this.lastPingTime

Memory Tracking

// Get real-time V8 heap statistics
const heap = await isolate.getHeapStatistics()
return {
  usedMemory: heap.used_heap_size,
  totalMemory: heap.heap_size_limit,
  memoryUsedPercentage: (heap.used_heap_size / heap.heap_size_limit) * 100
}

Multi-VM Monitoring

The VMMeasureMasterLibrary provides centralized monitoring of multiple VM instances with aggregated statistics and resource tracking.

Security Model

Isolation Guarantees

  • Memory Isolation: Each VM has its own V8 heap with configurable limits
  • CPU Isolation: Catastrophic error handling for infinite loops/recursion
  • Filesystem Isolation: Virtual filesystem with no access to host filesystem
  • Network Isolation: No direct network access (only through host-provided functions)

Resource Management

  • Memory Limits: Hard limits enforced by V8 isolate
  • Timer Limits: Configurable maximum number of concurrent timers
  • File Limits: Maximum virtual filesystem size and file count
  • Response Time Limits: Automatic panic stop for unresponsive code

Error Boundaries

const vm = new VM([...libraries], {
  onError: (error) => {
    // Recoverable errors (syntax, runtime, etc.)
  },
  onCatastrophicError: (error) => {
    // Critical errors requiring VM termination
    // Memory violations, infinite loops, etc.
  }
})

Advanced Usage

Safe Code Execution with Data Processing

import { VM, VMConsoleLibrary, VMInjectLibrary, VMIpcRxLibrary, VMIpcLibrary } from '@david.uhlir/vm-machine'

class SafeCodeRunner {
  async executeUserCode(userCode: string, inputData: any): Promise<any> {
    // Host functions available to VM
    const ipcRxLib = new VMIpcRxLibrary({
      // Provide safe data access
      getData: async (key: string) => {
        // Validate and return data safely
        if (key === 'input') return inputData
        throw new Error(`Access denied to: ${key}`)
      },
      // Provide safe logging
      logResult: async (message: string) => {
        console.log('VM Result:', message)
        return { logged: true }
      }
    })

    // VM function caller
    const ipcTxLib = new VMIpcLibrary({ timeout: 5000 })

    // Inject safe constants
    const injectLib = new VMInjectLibrary({
      MAX_ITEMS: 1000,
      API_VERSION: '1.0.0'
    })

    const vm = new VM([
      new VMConsoleLibrary(),
      ipcRxLib,
      ipcTxLib,
      injectLib
    ], {
      memoryLimit: 32, // Limit memory
      onError: (error) => console.error('VM Error:', error)
    })

    try {
      await vm.initialize()
      await vm.run(userCode)

      // Call the main function if it exists
      const result = await ipcTxLib.callIpc('main', [])
      return { success: true, result }
    } catch (error) {
      return { success: false, error: error.message }
    } finally {
      vm.dispose()
    }
  }
}

// Usage example
const runner = new SafeCodeRunner()

const userCode = `
  async function main() {
    console.log('Processing data with version:', API_VERSION);

    // Get input data from host
    const data = await getData('input');

    // Process the data safely
    const processed = data.map(x => x * 2).slice(0, MAX_ITEMS);

    // Log result
    await logResult(\`Processed \${processed.length} items\`);

    return {
      processed,
      count: processed.length,
      version: API_VERSION
    };
  }

  return { main };
`

const result = await runner.executeUserCode(userCode, [1, 2, 3, 4, 5])
console.log(result)
// Output: { success: true, result: { processed: [2, 4, 6, 8, 10], count: 5, version: '1.0.0' } }

Mathematical Expression Evaluator

class MathEvaluator {
  async evaluateExpression(expression: string, variables: Record<string, number> = {}): Promise<number> {
    // Provide math functions to VM
    const ipcRxLib = new VMIpcRxLibrary({
      // Safe math operations
      pow: async (base: number, exponent: number) => Math.pow(base, exponent),
      sqrt: async (x: number) => Math.sqrt(x),
      sin: async (x: number) => Math.sin(x),
      cos: async (x: number) => Math.cos(x),
      log: async (x: number) => Math.log(x)
    })

    const ipcTxLib = new VMIpcLibrary()

    // Inject variables
    const injectLib = new VMInjectLibrary(variables)

    const vm = new VM([ipcRxLib, ipcTxLib, injectLib], {
      memoryLimit: 8, // Very small for math operations
      onError: (error) => { throw new Error(`Math evaluation error: ${error.message}`) }
    })

    try {
      await vm.initialize()

      const code = `
        async function evaluate() {
          // User's expression is evaluated here safely
          return ${expression};
        }
        return { evaluate };
      `

      await vm.run(code)
      return await ipcTxLib.callIpc('evaluate', [])
    } finally {
      vm.dispose()
    }
  }
}

// Usage
const evaluator = new MathEvaluator()

// Simple expression
let result = await evaluator.evaluateExpression('2 + 3 * 4') // 14

// With variables
result = await evaluator.evaluateExpression('x * y + z', { x: 5, y: 10, z: 2 }) // 52

// With math functions
result = await evaluator.evaluateExpression('await pow(2, 3) + await sqrt(16)') // 12

Template Processing with Data

class TemplateProcessor {
  async processTemplate(template: string, data: any): Promise<string> {
    const ipcRxLib = new VMIpcRxLibrary({
      // Provide data access
      getValue: async (path: string) => {
        // Safe property access using path like 'user.name'
        return path.split('.').reduce((obj, key) => obj?.[key], data)
      },
      // Provide utility functions
      formatDate: async (date: string) => new Date(date).toLocaleDateString(),
      uppercase: async (str: string) => str.toUpperCase(),
      lowercase: async (str: string) => str.toLowerCase()
    })

    const ipcTxLib = new VMIpcLibrary()
    const timingLib = new VMTimingLibrary()

    const vm = new VM([ipcRxLib, ipcTxLib, timingLib], {
      memoryLimit: 16,
      onError: (error) => { throw error }
    })

    try {
      await vm.initialize()

      const code = `
        async function processTemplate() {
          // Template processing logic
          let result = \`${template}\`;

          // Replace {{variable}} patterns
          const regex = /\{\{([^}]+)\}\}/g;
          const matches = [];
          let match;

          while ((match = regex.exec(\`${template}\`)) !== null) {
            matches.push({
              full: match[0],
              path: match[1].trim()
            });
          }

          for (const m of matches) {
            let value;

            // Check for function calls like {{formatDate(user.birthdate)}}
            if (m.path.includes('(')) {
              const funcMatch = m.path.match(/(\w+)\(([^)]+)\)/);
              if (funcMatch) {
                const funcName = funcMatch[1];
                const argPath = funcMatch[2];
                const argValue = await getValue(argPath);

                if (funcName === 'formatDate') value = await formatDate(argValue);
                else if (funcName === 'uppercase') value = await uppercase(argValue);
                else if (funcName === 'lowercase') value = await lowercase(argValue);
                else value = argValue;
              }
            } else {
              value = await getValue(m.path);
            }

            result = result.replace(m.full, value || '');
          }

          return result;
        }

        return { processTemplate };
      `

      await vm.run(code)
      return await ipcTxLib.callIpc('processTemplate', [])
    } finally {
      vm.dispose()
    }
  }
}

// Usage
const processor = new TemplateProcessor()

const template = `
Hello {{user.name}}!
Your registration date: {{formatDate(user.registeredAt)}}
Email: {{lowercase(user.email)}}
Status: {{uppercase(user.status)}}
`

const data = {
  user: {
    name: 'John Doe',
    email: '[email protected]',
    status: 'active',
    registeredAt: '2024-01-15'
  }
}

const result = await processor.processTemplate(template, data)
console.log(result)
// Output:
// Hello John Doe!
// Your registration date: 1/15/2024
// Email: [email protected]
// Status: ACTIVE

Performance Tips

  1. Reuse VM instances when possible instead of creating new ones
  2. Set appropriate memory limits based on your use case
  3. Implement timeouts for long-running operations
  4. Use connection pooling for database operations in IPC functions
  5. Monitor memory usage and dispose VMs regularly
  6. Batch operations when processing multiple items

Testing

The library includes comprehensive tests. Run them with:

npm test

Example test structure:

import { expect } from 'chai'
import { VM, VMConsoleLibrary, VMIpcRxLibrary, VMIpcLibrary } from '@david.uhlir/vm-machine'

describe('Custom Tests', () => {
  let vm: VM

  afterEach(() => {
    if (vm) {
      vm.dispose()
      vm = undefined
    }
  })

  it('should execute code safely', async () => {
    const ipcRxLib = new VMIpcRxLibrary({
      testFunction: async () => 'success'
    })
    const ipcTxLib = new VMIpcLibrary()

    vm = new VM([ipcRxLib, ipcTxLib])
    await vm.initialize()

    const code = `
      async function main() {
        return await testFunction();
      }
      return { main };
    `

    await vm.run(code)
    const result = await ipcTxLib.callIpc('main', [])
    expect(result).to.equal('success')
  })
})

Error Handling

The library provides multiple layers of error handling:

VM-level Errors

const vm = new VM([...libraries], {
  onError: (error: Error) => {
    // Runtime errors (syntax, reference errors, etc.)
    console.error('Runtime error:', error.message)
  },
  onCatastrophicError: (error: string) => {
    // Memory violations, security issues
    console.error('Critical error:', error)
    process.exit(1) // May need to restart
  }
})

IPC Errors

const ipcRxLib = new VMIpcRxLibrary({
  riskyOperation: async (data: any) => {
    if (!data) {
      throw new Error('Data is required')
    }
    // This error will be propagated to VM
    return processData(data)
  }
})

const ipcTxLib = new VMIpcLibrary({
  timeout: 5000 // Prevent hanging calls
})

// In VM code:
const code = `
  async function main() {
    try {
      const result = await riskyOperation(null);
      return { success: true, result };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
  return { main };
`

Library Disposal Errors

class SafeCustomLibrary extends VMLibrary {
  dispose(): void {
    try {
      // Cleanup resources
      this.connections?.forEach(conn => conn.close())
      this.timers?.forEach(timer => clearTimeout(timer))
    } catch (error) {
      console.error('Disposal error:', error)
      // Don't throw - allow other libraries to dispose
    }
  }
}

Creating Custom Libraries

You can create custom libraries in several ways depending on your needs:

1. Extending VMLibrary Base Class

For complete control over library functionality:

import { VMLibrary } from '@david.uhlir/vm-machine'

class CustomMathLibrary extends VMLibrary {
  getReferences() {
    return {
      __advancedMath: (operation: string, ...args: number[]) => {
        try {
          switch (operation) {
            case 'fibonacci':
              return this.fibonacci(args[0])
            case 'factorial':
              return this.factorial(args[0])
            default:
              throw new Error(`Unknown operation: ${operation}`)
          }
        } catch (error) {
          throw new Error(`Math operation failed: ${error.message}`)
        }
      }
    }
  }

  getVmCode() {
    return `
      global.Math.fibonacci = (n) => global.__advancedMath('fibonacci', n);
      global.Math.factorial = (n) => global.__advancedMath('factorial', n);
    `
  }

  dispose(): void {
    // Always call super.dispose() when extending VMLibrary
    super.dispose()
    // Add your cleanup logic here
    console.log('CustomMathLibrary disposed')
  }

  private fibonacci(n: number): number {
    if (n <= 1) return n
    return this.fibonacci(n - 1) + this.fibonacci(n - 2)
  }

  private factorial(n: number): number {
    if (n <= 1) return 1
    return n * this.factorial(n - 1)
  }
}

2. Extending VMIpcRxLibrary for Async Operations

This is the recommended pattern for libraries that provide async functions to the VM, as shown in vm.crypto.library.ts:

import { VMIpcRxLibrary } from '@david.uhlir/vm-machine'
import fs from 'fs/promises'
import path from 'path'

class VMFileSystemLibrary extends VMIpcRxLibrary {
  private allowedPaths: string[]

  constructor(allowedPaths: string[] = []) {
    super({
      __readFile: async (filePath: string): Promise<string> => {
        this.validateP