629 lines
16 KiB
JavaScript
629 lines
16 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
const compression = require('compression');
|
|
const helmet = require('helmet');
|
|
const rateLimit = require('express-rate-limit');
|
|
const { WebSocketServer } = require('ws');
|
|
const { EventEmitter } = require('events');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const { StreamingManager } = require('./streaming');
|
|
const { SessionManager } = require('./session-manager');
|
|
const { FlowNexusIntegration } = require('../integrations/flow-nexus');
|
|
|
|
class SolverServer extends EventEmitter {
|
|
constructor(options = {}) {
|
|
super();
|
|
|
|
this.config = {
|
|
port: options.port || 3000,
|
|
cors: options.cors || false,
|
|
workers: options.workers || 1,
|
|
maxSessions: options.maxSessions || 100,
|
|
authToken: options.authToken,
|
|
flowNexusEnabled: options.flowNexusEnabled || false,
|
|
...options
|
|
};
|
|
|
|
this.app = express();
|
|
this.server = null;
|
|
this.wss = null;
|
|
|
|
this.sessions = new SessionManager(this.config);
|
|
this.streaming = new StreamingManager(this.config);
|
|
this.flowNexus = this.config.flowNexusEnabled ? new FlowNexusIntegration() : null;
|
|
|
|
this.setupMiddleware();
|
|
this.setupRoutes();
|
|
this.setupWebSocket();
|
|
}
|
|
|
|
setupMiddleware() {
|
|
// Security middleware
|
|
this.app.use(helmet({
|
|
contentSecurityPolicy: false, // Allow WebSocket connections
|
|
crossOriginEmbedderPolicy: false
|
|
}));
|
|
|
|
// CORS
|
|
if (this.config.cors) {
|
|
this.app.use(cors({
|
|
origin: true,
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Session-ID']
|
|
}));
|
|
}
|
|
|
|
// Compression
|
|
this.app.use(compression());
|
|
|
|
// Rate limiting
|
|
const limiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 1000, // Limit each IP to 1000 requests per windowMs
|
|
message: {
|
|
error: 'Too many requests',
|
|
retryAfter: '15 minutes'
|
|
}
|
|
});
|
|
this.app.use('/api', limiter);
|
|
|
|
// Body parsing
|
|
this.app.use(express.json({ limit: '50mb' }));
|
|
this.app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
|
|
|
// Request logging
|
|
this.app.use((req, res, next) => {
|
|
const timestamp = new Date().toISOString();
|
|
console.log(`[${timestamp}] ${req.method} ${req.path}`);
|
|
next();
|
|
});
|
|
|
|
// Authentication middleware
|
|
this.app.use('/api/protected', this.authenticateToken.bind(this));
|
|
}
|
|
|
|
setupRoutes() {
|
|
// Health check
|
|
this.app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
memory: process.memoryUsage(),
|
|
sessions: this.sessions.getStats()
|
|
});
|
|
});
|
|
|
|
// API routes
|
|
this.app.use('/api/v1', this.createAPIRoutes());
|
|
|
|
// Static files for documentation
|
|
this.app.use('/docs', express.static('docs'));
|
|
|
|
// Root endpoint
|
|
this.app.get('/', (req, res) => {
|
|
res.json({
|
|
name: 'Sublinear Time Solver API',
|
|
version: '1.0.0',
|
|
endpoints: {
|
|
health: '/health',
|
|
api: '/api/v1',
|
|
docs: '/docs',
|
|
websocket: '/ws'
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
createAPIRoutes() {
|
|
const router = express.Router();
|
|
|
|
// Start streaming solve session
|
|
router.post('/solve-stream', async (req, res) => {
|
|
try {
|
|
const {
|
|
matrix,
|
|
vector,
|
|
method = 'adaptive',
|
|
options = {},
|
|
flow_nexus = {}
|
|
} = req.body;
|
|
|
|
if (!matrix || !vector) {
|
|
return res.status(400).json({
|
|
error: 'Matrix and vector are required',
|
|
code: 'MISSING_INPUT'
|
|
});
|
|
}
|
|
|
|
const sessionId = uuidv4();
|
|
const session = await this.sessions.createSession(sessionId, {
|
|
matrix,
|
|
vector,
|
|
method,
|
|
options,
|
|
flowNexus: flow_nexus
|
|
});
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'application/x-ndjson',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'X-Session-ID': sessionId
|
|
});
|
|
|
|
// Start streaming solve
|
|
const stream = await this.streaming.startSolve(session);
|
|
|
|
for await (const update of stream) {
|
|
const data = JSON.stringify({
|
|
type: 'iteration_update',
|
|
session_id: sessionId,
|
|
timestamp: new Date().toISOString(),
|
|
...update
|
|
}) + '\n';
|
|
|
|
if (!res.write(data)) {
|
|
// Backpressure handling
|
|
await new Promise(resolve => res.once('drain', resolve));
|
|
}
|
|
|
|
if (update.converged || update.error) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
res.end();
|
|
|
|
} catch (error) {
|
|
console.error('Solve stream error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'SOLVE_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Submit solve job
|
|
router.post('/solve', async (req, res) => {
|
|
try {
|
|
const { matrix, vector, method, options } = req.body;
|
|
|
|
if (!matrix || !vector) {
|
|
return res.status(400).json({
|
|
error: 'Matrix and vector are required'
|
|
});
|
|
}
|
|
|
|
const jobId = await this.sessions.submitJob({
|
|
matrix,
|
|
vector,
|
|
method,
|
|
options
|
|
});
|
|
|
|
res.json({
|
|
job_id: jobId,
|
|
status: 'submitted',
|
|
endpoints: {
|
|
status: `/api/v1/jobs/${jobId}`,
|
|
stream: `/api/v1/jobs/${jobId}/stream`
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Submit job error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'SUBMIT_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get job status
|
|
router.get('/jobs/:jobId', async (req, res) => {
|
|
try {
|
|
const status = await this.sessions.getJobStatus(req.params.jobId);
|
|
if (!status) {
|
|
return res.status(404).json({
|
|
error: 'Job not found',
|
|
code: 'JOB_NOT_FOUND'
|
|
});
|
|
}
|
|
|
|
res.json(status);
|
|
|
|
} catch (error) {
|
|
console.error('Get job status error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'STATUS_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Stream job updates
|
|
router.get('/jobs/:jobId/stream', async (req, res) => {
|
|
try {
|
|
const stream = await this.sessions.getJobStream(req.params.jobId);
|
|
if (!stream) {
|
|
return res.status(404).json({
|
|
error: 'Job not found or not streaming'
|
|
});
|
|
}
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive'
|
|
});
|
|
|
|
for await (const update of stream) {
|
|
res.write(`data: ${JSON.stringify(update)}\n\n`);
|
|
}
|
|
|
|
res.end();
|
|
|
|
} catch (error) {
|
|
console.error('Job stream error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'STREAM_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Verification endpoint
|
|
router.post('/verify', async (req, res) => {
|
|
try {
|
|
const {
|
|
session_id,
|
|
probe_count = 10,
|
|
tolerance = 1e-8
|
|
} = req.body;
|
|
|
|
if (!session_id) {
|
|
return res.status(400).json({
|
|
error: 'Session ID is required'
|
|
});
|
|
}
|
|
|
|
const result = await this.sessions.verifySession(session_id, {
|
|
probeCount: probe_count,
|
|
tolerance
|
|
});
|
|
|
|
res.json({
|
|
type: 'verification_result',
|
|
session_id,
|
|
timestamp: new Date().toISOString(),
|
|
...result
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Verification error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'VERIFICATION_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Session status
|
|
router.get('/sessions/:sessionId', async (req, res) => {
|
|
try {
|
|
const session = await this.sessions.getSession(req.params.sessionId);
|
|
if (!session) {
|
|
return res.status(404).json({
|
|
error: 'Session not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
session_id: req.params.sessionId,
|
|
status: session.status,
|
|
created_at: session.createdAt,
|
|
last_activity: session.lastActivity,
|
|
metrics: session.metrics
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Session status error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'SESSION_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Cost update endpoint for swarm coordination
|
|
router.post('/swarm/costs', async (req, res) => {
|
|
try {
|
|
const {
|
|
session_id,
|
|
delta_costs,
|
|
matrix_updates,
|
|
source_node
|
|
} = req.body;
|
|
|
|
if (!session_id || !delta_costs) {
|
|
return res.status(400).json({
|
|
error: 'Session ID and delta costs are required'
|
|
});
|
|
}
|
|
|
|
await this.sessions.updateCosts(session_id, {
|
|
deltaCosts: delta_costs,
|
|
matrixUpdates: matrix_updates,
|
|
sourceNode: source_node
|
|
});
|
|
|
|
res.json({
|
|
status: 'updated',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Cost update error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'COST_UPDATE_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Swarm join endpoint
|
|
router.post('/swarm/join', async (req, res) => {
|
|
try {
|
|
const {
|
|
session_id,
|
|
node_id,
|
|
capabilities = []
|
|
} = req.body;
|
|
|
|
if (!session_id || !node_id) {
|
|
return res.status(400).json({
|
|
error: 'Session ID and node ID are required'
|
|
});
|
|
}
|
|
|
|
await this.sessions.joinSwarm(session_id, {
|
|
nodeId: node_id,
|
|
capabilities
|
|
});
|
|
|
|
res.json({
|
|
status: 'joined',
|
|
node_id,
|
|
session_id
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Swarm join error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'SWARM_JOIN_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Flow-Nexus integration endpoints
|
|
if (this.flowNexus) {
|
|
router.post('/flow-nexus/register', async (req, res) => {
|
|
try {
|
|
const result = await this.flowNexus.registerSolver({
|
|
endpoint: `http://localhost:${this.config.port}`,
|
|
capabilities: ['streaming', 'verification', 'swarm']
|
|
});
|
|
|
|
res.json(result);
|
|
|
|
} catch (error) {
|
|
console.error('Flow-Nexus registration error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'FLOW_NEXUS_ERROR'
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/flow-nexus/status', async (req, res) => {
|
|
try {
|
|
const status = await this.flowNexus.getStatus();
|
|
res.json(status);
|
|
|
|
} catch (error) {
|
|
console.error('Flow-Nexus status error:', error);
|
|
res.status(500).json({
|
|
error: error.message,
|
|
code: 'FLOW_NEXUS_ERROR'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return router;
|
|
}
|
|
|
|
setupWebSocket() {
|
|
// WebSocket setup will be done when server starts
|
|
}
|
|
|
|
authenticateToken(req, res, next) {
|
|
const authHeader = req.headers['authorization'];
|
|
const token = authHeader && authHeader.split(' ')[1];
|
|
|
|
if (!token && this.config.authToken) {
|
|
return res.status(401).json({
|
|
error: 'Authentication token required',
|
|
code: 'AUTH_REQUIRED'
|
|
});
|
|
}
|
|
|
|
if (this.config.authToken && token !== this.config.authToken) {
|
|
return res.status(403).json({
|
|
error: 'Invalid authentication token',
|
|
code: 'AUTH_INVALID'
|
|
});
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
async start() {
|
|
return new Promise((resolve, reject) => {
|
|
this.server = this.app.listen(this.config.port, (error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
// Setup WebSocket server
|
|
this.wss = new WebSocketServer({
|
|
server: this.server,
|
|
path: '/ws'
|
|
});
|
|
|
|
this.setupWebSocketHandlers();
|
|
|
|
console.log(`🚀 Solver server started on port ${this.config.port}`);
|
|
this.emit('started');
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
setupWebSocketHandlers() {
|
|
this.wss.on('connection', (ws, req) => {
|
|
console.log('WebSocket connection established');
|
|
|
|
ws.on('message', async (data) => {
|
|
try {
|
|
const message = JSON.parse(data.toString());
|
|
await this.handleWebSocketMessage(ws, message);
|
|
} catch (error) {
|
|
console.error('WebSocket message error:', error);
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
error: error.message
|
|
}));
|
|
}
|
|
});
|
|
|
|
ws.on('close', () => {
|
|
console.log('WebSocket connection closed');
|
|
});
|
|
|
|
ws.on('error', (error) => {
|
|
console.error('WebSocket error:', error);
|
|
});
|
|
|
|
// Send welcome message
|
|
ws.send(JSON.stringify({
|
|
type: 'welcome',
|
|
timestamp: new Date().toISOString()
|
|
}));
|
|
});
|
|
}
|
|
|
|
async handleWebSocketMessage(ws, message) {
|
|
switch (message.type) {
|
|
case 'subscribe':
|
|
if (message.session_id) {
|
|
await this.subscribeToSession(ws, message.session_id);
|
|
}
|
|
break;
|
|
|
|
case 'solve':
|
|
const sessionId = await this.startWebSocketSolve(ws, message);
|
|
ws.send(JSON.stringify({
|
|
type: 'solve_started',
|
|
session_id: sessionId
|
|
}));
|
|
break;
|
|
|
|
case 'ping':
|
|
ws.send(JSON.stringify({
|
|
type: 'pong',
|
|
timestamp: new Date().toISOString()
|
|
}));
|
|
break;
|
|
|
|
default:
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
error: `Unknown message type: ${message.type}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async subscribeToSession(ws, sessionId) {
|
|
const stream = await this.sessions.getJobStream(sessionId);
|
|
if (!stream) {
|
|
ws.send(JSON.stringify({
|
|
type: 'error',
|
|
error: 'Session not found or not streaming'
|
|
}));
|
|
return;
|
|
}
|
|
|
|
for await (const update of stream) {
|
|
if (ws.readyState === ws.OPEN) {
|
|
ws.send(JSON.stringify({
|
|
type: 'session_update',
|
|
session_id: sessionId,
|
|
...update
|
|
}));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
async startWebSocketSolve(ws, message) {
|
|
const sessionId = uuidv4();
|
|
const session = await this.sessions.createSession(sessionId, {
|
|
matrix: message.matrix,
|
|
vector: message.vector,
|
|
method: message.method || 'adaptive',
|
|
options: message.options || {}
|
|
});
|
|
|
|
// Start streaming to WebSocket
|
|
this.subscribeToSession(ws, sessionId);
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
async stop() {
|
|
return new Promise((resolve) => {
|
|
if (this.wss) {
|
|
this.wss.close();
|
|
}
|
|
|
|
if (this.server) {
|
|
this.server.close(() => {
|
|
console.log('Server stopped');
|
|
this.emit('stopped');
|
|
resolve();
|
|
});
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
getStats() {
|
|
return {
|
|
sessions: this.sessions.getStats(),
|
|
server: {
|
|
uptime: process.uptime(),
|
|
memory: process.memoryUsage(),
|
|
connections: this.wss ? this.wss.clients.size : 0
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = { SolverServer }; |