With the rise of real-time applications, developers are constantly seeking efficient ways to handle live data streams and process files on the fly. Deno, a modern runtime for JavaScript and TypeScript, offers robust WebSocket support and enhanced security features that make it an excellent choice for building secure, real-time file processing systems.

In this DevTip, we'll explore how to leverage Deno's WebSocket capabilities and its secure permissions model to implement a real-time WebSocket server. This pattern can serve as a foundation for real-time file processing tasks—such as monitoring and processing file data—as part of a broader application ecosystem.

Why Deno?

Deno provides a secure runtime that includes TypeScript support, an integrated permissions system, and a sandboxed environment. These features help ensure that operations, including file processing tasks, are executed safely without exposing unnecessary system resources. Unlike Node.js, Deno focuses on security and simplicity, giving you more control over network and file system access.

Setting up a websocket server

Create a new file called server.ts and add the following code:

Deno.serve((req) => {
  if (req.headers.get('upgrade') !== 'websocket') {
    return new Response(null, { status: 501 })
  }

  const { socket, response } = Deno.upgradeWebSocket(req)

  socket.addEventListener('open', () => {
    console.log('A client connected!')
  })

  socket.addEventListener('message', (event) => {
    if (event.data === 'ping') {
      // In a real application, you might trigger file processing here
      socket.send('pong')
    }
  })

  socket.addEventListener('error', (event) => {
    console.error('WebSocket error:', event)
  })

  socket.addEventListener('close', () => {
    console.log('A client disconnected')
  })

  return response
})

Running the server

To run the server, execute:

deno run --allow-net server.ts

Understanding the websocket lifecycle

WebSocket connections transition through several states:

  1. CONNECTING (0): The initial state while establishing the connection.
  2. OPEN (1): The connection is active and ready for data transfer.
  3. CLOSING (2): The connection is in the process of closing.
  4. CLOSED (3): The connection has been terminated.

For example, you can check if a socket is ready before sending a message:

function isSocketReady(socket: WebSocket): boolean {
  return socket.readyState === WebSocket.OPEN
}

socket.onmessage = (event) => {
  if (!isSocketReady(socket)) {
    return
  }
  // Handle message
}

Client implementation

Below is a streamlined client example to test the server:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>WebSocket Test</title>
  </head>
  <body>
    <script>
      const ws = new WebSocket('ws://localhost:8000')
      ws.onopen = () => {
        console.log('Connected to the server')
        ws.send('ping')
      }
      ws.onmessage = (event) => {
        console.log('Received:', event.data)
      }
      ws.onerror = (error) => {
        console.error('WebSocket error:', error)
      }
      ws.onclose = () => {
        console.log('Disconnected from the server')
      }
    </script>
  </body>
</html>

Security best practices

When building real-time applications, especially those involving file processing, consider these security measures:

1. Input validation

Ensure incoming data is correctly formatted to avoid processing unexpected inputs.

function validateMessage(data: unknown): boolean {
  if (typeof data !== 'string') {
    return false
  }

  try {
    const message = JSON.parse(data)
    return typeof message === 'object' && message !== null && typeof message.type === 'string'
  } catch {
    return false
  }
}

2. Rate limiting

Integrate a rate limiter to prevent abuse and ensure system stability.

class RateLimiter {
  private requests = new Map<string, number[]>()
  private readonly limit: number
  private readonly window: number

  constructor(limit = 100, window = 60000) {
    this.limit = limit
    this.window = window
  }

  isAllowed(clientId: string): boolean {
    const now = Date.now()
    const timestamps = this.requests.get(clientId) || []
    const recent = timestamps.filter((time) => now - time < this.window)

    if (recent.length >= this.limit) {
      return false
    }

    recent.push(now)
    this.requests.set(clientId, recent)
    return true
  }
}

3. Message size limits

Constrain message sizes to mitigate the risk of denial-of-service attacks.

const MAX_MESSAGE_SIZE = 1024 * 1024 // 1MB

socket.addEventListener('message', (event) => {
  if (typeof event.data === 'string' && event.data.length > MAX_MESSAGE_SIZE) {
    socket.send(
      JSON.stringify({
        type: 'error',
        message: 'Message size exceeds limit',
      }),
    )
    return
  }
  // Process message
})

Conclusion

Deno's built-in WebSocket support and strong security model make it a powerful tool for developing real-time applications, including those that process files dynamically. By applying the best practices outlined in this guide, you can build secure and efficient systems capable of handling real-time data and file operations.

At Transloadit, we recognize the value of secure, real-time communication. Explore our File Filtering service to see how advanced file handling can complement your real-time applications.