Backend: - fix(chat): ChatGateway stored the per-socket caller only after the async token check resolved, so a client that sent chat:join immediately on open raced ahead and got 4001. The caller is now stored as a promise that the message handlers await. Verified with a two-client send/receive E2E test against local Postgres. Client (client/app/): - Wahl screen gains a "Ergebnis" tab backed by GET /wahl/guest/results (PENDING / ASSIGNED with workshop + wish rank / UNASSIGNED). - Chat channel view loads history over REST, then connects the /chat WebSocket (chat_socket.dart): live chat:message stream + a compose bar that sends chat:send. Shows the socket status. - web_socket_channel dependency added. flutter analyze/test/build web all green; backend npm test 56. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
58 lines
1.6 KiB
Dart
58 lines
1.6 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
|
|
import 'api.dart';
|
|
|
|
/// Thin wrapper over the raw `ws` chat gateway. The NestJS `WsAdapter`
|
|
/// expects `{"event": ..., "data": ...}` frames in both directions.
|
|
class ChatSocket {
|
|
ChatSocket(this._uri);
|
|
final Uri _uri;
|
|
|
|
WebSocketChannel? _channel;
|
|
final _messages = StreamController<ChatMessage>.broadcast();
|
|
final _status = StreamController<String>.broadcast();
|
|
|
|
/// Incoming `chat:message` frames.
|
|
Stream<ChatMessage> get messages => _messages.stream;
|
|
|
|
/// "connected" / "closed" / "error: ..." for a small status line.
|
|
Stream<String> get status => _status.stream;
|
|
|
|
void connect(String channelId) {
|
|
_channel = WebSocketChannel.connect(_uri);
|
|
_channel!.stream.listen(
|
|
(raw) {
|
|
_status.add('connected');
|
|
try {
|
|
final frame = jsonDecode(raw as String) as Map<String, dynamic>;
|
|
if (frame['event'] == 'chat:message') {
|
|
_messages.add(ChatMessage.fromJson(frame['data'] as Map<String, dynamic>));
|
|
}
|
|
} catch (_) {
|
|
// ignore frames we don't model
|
|
}
|
|
},
|
|
onError: (Object e) => _status.add('error: $e'),
|
|
onDone: () => _status.add('closed'),
|
|
);
|
|
_send('chat:join', {'channelId': channelId});
|
|
}
|
|
|
|
void sendMessage(String channelId, String body) {
|
|
_send('chat:send', {'channelId': channelId, 'body': body});
|
|
}
|
|
|
|
void _send(String event, Map<String, dynamic> data) {
|
|
_channel?.sink.add(jsonEncode({'event': event, 'data': data}));
|
|
}
|
|
|
|
void dispose() {
|
|
_channel?.sink.close();
|
|
_messages.close();
|
|
_status.close();
|
|
}
|
|
}
|