Skip to main content

Class Overview

class RouteStore {
  constructor(dir: string, options?: { onWarning?: (message: string) => void })
}
Manages route mappings stored as a JSON file on disk. Provides file locking for concurrent access, automatic cleanup of stale routes, and process lifecycle tracking.

Constructor

dir
string
required
Path to the state directory where route data will be stored. The directory will be created if it doesn’t exist.
const store = new RouteStore("/tmp/portless-state");
options
object
Optional configuration

Properties

dir
string
The state directory path (read-only)
pidPath
string
Path to the proxy PID file (read-only)
portFilePath
string
Path to the proxy port file (read-only)

Methods

ensureDir()

ensureDir(): void
Creates the state directory if it doesn’t exist and sets appropriate permissions. Automatically called by addRoute() and removeRoute(). Example:
const store = new RouteStore("/tmp/portless");
store.ensureDir();

addRoute()

addRoute(hostname: string, port: number, pid: number, force?: boolean): void
Registers a route mapping from a hostname to a local port. Acquires a file lock, checks for conflicts, and persists the route to disk.
hostname
string
required
The hostname to register (e.g., “api.localhost”)
port
number
required
The local port the application is listening on
pid
number
required
Process ID of the owning process. Use process.pid for the current process, or 0 for system-managed routes.
force
boolean
default:"false"
Override an existing route even if owned by a live process
Throws:
  • RouteConflictError - When hostname is already registered by a live process and force is false
  • Error - When the file lock cannot be acquired
Example:
store.addRoute("api.localhost", 3000, process.pid);

// Override existing route
store.addRoute("api.localhost", 3001, process.pid, true);

removeRoute()

removeRoute(hostname: string): void
Unregisters a route mapping. Acquires a file lock and removes the route from persistent storage.
hostname
string
required
The hostname to unregister
Throws:
  • Error - When the file lock cannot be acquired
Example:
store.removeRoute("api.localhost");

// Cleanup on process exit
process.on("SIGINT", () => {
  store.removeRoute("api.localhost");
  process.exit(0);
});

loadRoutes()

loadRoutes(persistCleanup?: boolean): RouteMapping[]
Loads all routes from disk, filtering out stale entries whose owning process is no longer alive.
persistCleanup
boolean
default:"false"
When true, writes the cleaned-up route list back to disk. Only safe when the caller already holds the lock (used internally by addRoute and removeRoute).
Returns:
  • RouteMapping[] - Array of live route mappings
Example:
const routes = store.loadRoutes();
routes.forEach(route => {
  console.log(`${route.hostname} -> localhost:${route.port} (PID ${route.pid})`);
});

getRoutesPath()

getRoutesPath(): string
Returns the absolute path to the routes JSON file. Example:
const path = store.getRoutesPath();
console.log(`Routes stored at: ${path}`);

File Locking

The RouteStore uses directory-based file locking to coordinate concurrent access:
  • Lock acquisition retries up to 20 times with 50ms delays
  • Stale locks (older than 10 seconds) are automatically removed
  • All write operations (addRoute, removeRoute) acquire the lock automatically
Do not manually modify the routes file while applications are running. Use the RouteStore API to ensure proper locking.

Stale Route Cleanup

Routes are automatically cleaned up when:
  1. The owning process terminates (detected via PID check)
  2. Any method loads the routes from disk
  3. Routes with pid: 0 are never removed (system-managed)

Error Handling

RouteConflictError

class RouteConflictError extends Error {
  readonly hostname: string;
  readonly existingPid: number;
}
Thrown when attempting to register a hostname that’s already in use:
try {
  store.addRoute("api.localhost", 3000, process.pid);
} catch (err) {
  if (err instanceof RouteConflictError) {
    console.error(`${err.hostname} is already registered by PID ${err.existingPid}`);
    // Use force flag to override
    store.addRoute(err.hostname, 3000, process.pid, true);
  }
}

Complete Example

import { RouteStore, createProxyServer } from "portless";
import { spawn } from "node:child_process";

// Initialize store
const store = new RouteStore("/tmp/portless", {
  onWarning: (msg) => console.warn(`[WARNING] ${msg}`),
});

store.ensureDir();

// Start an application
const app = spawn("node", ["server.js"], {
  env: { PORT: "3000" },
});

// Register the route
try {
  store.addRoute("myapp.localhost", 3000, app.pid);
  console.log("Route registered: http://myapp.localhost:8080");
} catch (err) {
  console.error("Failed to register route:", err);
  app.kill();
  process.exit(1);
}

// Create proxy server
const proxy = createProxyServer({
  getRoutes: () => store.loadRoutes(),
  proxyPort: 8080,
});

proxy.listen(8080);

// Cleanup on exit
process.on("SIGINT", () => {
  store.removeRoute("myapp.localhost");
  proxy.close();
  app.kill();
  process.exit(0);
});

// Cleanup if child process exits
app.on("exit", () => {
  store.removeRoute("myapp.localhost");
});

Type Definitions

interface RouteMapping extends RouteInfo {
  pid: number;
}

interface RouteInfo {
  hostname: string;
  port: number;
}

Constants

// File permissions
export const FILE_MODE = 0o644;
export const DIR_MODE = 0o755;
export const SYSTEM_DIR_MODE = 0o1777;
export const SYSTEM_FILE_MODE = 0o666;

createProxyServer

Create a proxy server that uses routes

Type Definitions

Complete type reference

Build docs developers (and LLMs) love