Skip to content

Commit

Permalink
fix server
Browse files Browse the repository at this point in the history
  • Loading branch information
bunsenstraat committed Jan 24, 2025
1 parent 0563f4a commit 47d6a8e
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 180 deletions.
253 changes: 141 additions & 112 deletions apps/remixdesktop/src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,171 +9,198 @@ import cbor from 'cbor'
import { findAvailablePort } from '../utils/portFinder'
import { isPackaged } from '../main'
import { isE2ELocal } from '../main'
import JSONbig from 'json-bigint'

// Forwarding WebSocket client
let connectedWebSocket: WebSocket | null = null

// Handle incoming JSON-RPC requests and forward to WebSocket client
export const handleRequest = async (jsonRpcPayload: RequestArguments, eventEmitter: EventEmitter): Promise<any> => {
if (!connectedWebSocket || connectedWebSocket.readyState !== WebSocket.OPEN) {
throw new Error('No active WebSocket connection to forward request')
// We will hold onto the pending requests here.
// Key: The request ID; Value: an object containing { resolve, reject, method }
const pendingRequests: Record<
number | string,
{
resolve: (value: any) => void
reject: (reason: any) => void
method?: string
}
> = {}

// Send the payload to the WebSocket client and wait for response
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout waiting for WebSocket response')), 240000) // 10 seconds timeout
let connectedWebSocket: WebSocket | null = null

connectedWebSocket &&
connectedWebSocket.once('message', async (data: any) => {
if (Buffer.isBuffer(data)) {
data = data.toString('utf-8');
}
// -------------------------
// Single top-level message handler
// -------------------------
function setupMessageHandler(ws: WebSocket, eventEmitter: EventEmitter) {
ws.on('message', (data: Buffer | string) => {
if (Buffer.isBuffer(data)) {
data = data.toString('utf8')
}

//console.log('received message from WebSocket ONCE client:', data);
let parsed: { id: any; error: any; result: any; type: string | symbol; payload: any }
try {
parsed = parseWithBigInt(data)
} catch (err) {
console.error('Could not parse incoming WebSocket message:', err)
return
}

clearTimeout(timeout)
try {
//console.log('received message from WebSocket ONCE client:', data)
const response = parseWithBigInt(data)
//console.log('received message from WebSocket ONCE client:', response)
if (response.id === jsonRpcPayload.id) {
if (jsonRpcPayload.method === 'eth_sendTransaction' || jsonRpcPayload.method === 'eth_getTransactionReceipt') {
console.log('response from WebSocket client:', data, response)
eventEmitter.emit('focus')
}
if (response.error) {
const error = { data: response.error }
if (error.data && error.data.originalError && error.data.originalError.data) {
resolve({
jsonrpc: '2.0',
error: error.data.originalError,
id: response.id,
})
} else if (error.data && error.data.message) {
resolve({
jsonrpc: '2.0',
error: error.data && error.data,
id: response.id,
})
} else {
resolve({
jsonrpc: '2.0',
error,
id: response.id,
})
}
} else {
if (jsonRpcPayload.method === 'eth_sendTransaction' || jsonRpcPayload.method === 'eth_getTransactionReceipt') {
//console.log('resolve response from WebSocket client:', jsonRpcPayload.method, response)
// if(jsonRpcPayload.method === 'eth_getTransactionReceipt'){
// response.result = JSON.parse(response.result)
// }
}
resolve(response.result)
}
// If the message has an 'id', try to find a pending request
if (typeof parsed?.id !== 'undefined') {
const requestId = parsed.id
const pendingReq = pendingRequests[requestId]
if (pendingReq) {
// Found a matching pending request.
// Clear it from the queue to avoid memory leak
delete pendingRequests[requestId]

// If there's an error in the response
if (parsed.error) {
const errorObj = { data: parsed.error }
// Your same logic as before
if (errorObj.data && errorObj.data.originalError) {
pendingReq.resolve({
jsonrpc: '2.0',
error: errorObj.data.originalError,
id: parsed.id,
})
} else if (errorObj.data && errorObj.data.message) {
pendingReq.resolve({
jsonrpc: '2.0',
error: errorObj.data,
id: parsed.id,
})
} else {
//console.log('ignore response from WebSocket client:', data)
//reject(new Error('Invalid response ID'));
pendingReq.resolve({
jsonrpc: '2.0',
error: errorObj,
id: parsed.id,
})
}
} catch (error) {
//console.log('REJECT error response from WebSocket client:', error)
reject(error)
} else {
// No error; resolve with result
pendingReq.resolve(parsed.result)
}
})
} else {
// If there's no matching pending request, you can decide to ignore or handle differently
// console.log('No pending request matches id', requestId, parsed)
}
} else if (parsed?.type) {
// Possibly a "notification" or event-based message
// that doesn't match a pending JSON-RPC request
eventEmitter.emit(parsed.type, parsed.payload)
}
})
}

connectedWebSocket &&
connectedWebSocket.send(JSON.stringify(jsonRpcPayload), (err) => {
// -------------------------
// The request forwarder
// -------------------------
export const handleRequest = async (
jsonRpcPayload: RequestArguments,
eventEmitter: EventEmitter
): Promise<any> => {
if (!connectedWebSocket || connectedWebSocket.readyState !== WebSocket.OPEN) {
throw new Error('No active WebSocket connection to forward request')
}

const requestId = jsonRpcPayload.id

return new Promise((resolve, reject) => {
// Store references in our pendingRequests map
pendingRequests[requestId] = { resolve, reject, method: jsonRpcPayload.method }

// Optional: You can start a request-specific timeout here
// to reject the request if it doesn't resolve in time.
const timeout = setTimeout(() => {
// If it times out, remove from pendingRequests.
delete pendingRequests[requestId]
reject(new Error('Timeout waiting for WebSocket response'))
}, 240000) // 4-min timeout or whatever you prefer

connectedWebSocket.send(JSON.stringify(jsonRpcPayload), (err) => {
if (err) {
delete pendingRequests[requestId]
clearTimeout(timeout)
reject(err)
} else {
// If you want to log for specific methods:
if (jsonRpcPayload.method === 'eth_sendTransaction' || jsonRpcPayload.method === 'eth_getTransactionReceipt') {
console.log('sent message to WebSocket client:', jsonRpcPayload)
}
if (err) {
clearTimeout(timeout)
reject(err)
console.log('Sent message to WebSocket client:', jsonRpcPayload)
}
})
}
})
})
}

export const startHostServer = async (eventEmitter: EventEmitter) => {
let http_port = await findAvailablePort([49589])
const websocket_port = await findAvailablePort([49588])

// Create an Express server
const startServer = () => {
const server = express()

// Serve static files from the 'remix-ide' directory
const remixPath = path.join(__dirname, 'remix-ide')
server.use(express.static(remixPath))

console.log('remixPath', remixPath)

// Serve 'index.html' at the root route
server.get('/', (req, res) => {
res.sendFile(path.join(remixPath, 'index.html'))
})

// Start the server
const httpServer = http.createServer(server)
httpServer.listen(http_port, () => {
const address = httpServer.address()
if (typeof address === 'string') {
console.log(`Server started at ${address}`)
} else if (address && address.port) {
console.log(`Server started at http://localhost:${address.port}`)
} else {
}
})

return httpServer
}

// Create the WebSocket server
const wsServer = new WebSocketServer({ port: websocket_port })

wsServer.on('connection', (ws) => {
console.log('WebSocket client connected')
if (connectedWebSocket?.OPEN) {

// If we already have a connected client, close the new one
if (connectedWebSocket && connectedWebSocket.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'error', payload: 'ALREADY_CONNECTED' }))
ws.close(1000, 'Another client connected')
return
//console.log(connectedWebSocket.url)
} else {
} else if (connectedWebSocket) {
try {
// Clean up any leftover listeners
connectedWebSocket.removeAllListeners()
} catch (e) {}
} catch (_) {}
}

connectedWebSocket = ws
eventEmitter.emit('connected', true)

connectedWebSocket.on('message', (data: any) => {
if (Buffer.isBuffer(data)) {
data = data.toString('utf-8');
}
const response = parseWithBigInt(data)
if (response && response.type) {
//console.log('received message from WebSocket client:', response)
eventEmitter.emit(response.type, response.payload)
}
})
// Important: Use a single on('message') listener for all requests
setupMessageHandler(ws, eventEmitter)

connectedWebSocket.on('close', () => {
//console.log('WebSocket client disconnected');
console.log('WebSocket client disconnected')
connectedWebSocket = null
eventEmitter.emit('connected', false)
// Optionally clean up pendingRequests or handle them as errors
// for (const requestId in pendingRequests) {
// pendingRequests[requestId].reject(new Error('WebSocket closed'))
// delete pendingRequests[requestId]
// }
})

connectedWebSocket.on('error', (error) => {
//console.error('WebSocket error:', error.message);
console.error('WebSocket error:', error)
connectedWebSocket = null
eventEmitter.emit('connected', false)
})
})

console.log(`WebSocket server running on ws://localhost:` + JSON.stringify((wsServer.address() as any).port))
console.log(`WebSocket server running on ws://localhost:${(wsServer.address() as any).port}`)
if ((process.env.NODE_ENV === 'production' || isPackaged) && !isE2ELocal) {
startServer()
} else {
// For local dev, maybe keep using port 8080
http_port = 8080
}

Expand All @@ -183,21 +210,23 @@ export const startHostServer = async (eventEmitter: EventEmitter) => {
}
}

function parseWithBigInt(json) {
const result = JSON.parse(json, (key, value) => {
if (typeof value === 'string' && /^\d+n?$/.test(value)) {
return BigInt(value.endsWith('n') ? value.slice(0, -1) : value)
}
return value
})
return result
function parseWithBigInt(json: string) {
// You can unify your approach here, either JSON.parse or try cbor first:
try {
return cbor.decode(json)
} catch (e) {
console.log('parseWithBigInt error', e, json)
console.log('parseWithBigInt error', e)
return {}
// Attempt JSON parse with BigInt
return JSON.parse(json, (key, value) => {
if (typeof value === 'string' && /^\d+n?$/.test(value)) {
return BigInt(value.endsWith('n') ? value.slice(0, -1) : value)
}
return value
})
} catch (jsonErr) {
// fallback to cbor if you like:
try {
return cbor.decode(json)
} catch (cborErr) {
console.log('parseWithBigInt error', cborErr, json)
return {}
}
}
console.log('parseWithBigInt', json)
return JSON.parse(json, (key, value) => (typeof value === 'string' && /^\d+n?$/.test(value) ? BigInt(value) : value))
}
}
Loading

0 comments on commit 47d6a8e

Please sign in to comment.