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
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" );
Optional configuration onWarning
(message: string) => void
Callback invoked when warnings occur (corrupted files, stale locks, etc.)
Properties
The state directory path (read-only)
Path to the proxy PID file (read-only)
Path to the proxy port file (read-only)
Methods
ensureDir()
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.
The hostname to register (e.g., “api.localhost”)
The local port the application is listening on
Process ID of the owning process. Use process.pid for the current process, or 0 for system-managed routes.
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.
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.
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()
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:
The owning process terminates (detected via PID check)
Any method loads the routes from disk
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