LogoRevali

WebSockets

Real-time bidirectional communication between client and server

Annotation: @WebSocket

WebSockets enable real-time, bidirectional communication between client and server. Unlike HTTP's request-response pattern, WebSockets allow both sides to send messages at any time.

Creating a WebSocket Handler#

Create a WebSocket handler by annotating a method with @WebSocket:

import 'package:revali_router/revali_router.dart';

@Controller('chat')
class ChatController {
  const ChatController();

  @WebSocket('messages')
  String handleMessage(@Body() String message) {
    return 'Echo: $message';
  }
}

WebSocket Modes#

Two-Way Communication (Default)#

Both client and server can send messages:

@WebSocket('messages', mode: WebSocketMode.twoWay)
String handleMessage(@Body() String message) {
  return 'Echo: $message';
}

Receive-Only#

Server only receives messages from client:

@WebSocket('messages', mode: WebSocketMode.receiveOnly)
void handleMessage(@Body() String message) {
  // Process message without sending response
  print('Received: $message');
}

Send-Only#

Server only sends messages to client:

@WebSocket('notifications', mode: WebSocketMode.sendOnly)
String sendNotification() {
  return 'Server notification';
}

Handling Messages#

Basic Message Handling#

@WebSocket('chat')
String handleChatMessage(@Body() String message) {
  // Process the message
  final processedMessage = processMessage(message);

  // Return response to client
  return 'Server: $processedMessage';
}

JSON Messages#

class ChatMessage {
  const ChatMessage({required this.user, required this.text});

  final String user;
  final String text;

  factory ChatMessage.fromJson(Map<String, dynamic> json) {
    return ChatMessage(
      user: json['user'] as String,
      text: json['text'] as String,
    );
  }
}

@WebSocket('chat')
String handleChatMessage(@Body() ChatMessage message) {
  return '${message.user}: ${message.text}';
}

Async Message Handling#

@WebSocket('async')
Future<String> handleAsyncMessage(@Body() String message) async {
  // Simulate async processing
  await Future.delayed(const Duration(seconds: 1));

  return 'Processed: $message';
}

Sending Messages Without Client Input#

Use AsyncWebSocketSender to send messages without waiting for client input:

@WebSocket('notifications')
String sendNotifications(AsyncWebSocketSender<String> sender) {
  // Send periodic notifications
  Timer.periodic(const Duration(seconds: 5), (timer) {
    sender.send('Notification ${DateTime.now()}');
  });

  return 'Notifications started';
}

Connection Management#

Closing Connections#

Send-Only Mode

Connections close automatically when the handler returns:

@WebSocket('send-only', mode: WebSocketMode.sendOnly)
String sendData() {
  return 'Final message'; // Connection closes after this
}

Two-Way and Receive-Only Modes

Connections stay open until manually closed:

@WebSocket('persistent', mode: WebSocketMode.twoWay)
String handleMessage(CloseWebSocket closer) {
  // Check if connection should close
  if (shouldCloseConnection()) {
    closer.close(1000, 'Normal closure');
    return '';
  }

  return 'Message processed';
}

Close Codes#

WebSocket close codes follow HTTP status code principles but use the range 1000-4999:

// Normal closure
closer.close(1000, 'Normal closure');

// Going away
closer.close(1001, 'Going away');

// Protocol error
closer.close(1002, 'Protocol error');

// Unsupported data
closer.close(1003, 'Unsupported data');

// Custom application codes (4000-4999)
closer.close(4000, 'Custom application error');

Advanced Features#

Ping/Pong#

Enable automatic ping/pong to keep connections alive:

@WebSocket.ping('heartbeat', ping: Duration(seconds: 30))
String handleHeartbeat(@Body() String? message) {
  return 'Pong: ${DateTime.now()}';
}

On Connect Trigger#

Run handler when connection is established:

@WebSocket('connection', triggerOnConnect: true)
String onConnect(@Body() String? message) {
  if (message == null) {
    return 'Connected to WebSocket';
  }

  return 'Message: $message';
}

Real-World Examples#

Chat Application#

@Controller('chat')
class ChatController {
  final List<String> _messages = [];

  @WebSocket('messages')
  String handleMessage(@Body() String message) {
    _messages.add('${DateTime.now()}: $message');

    // Broadcast to all connected clients
    broadcastMessage(message);

    return 'Message received';
  }

  void broadcastMessage(String message) {
    // Implementation depends on your broadcasting strategy
  }
}

Real-Time Notifications#

@Controller('notifications')
class NotificationController {
  @WebSocket('stream', mode: WebSocketMode.sendOnly)
  String streamNotifications(AsyncWebSocketSender<String> sender) {
    // Stream notifications from a service
    notificationService.stream.listen((notification) {
      sender.send(notification.toJson());
    });

    return 'Notification stream started';
  }
}

File Upload Progress#

@Controller('upload')
class UploadController {
  @WebSocket('progress')
  String handleUpload(@Body() Map<String, dynamic> data) {
    final progress = data['progress'] as int;
    final filename = data['filename'] as String;

    if (progress == 100) {
      return 'Upload complete: $filename';
    }

    return 'Upload progress: $progress%';
  }
}

Client Connection#

Using web_socket_channel#

import 'dart:convert';
import 'package:web_socket_channel/io.dart';

void main() {
  final channel = IOWebSocketChannel.connect('ws://localhost:8080/chat/messages');

  // Listen for messages
  channel.stream.listen(
    (message) {
      print('Received: $message');
    },
    onError: (error) {
      print('Error: $error');
    },
    onDone: () {
      print('Connection closed');
    },
  );

  // Send message
  channel.sink.add('Hello WebSocket!');
}

Using dart:io WebSocket#

import 'dart:io';

void main() async {
  final socket = await WebSocket.connect('ws://localhost:8080/chat/messages');

  socket.listen(
    (message) {
      print('Received: $message');
    },
    onError: (error) {
      print('Error: $error');
    },
    onDone: () {
      print('Connection closed');
    },
  );

  socket.add('Hello WebSocket!');
}

WebSocket Lifecycle#

The WebSocket lifecycle follows this pattern:

  1. Open Connection - Client connects
  2. Observer - Pre-connection observers run
  3. Middleware - Middleware components run
  4. Guard - Guard components run
  5. On Connect - Handler runs if triggerOnConnect: true
  6. Message Loop - For each message:
    • Interceptor (Pre)
    • Endpoint Handler
    • Interceptor (Post)
  7. Observer - Post-connection observers run
  8. Close Connection - Connection closes

Best Practices#

Use Appropriate Modes#

// ✅ Good - Use send-only for notifications
@WebSocket('notifications', mode: WebSocketMode.sendOnly)
String sendNotification() {
  return 'Notification';
}

// ✅ Good - Use receive-only for logging
@WebSocket('logs', mode: WebSocketMode.receiveOnly)
void logMessage(@Body() String message) {
  logger.info(message);
}

Handle Connection Cleanup#

// ✅ Good - Proper cleanup
@WebSocket('data')
String handleData(CloseWebSocket closer, CleanUp cleanUp) {
  cleanUp.add(() {
    // Cleanup resources when connection closes
    resourceService.dispose();
  });

  return 'Data processed';
}

Use Type-Safe Messages#

// ✅ Good - Type-safe message handling
class ChatMessage {
  const ChatMessage({required this.user, required this.text});
  final String user;
  final String text;

  factory ChatMessage.fromJson(Map<String, dynamic> json) {
    return ChatMessage(
      user: json['user'] as String,
      text: json['text'] as String,
    );
  }
}

@WebSocket('chat')
String handleChat(@Body() ChatMessage message) {
  return '${message.user}: ${message.text}';
}

What's Next?#