Nasal-Interpreter/nasal-web-app/server_repl.js

188 lines
5.6 KiB
JavaScript

const express = require('express');
const path = require('path');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const koffi = require('koffi');
// Parse command line arguments
const argv = yargs(hideBin(process.argv))
.usage('Usage: $0 [options]')
.option('verbose', {
alias: 'v',
type: 'boolean',
description: 'Run with verbose logging'
})
.option('port', {
alias: 'p',
type: 'number',
description: 'Port to run the server on',
default: 3001
})
.option('host', {
type: 'string',
description: 'Host to run the server on',
default: 'localhost'
})
.help()
.alias('help', 'h')
.version()
.argv;
const app = express();
app.use(express.json());
app.use(express.static('public'));
// Load Nasal REPL library functions
let nasalLib;
try {
const lib = koffi.load(path.join(__dirname, '../module/libnasal-web.dylib'));
nasalLib = {
nasal_repl_init: lib.func('nasal_repl_init', 'void*', []),
nasal_repl_cleanup: lib.func('nasal_repl_cleanup', 'void', ['void*']),
nasal_repl_eval: lib.func('nasal_repl_eval', 'const char*', ['void*', 'const char*']),
nasal_repl_is_complete: lib.func('nasal_repl_is_complete', 'int', ['void*', 'const char*']),
nasal_repl_get_version: lib.func('nasal_repl_get_version', 'const char*', [])
};
if (argv.verbose) {
console.log('REPL Library loaded successfully');
}
} catch (err) {
console.error('Failed to load REPL library:', err);
process.exit(1);
}
// Store active REPL sessions
const replSessions = new Map();
// Clean up inactive sessions periodically (30 minutes timeout)
const SESSION_TIMEOUT = 30 * 60 * 1000;
setInterval(() => {
const now = Date.now();
for (const [sessionId, session] of replSessions.entries()) {
if (now - session.lastAccess > SESSION_TIMEOUT) {
if (argv.verbose) {
console.log(`Cleaning up inactive session: ${sessionId}`);
}
nasalLib.nasal_repl_cleanup(session.context);
replSessions.delete(sessionId);
}
}
}, 60000); // Check every minute
app.post('/repl/init', (req, res) => {
try {
const ctx = nasalLib.nasal_repl_init();
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const version = nasalLib.nasal_repl_get_version();
replSessions.set(sessionId, {
context: ctx,
lastAccess: Date.now()
});
if (argv.verbose) {
console.log(`New REPL session initialized: ${sessionId}`);
}
res.json({
sessionId,
version
});
} catch (err) {
if (argv.verbose) {
console.error('Failed to initialize REPL session:', err);
}
res.status(500).json({ error: 'Failed to initialize REPL session' });
}
});
app.post('/repl/eval', (req, res) => {
const { sessionId, line, check, buffer } = req.body;
if (!sessionId || !replSessions.has(sessionId)) {
return res.status(400).json({ error: 'Invalid or expired session' });
}
if (!line) {
return res.status(400).json({ error: 'No code provided' });
}
try {
const session = replSessions.get(sessionId);
session.lastAccess = Date.now();
if (check) {
const codeToCheck = buffer ? [...buffer, line].join('\n') : line;
const isComplete = nasalLib.nasal_repl_is_complete(session.context, codeToCheck);
if (isComplete === 1) {
return res.json({ needsMore: true });
} else if (isComplete === -1) {
return res.json({ error: 'Invalid input' });
}
}
const result = nasalLib.nasal_repl_eval(session.context, line);
if (argv.verbose) {
console.log(`REPL evaluation for session ${sessionId}:`, { line, result });
}
res.json({ result });
} catch (err) {
if (argv.verbose) {
console.error(`REPL evaluation error for session ${sessionId}:`, err);
}
res.status(500).json({ error: err.message });
}
});
app.post('/repl/cleanup', (req, res) => {
const { sessionId } = req.body;
if (!sessionId || !replSessions.has(sessionId)) {
return res.status(400).json({ error: 'Invalid session' });
}
try {
const session = replSessions.get(sessionId);
nasalLib.nasal_repl_cleanup(session.context);
replSessions.delete(sessionId);
if (argv.verbose) {
console.log(`REPL session cleaned up: ${sessionId}`);
}
res.json({ message: 'Session cleaned up successfully' });
} catch (err) {
if (argv.verbose) {
console.error(`Failed to cleanup session ${sessionId}:`, err);
}
res.status(500).json({ error: err.message });
}
});
// Handle cleanup on server shutdown
process.on('SIGINT', () => {
console.log('\nCleaning up REPL sessions before exit...');
for (const [sessionId, session] of replSessions.entries()) {
try {
nasalLib.nasal_repl_cleanup(session.context);
if (argv.verbose) {
console.log(`Cleaned up session: ${sessionId}`);
}
} catch (err) {
console.error(`Error cleaning up session ${sessionId}:`, err);
}
}
process.exit(0);
});
const PORT = argv.port || 3001;
app.listen(PORT, () => {
console.log(`REPL Server running on http://localhost:${PORT}`);
if (argv.verbose) console.log('Verbose logging enabled');
});