Server on an isolate, dedicated server binary, room -> server (proto 5)
- serve(Map config, events) is the one server entrypoint: the app hosts it via Isolate.spawn (crash surfaced on the Host card, logs forwarded as [server]); bin/server.dart is the dedicated server, compiled into the bundle as watch-together-server. --headless gone. - Host identity via a per-hosting hostKey in hello, replacing the loopback heuristic. - Rename room -> server across code, files and wire (t:"state", /health name); protocol bumped to 5. - CI compiles the server binary; isolate smoke tests in server and desktop suites. Docs updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8a0729029
commit
81cac1214d
48 files changed
+1073
-423
No files matched your search
+32
-32
@@ -19,9 +19,9 @@ class RateAction extends PlayerAction {
|
||||
const RateAction(this.rate);
|
||||
}
|
||||
|
||||
double roomPositionAt(RoomBroadcast room, DateTime now) {
|
||||
if (room.transport != 'playing') return room.position;
|
||||
return room.position + now.difference(room.serverTime).inMilliseconds / 1000.0;
|
||||
double serverPositionAt(StateBroadcast state, DateTime now) {
|
||||
if (state.transport != 'playing') return state.position;
|
||||
return state.position + now.difference(state.serverTime).inMilliseconds / 1000.0;
|
||||
}
|
||||
|
||||
bool seekLanded({required double ownPosition, required double target}) {
|
||||
@@ -31,9 +31,9 @@ bool seekLanded({required double ownPosition, required double target}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool localPlayAllowed(RoomBroadcast room, String memberId) {
|
||||
bool localPlayAllowed(StateBroadcast state, String memberId) {
|
||||
if (memberId.isEmpty) return false;
|
||||
for (final m in room.members) {
|
||||
for (final m in state.members) {
|
||||
if (m.id == memberId) return m.inSet && m.ready;
|
||||
}
|
||||
return false;
|
||||
@@ -45,7 +45,7 @@ bool localPlayAllowed(RoomBroadcast room, String memberId) {
|
||||
bool shouldReassertReady(MemberView? self) =>
|
||||
self != null && self.ready && !self.inSet && !self.stale;
|
||||
|
||||
class RoomDiff {
|
||||
class StateDiff {
|
||||
static const debounceWindow = Duration(seconds: 2);
|
||||
|
||||
int _lastEventId = -1;
|
||||
@@ -57,14 +57,14 @@ class RoomDiff {
|
||||
|
||||
bool get slowing => _slowing;
|
||||
|
||||
List<PlayerAction> diff(RoomBroadcast room, double ownPosition, DateTime now) {
|
||||
List<PlayerAction> diff(StateBroadcast state, double ownPosition, DateTime now) {
|
||||
final out = <PlayerAction>[];
|
||||
final roomPos = roomPositionAt(room, now);
|
||||
final serverPos = serverPositionAt(state, now);
|
||||
|
||||
if (room.mediaId != _lastMediaId && _lastMediaId != -1) {
|
||||
_lastEventId = room.eventId;
|
||||
_lastTransport = room.transport;
|
||||
_lastMediaId = room.mediaId;
|
||||
if (state.mediaId != _lastMediaId && _lastMediaId != -1) {
|
||||
_lastEventId = state.eventId;
|
||||
_lastTransport = state.transport;
|
||||
_lastMediaId = state.mediaId;
|
||||
_slowing = false;
|
||||
_deviatingSince = null;
|
||||
out.add(const SeekAction(0));
|
||||
@@ -74,39 +74,39 @@ class RoomDiff {
|
||||
}
|
||||
|
||||
if (_lastEventId == -1) {
|
||||
_lastEventId = room.eventId;
|
||||
_lastTransport = room.transport;
|
||||
_lastMediaId = room.mediaId;
|
||||
_lastEventId = state.eventId;
|
||||
_lastTransport = state.transport;
|
||||
_lastMediaId = state.mediaId;
|
||||
_deviatingSince = null;
|
||||
out.add(SeekAction(roomPos));
|
||||
out.add(TransportAction(room.transport == 'playing'));
|
||||
out.add(SeekAction(serverPos));
|
||||
out.add(TransportAction(state.transport == 'playing'));
|
||||
return out;
|
||||
}
|
||||
|
||||
if (room.eventId != _lastEventId) {
|
||||
_lastEventId = room.eventId;
|
||||
if (state.eventId != _lastEventId) {
|
||||
_lastEventId = state.eventId;
|
||||
_slowing = false;
|
||||
_deviatingSince = null;
|
||||
out.add(const RateAction(1.0));
|
||||
out.add(SeekAction(roomPos));
|
||||
if (room.transport != _lastTransport) {
|
||||
_lastTransport = room.transport;
|
||||
out.add(TransportAction(room.transport == 'playing'));
|
||||
out.add(SeekAction(serverPos));
|
||||
if (state.transport != _lastTransport) {
|
||||
_lastTransport = state.transport;
|
||||
out.add(TransportAction(state.transport == 'playing'));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (room.transport != _lastTransport) {
|
||||
_lastTransport = room.transport;
|
||||
if (state.transport != _lastTransport) {
|
||||
_lastTransport = state.transport;
|
||||
_slowing = false;
|
||||
_deviatingSince = null;
|
||||
out.add(const RateAction(1.0));
|
||||
out.add(TransportAction(room.transport == 'playing'));
|
||||
out.add(SeekAction(roomPos));
|
||||
out.add(TransportAction(state.transport == 'playing'));
|
||||
out.add(SeekAction(serverPos));
|
||||
return out;
|
||||
}
|
||||
|
||||
final drift = ownPosition - roomPos;
|
||||
final drift = ownPosition - serverPos;
|
||||
if (_slowing) {
|
||||
if (drift.abs() < 0.1) {
|
||||
_slowing = false;
|
||||
@@ -114,10 +114,10 @@ class RoomDiff {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
switch (correctionFor(ownPosition: ownPosition, roomPosition: roomPos)) {
|
||||
switch (correctionFor(ownPosition: ownPosition, serverPosition: serverPos)) {
|
||||
case Correction.seek:
|
||||
if (_deviatingEvent != room.eventId || _deviatingSince == null) {
|
||||
_deviatingEvent = room.eventId;
|
||||
if (_deviatingEvent != state.eventId || _deviatingSince == null) {
|
||||
_deviatingEvent = state.eventId;
|
||||
_deviatingSince = now;
|
||||
break;
|
||||
}
|
||||
@@ -125,7 +125,7 @@ class RoomDiff {
|
||||
break;
|
||||
}
|
||||
_deviatingSince = null;
|
||||
out.add(SeekAction(roomPos));
|
||||
out.add(SeekAction(serverPos));
|
||||
case Correction.slowDown:
|
||||
_deviatingSince = null;
|
||||
_slowing = true;
|
||||
@@ -9,13 +9,13 @@ enum SyncStatus { idle, connecting, open, kicked, closed, error }
|
||||
class SyncClient {
|
||||
WebSocketChannel? _channel;
|
||||
StreamSubscription? _sub;
|
||||
final StreamController<RoomBroadcast> _rooms = StreamController.broadcast();
|
||||
final StreamController<StateBroadcast> _states = StreamController.broadcast();
|
||||
final StreamController<FeedMessage> _feeds = StreamController.broadcast();
|
||||
final StreamController<String> _kicked = StreamController.broadcast();
|
||||
final StreamController<String> _errors = StreamController.broadcast();
|
||||
final StreamController<SyncStatus> _status = StreamController.broadcast();
|
||||
|
||||
Stream<RoomBroadcast> get rooms => _rooms.stream;
|
||||
Stream<StateBroadcast> get states => _states.stream;
|
||||
Stream<FeedMessage> get feeds => _feeds.stream;
|
||||
Stream<String> get kicked => _kicked.stream;
|
||||
Stream<String> get errors => _errors.stream;
|
||||
@@ -24,7 +24,7 @@ class SyncClient {
|
||||
String token = '';
|
||||
String memberId = '';
|
||||
String? mediaUrl;
|
||||
RoomBroadcast? lastRoom;
|
||||
StateBroadcast? lastState;
|
||||
Completer<WelcomeMessage>? _pendingWelcome;
|
||||
|
||||
Future<WelcomeMessage> connect({
|
||||
@@ -33,6 +33,7 @@ class SyncClient {
|
||||
required String name,
|
||||
String avatar = '',
|
||||
HelloMedia? media,
|
||||
String hostKey = '',
|
||||
}) async {
|
||||
await disconnect();
|
||||
_status.add(SyncStatus.connecting);
|
||||
@@ -61,6 +62,7 @@ class SyncClient {
|
||||
secret: secret,
|
||||
user: HelloUser(name: name, avatar: avatar),
|
||||
media: media ?? const HelloMedia(name: '', size: 0, duration: 0),
|
||||
hostKey: hostKey,
|
||||
);
|
||||
ch.sink.add(jsonEncode(hello.toJson()));
|
||||
_pendingWelcome = welcomeCompleter;
|
||||
@@ -74,8 +76,8 @@ class SyncClient {
|
||||
mediaUrl = welcome.mediaUrl;
|
||||
if (welcome.state != null) {
|
||||
try {
|
||||
lastRoom = RoomBroadcast.fromJson(welcome.state!);
|
||||
_rooms.add(lastRoom!);
|
||||
lastState = StateBroadcast.fromJson(welcome.state!);
|
||||
_states.add(lastState!);
|
||||
} catch (_) {}
|
||||
}
|
||||
_status.add(SyncStatus.open);
|
||||
@@ -113,11 +115,11 @@ class SyncClient {
|
||||
return;
|
||||
}
|
||||
switch (t) {
|
||||
case 'room':
|
||||
case 'state':
|
||||
try {
|
||||
final room = RoomBroadcast.fromJson(decoded);
|
||||
lastRoom = room;
|
||||
_rooms.add(room);
|
||||
final state = StateBroadcast.fromJson(decoded);
|
||||
lastState = state;
|
||||
_states.add(state);
|
||||
} catch (_) {}
|
||||
case 'feed':
|
||||
try {
|
||||
@@ -219,12 +221,12 @@ class SyncClient {
|
||||
token = '';
|
||||
memberId = '';
|
||||
mediaUrl = null;
|
||||
lastRoom = null;
|
||||
lastState = null;
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await disconnect();
|
||||
await _rooms.close();
|
||||
await _states.close();
|
||||
await _feeds.close();
|
||||
await _kicked.close();
|
||||
await _errors.close();
|
||||
|
||||
Reference in new issue
Block a user