- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination
Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
547 lines
No EOL
14 KiB
TypeScript
547 lines
No EOL
14 KiB
TypeScript
/**
|
|
* HTTP + SSE Transport for Zero-Config Distributed Brainy
|
|
* Simple, reliable, works everywhere - no WebSocket complexity!
|
|
* REAL PRODUCTION CODE - Handles millions of operations
|
|
*/
|
|
|
|
import * as http from 'node:http'
|
|
import * as https from 'node:https'
|
|
import { EventEmitter } from 'node:events'
|
|
import * as net from 'node:net'
|
|
import { URL } from 'node:url'
|
|
|
|
export interface TransportMessage {
|
|
id: string
|
|
method: string
|
|
params: any
|
|
timestamp: number
|
|
from: string
|
|
to?: string
|
|
}
|
|
|
|
export interface TransportResponse {
|
|
id: string
|
|
result?: any
|
|
error?: {
|
|
code: number
|
|
message: string
|
|
data?: any
|
|
}
|
|
timestamp: number
|
|
}
|
|
|
|
export interface SSEClient {
|
|
id: string
|
|
response: http.ServerResponse
|
|
lastPing: number
|
|
}
|
|
|
|
export class HTTPTransport extends EventEmitter {
|
|
private server: http.Server | null = null
|
|
private port: number = 0
|
|
private nodeId: string
|
|
private endpoints: Map<string, string> = new Map() // nodeId -> endpoint
|
|
private pendingRequests: Map<string, {
|
|
resolve: (value: any) => void
|
|
reject: (error: any) => void
|
|
timeout: NodeJS.Timeout
|
|
}> = new Map()
|
|
private sseClients: Map<string, SSEClient> = new Map()
|
|
private messageHandlers: Map<string, (params: any, from: string) => Promise<any>> = new Map()
|
|
private isRunning: boolean = false
|
|
private readonly REQUEST_TIMEOUT = 30000 // 30 seconds
|
|
private readonly SSE_HEARTBEAT_INTERVAL = 15000 // 15 seconds
|
|
private sseHeartbeatTimer: NodeJS.Timeout | null = null
|
|
|
|
constructor(nodeId: string) {
|
|
super()
|
|
this.nodeId = nodeId
|
|
}
|
|
|
|
/**
|
|
* Start HTTP server with automatic port selection
|
|
*/
|
|
async start(): Promise<number> {
|
|
if (this.isRunning) return this.port
|
|
|
|
// Create HTTP server with all handlers
|
|
this.server = http.createServer(async (req, res) => {
|
|
// Enable CORS for browser compatibility
|
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(204)
|
|
res.end()
|
|
return
|
|
}
|
|
|
|
const url = new URL(req.url || '/', `http://${req.headers.host}`)
|
|
|
|
try {
|
|
// Route requests
|
|
if (url.pathname === '/health') {
|
|
await this.handleHealth(req, res)
|
|
} else if (url.pathname === '/rpc') {
|
|
await this.handleRPC(req, res)
|
|
} else if (url.pathname === '/events') {
|
|
await this.handleSSE(req, res)
|
|
} else if (url.pathname.startsWith('/stream/')) {
|
|
await this.handleStream(req, res, url)
|
|
} else {
|
|
res.writeHead(404)
|
|
res.end('Not Found')
|
|
}
|
|
} catch (err) {
|
|
console.error(`[${this.nodeId}] Request error:`, err)
|
|
res.writeHead(500)
|
|
res.end('Internal Server Error')
|
|
}
|
|
})
|
|
|
|
// Find available port
|
|
this.port = await this.findAvailablePort()
|
|
|
|
// Start server
|
|
await new Promise<void>((resolve, reject) => {
|
|
this.server!.listen(this.port, () => {
|
|
console.log(`[${this.nodeId}] HTTP transport listening on port ${this.port}`)
|
|
resolve()
|
|
})
|
|
this.server!.on('error', reject)
|
|
})
|
|
|
|
this.isRunning = true
|
|
|
|
// Start SSE heartbeat
|
|
this.startSSEHeartbeat()
|
|
|
|
this.emit('started', this.port)
|
|
|
|
return this.port
|
|
}
|
|
|
|
/**
|
|
* Stop HTTP server
|
|
*/
|
|
async stop(): Promise<void> {
|
|
if (!this.isRunning) return
|
|
|
|
this.isRunning = false
|
|
|
|
// Stop SSE heartbeat
|
|
if (this.sseHeartbeatTimer) {
|
|
clearInterval(this.sseHeartbeatTimer)
|
|
this.sseHeartbeatTimer = null
|
|
}
|
|
|
|
// Close all SSE connections
|
|
for (const client of this.sseClients.values()) {
|
|
client.response.end()
|
|
}
|
|
this.sseClients.clear()
|
|
|
|
// Cancel pending requests
|
|
for (const pending of this.pendingRequests.values()) {
|
|
clearTimeout(pending.timeout)
|
|
pending.reject(new Error('Transport shutting down'))
|
|
}
|
|
this.pendingRequests.clear()
|
|
|
|
// Close server
|
|
if (this.server) {
|
|
await new Promise<void>(resolve => {
|
|
this.server!.close(() => resolve())
|
|
})
|
|
this.server = null
|
|
}
|
|
|
|
this.emit('stopped')
|
|
}
|
|
|
|
/**
|
|
* Register a node endpoint
|
|
*/
|
|
registerEndpoint(nodeId: string, endpoint: string): void {
|
|
this.endpoints.set(nodeId, endpoint)
|
|
this.emit('endpointRegistered', { nodeId, endpoint })
|
|
}
|
|
|
|
/**
|
|
* Register RPC method handler
|
|
*/
|
|
registerHandler(method: string, handler: (params: any, from: string) => Promise<any>): void {
|
|
this.messageHandlers.set(method, handler)
|
|
}
|
|
|
|
/**
|
|
* Call RPC method on remote node
|
|
*/
|
|
async call(nodeId: string, method: string, params: any): Promise<any> {
|
|
const endpoint = this.endpoints.get(nodeId)
|
|
if (!endpoint) {
|
|
throw new Error(`No endpoint registered for node ${nodeId}`)
|
|
}
|
|
|
|
const message: TransportMessage = {
|
|
id: this.generateId(),
|
|
method,
|
|
params,
|
|
timestamp: Date.now(),
|
|
from: this.nodeId,
|
|
to: nodeId
|
|
}
|
|
|
|
// Send HTTP request
|
|
const response = await this.sendHTTPRequest(endpoint, '/rpc', message)
|
|
|
|
if (response.error) {
|
|
throw new Error(response.error.message)
|
|
}
|
|
|
|
return response.result
|
|
}
|
|
|
|
/**
|
|
* Broadcast to all SSE clients
|
|
*/
|
|
broadcast(event: string, data: any): void {
|
|
const message = JSON.stringify({ event, data, timestamp: Date.now() })
|
|
|
|
for (const [clientId, client] of this.sseClients.entries()) {
|
|
try {
|
|
client.response.write(`data: ${message}\n\n`)
|
|
} catch (err) {
|
|
// Client disconnected
|
|
console.debug(`[${this.nodeId}] SSE client ${clientId} disconnected`)
|
|
this.sseClients.delete(clientId)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle health check
|
|
*/
|
|
private async handleHealth(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({
|
|
status: 'healthy',
|
|
nodeId: this.nodeId,
|
|
uptime: process.uptime(),
|
|
memory: process.memoryUsage(),
|
|
connections: this.sseClients.size
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Handle RPC requests
|
|
*/
|
|
private async handleRPC(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
if (req.method !== 'POST') {
|
|
res.writeHead(405)
|
|
res.end('Method Not Allowed')
|
|
return
|
|
}
|
|
|
|
// Read body
|
|
const body = await this.readBody(req)
|
|
let message: TransportMessage
|
|
|
|
try {
|
|
message = JSON.parse(body)
|
|
} catch (err) {
|
|
res.writeHead(400)
|
|
res.end('Invalid JSON')
|
|
return
|
|
}
|
|
|
|
// Handle the RPC call
|
|
const response: TransportResponse = {
|
|
id: message.id,
|
|
timestamp: Date.now()
|
|
}
|
|
|
|
try {
|
|
const handler = this.messageHandlers.get(message.method)
|
|
if (!handler) {
|
|
throw new Error(`Method ${message.method} not found`)
|
|
}
|
|
|
|
response.result = await handler(message.params, message.from)
|
|
} catch (err: any) {
|
|
response.error = {
|
|
code: -32603,
|
|
message: err.message,
|
|
data: err.stack
|
|
}
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify(response))
|
|
}
|
|
|
|
/**
|
|
* Handle SSE connections for real-time updates
|
|
*/
|
|
private async handleSSE(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
// Setup SSE headers
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'X-Accel-Buffering': 'no' // Disable Nginx buffering
|
|
})
|
|
|
|
// Send initial connection event
|
|
const clientId = this.generateId()
|
|
res.write(`data: ${JSON.stringify({
|
|
event: 'connected',
|
|
clientId,
|
|
nodeId: this.nodeId
|
|
})}\n\n`)
|
|
|
|
// Store client
|
|
this.sseClients.set(clientId, {
|
|
id: clientId,
|
|
response: res,
|
|
lastPing: Date.now()
|
|
})
|
|
|
|
// Handle client disconnect
|
|
req.on('close', () => {
|
|
this.sseClients.delete(clientId)
|
|
this.emit('sseDisconnected', clientId)
|
|
})
|
|
|
|
this.emit('sseConnected', clientId)
|
|
}
|
|
|
|
/**
|
|
* Handle streaming data (for shard migration)
|
|
*/
|
|
private async handleStream(
|
|
req: http.IncomingMessage,
|
|
res: http.ServerResponse,
|
|
url: URL
|
|
): Promise<void> {
|
|
const streamId = url.pathname.split('/').pop()
|
|
|
|
if (req.method === 'POST') {
|
|
// Receiving stream
|
|
await this.handleStreamUpload(req, res, streamId!)
|
|
} else if (req.method === 'GET') {
|
|
// Sending stream
|
|
await this.handleStreamDownload(req, res, streamId!)
|
|
} else {
|
|
res.writeHead(405)
|
|
res.end('Method Not Allowed')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle stream upload (receiving data)
|
|
*/
|
|
private async handleStreamUpload(
|
|
req: http.IncomingMessage,
|
|
res: http.ServerResponse,
|
|
streamId: string
|
|
): Promise<void> {
|
|
const chunks: Buffer[] = []
|
|
let totalSize = 0
|
|
|
|
req.on('data', (chunk: Buffer) => {
|
|
chunks.push(chunk)
|
|
totalSize += chunk.length
|
|
|
|
// Emit progress
|
|
this.emit('streamProgress', {
|
|
streamId,
|
|
type: 'upload',
|
|
bytes: totalSize
|
|
})
|
|
})
|
|
|
|
req.on('end', () => {
|
|
const data = Buffer.concat(chunks)
|
|
|
|
// Process the streamed data
|
|
this.emit('streamComplete', {
|
|
streamId,
|
|
type: 'upload',
|
|
data,
|
|
size: totalSize
|
|
})
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
streamId,
|
|
size: totalSize
|
|
}))
|
|
})
|
|
|
|
req.on('error', (err) => {
|
|
console.error(`[${this.nodeId}] Stream upload error:`, err)
|
|
res.writeHead(500)
|
|
res.end('Stream Error')
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Handle stream download (sending data)
|
|
*/
|
|
private async handleStreamDownload(
|
|
req: http.IncomingMessage,
|
|
res: http.ServerResponse,
|
|
streamId: string
|
|
): Promise<void> {
|
|
// Stream download not yet implemented
|
|
// Return error response instead of fake data
|
|
res.writeHead(501, {
|
|
'Content-Type': 'application/json'
|
|
})
|
|
|
|
res.end(JSON.stringify({
|
|
error: 'Stream download not implemented',
|
|
message: 'This feature is not yet available in the current version',
|
|
streamId
|
|
}))
|
|
|
|
this.emit('streamError', {
|
|
streamId,
|
|
type: 'download',
|
|
error: 'Not implemented'
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Send HTTP request to another node
|
|
*/
|
|
private async sendHTTPRequest(
|
|
endpoint: string,
|
|
path: string,
|
|
data: any
|
|
): Promise<TransportResponse> {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(path, endpoint)
|
|
const options = {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}
|
|
|
|
const proto = url.protocol === 'https:' ? https : http
|
|
const req = proto.request(url, options, (res) => {
|
|
let body = ''
|
|
|
|
res.on('data', chunk => body += chunk)
|
|
res.on('end', () => {
|
|
try {
|
|
const response = JSON.parse(body)
|
|
resolve(response)
|
|
} catch (err) {
|
|
reject(new Error(`Invalid response: ${body}`))
|
|
}
|
|
})
|
|
})
|
|
|
|
req.on('error', reject)
|
|
req.on('timeout', () => {
|
|
req.destroy()
|
|
reject(new Error('Request timeout'))
|
|
})
|
|
|
|
req.setTimeout(this.REQUEST_TIMEOUT)
|
|
req.write(JSON.stringify(data))
|
|
req.end()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Read request body
|
|
*/
|
|
private readBody(req: http.IncomingMessage): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
let body = ''
|
|
req.on('data', chunk => body += chunk)
|
|
req.on('end', () => resolve(body))
|
|
req.on('error', reject)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Find an available port
|
|
*/
|
|
private async findAvailablePort(startPort: number = 7947): Promise<number> {
|
|
const checkPort = (port: number): Promise<boolean> => {
|
|
return new Promise(resolve => {
|
|
const server = net.createServer()
|
|
server.once('error', () => resolve(false))
|
|
server.once('listening', () => {
|
|
server.close()
|
|
resolve(true)
|
|
})
|
|
server.listen(port)
|
|
})
|
|
}
|
|
|
|
// Try preferred port first
|
|
if (await checkPort(startPort)) {
|
|
return startPort
|
|
}
|
|
|
|
// Find random available port
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer()
|
|
server.once('error', reject)
|
|
server.once('listening', () => {
|
|
const port = (server.address() as net.AddressInfo).port
|
|
server.close(() => resolve(port))
|
|
})
|
|
server.listen(0) // 0 = random available port
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Start SSE heartbeat to keep connections alive
|
|
*/
|
|
private startSSEHeartbeat(): void {
|
|
this.sseHeartbeatTimer = setInterval(() => {
|
|
const now = Date.now()
|
|
const ping = JSON.stringify({ event: 'ping', timestamp: now })
|
|
|
|
for (const [clientId, client] of this.sseClients.entries()) {
|
|
try {
|
|
client.response.write(`: ping\n\n`) // SSE comment for keep-alive
|
|
client.lastPing = now
|
|
} catch (err) {
|
|
// Client disconnected
|
|
this.sseClients.delete(clientId)
|
|
}
|
|
}
|
|
}, this.SSE_HEARTBEAT_INTERVAL)
|
|
}
|
|
|
|
/**
|
|
* Generate unique ID
|
|
*/
|
|
private generateId(): string {
|
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
|
|
}
|
|
|
|
/**
|
|
* Get connected nodes count
|
|
*/
|
|
getConnectionCount(): number {
|
|
return this.endpoints.size
|
|
}
|
|
|
|
/**
|
|
* Get SSE client count
|
|
*/
|
|
getSSEClientCount(): number {
|
|
return this.sseClients.size
|
|
}
|
|
} |