Restructure step 3: your server, Invite, version gate, AppGraph
ci / core (push) Successful in 27s
ci / server (push) Successful in 1m7s
ci / updater-windows (push) Failing after 6m4s

- One server per app run (loopback, no password); Invite attaches an
  all-interfaces listener to the same server and Stop sharing detaches
  it (guests told with 4016); members, media and Up next never reset.
  No solo mode: you're home on your own server unless in a friend's.
- Navigation: none / yours / friend; a join passes a version gate on
  /health first (clear "they/you need to update", update offer).
- Invite as a value (host, port, name, password flag, version, fp
  reserved); addresses filtered (loopback, link-local, 198.18/15,
  bridges) and ranked; Sharing chip, Copy invite, Stop sharing.
- Alone looks like a plain player (alone replaces solo).
- AppGraph builds and disposes systems; widgets use AppScope;
  SessionState deleted; a test caps the remaining singletons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-15 22:52:52 +03:00
1 parent 629be0d9ed
commit 4d00b7455e
86 files changed
+2993 -2071

No files matched your search

+21 -1
View File
@@ -487,6 +487,24 @@ class ServerHub {
changed();
}
/// The hosting app stopped sharing: every member but the host leaves
/// now (feed `leave`, reason `closed`), its socket closed with
/// [CloseCodes.closed] so its app does not reconnect. Returns how many
/// left.
int closeGuests() {
final guests = [
for (final m in engine.members.values)
if (!m.host) m.token,
];
for (final token in guests) {
final ws = sockets.remove(token);
if (ws != null) unawaited(closeWith(ws, CloseCodes.closed, 'the server stopped sharing'));
engine.applyLeave(token, reason: 'closed');
}
if (guests.isNotEmpty) changed();
return guests.length;
}
/// `bye`: [token]'s member leaves now.
void bye(String token) {
engine.applyLeave(token, reason: 'bye');
@@ -953,7 +971,9 @@ void mountSyncRoute(
return;
}
final session = hub.session;
if (session.secret.isNotEmpty && hello.secret != session.secret) {
// The hosting app's own client (its host key) needs no password: the
// password may change while it is connected ([SyncServer.share]).
if (session.secret.isNotEmpty && hello.secret != session.secret && !hub.isHostKey(hello.hostKey)) {
await refuse(const ErrorFrame(code: ErrCode.secret, message: 'wrong secret', forType: 'hello'),
CloseCodes.secret, '${cleanMemberName(hello.user.name)}: wrong secret');
return;