Restructure step 0: Connection extracted from the coordinator
Connection owns the socket lifecycle, connect/dial with epochs, reconnect, the clock offset, diagnostics upload and a sealed ConnState signal; the hello is built by an injectable builder, the resume token stays private, and the reconnect delay and silence limit are parameters. The coordinator maps ConnState to the same health texts and keeps the reconnect restore and drop handling (step 1 replaces them). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a52133b313
commit
eac45746ce
13 files changed
+780
-368
No files matched your search
+50
-8
@@ -9,7 +9,7 @@ Plain overview first: [HOW-IT-WORKS.md](HOW-IT-WORKS.md). Wire format:
|
||||
|---|---|---|
|
||||
| `packages/core` | protocol messages, sync engine (`SyncEngine`), constants, config, Range parsing, logging, Jellyfin client, updater, watched rule (`WatchSession`) | pure Dart, no Flutter |
|
||||
| `packages/server` | Relic HTTP + WebSocket server (`SyncServer`, `ServerHub`, `Session`, media endpoint, relay buffer, Jellyfin relay), the `serve()` entrypoint, `bin/server.dart` (dedicated server) | pure Dart, no Flutter |
|
||||
| `apps/desktop` | Flutter app: UI, `SyncClient`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, update UI (`UpdateController`) | ships with `watch-together-server` |
|
||||
| `apps/desktop` | Flutter app: UI, `SyncClient`, `Connection`, `SessionCoordinator`, `PlaybackController` (media_kit/mpv), `ServerIsolate`, update UI (`UpdateController`) | ships with `watch-together-server` |
|
||||
| `tool` | harnesses: `range_logger`, `shaper`, `fake_viewer`, `e2e_check` | dev only |
|
||||
| `deploy` | systemd unit for the dedicated server | |
|
||||
|
||||
@@ -93,8 +93,9 @@ Loosely modeled on Syncplay, with a different readiness design.
|
||||
a `ping` with its clock every second and estimates the offset NTP style
|
||||
(`ClockSync`: `offset = s - (t0 + t3) / 2` of the sample with the smallest
|
||||
round trip among the last 16). All extrapolation uses
|
||||
`SyncClient.serverNow()` (local clock plus offset), so a client whose clock
|
||||
is off still lands on the server position.
|
||||
`Connection.serverNow()` (= `SyncClient.serverNow()`: local clock plus
|
||||
offset), so a client whose clock is off still lands on the server
|
||||
position.
|
||||
- **Reports**: clients send `position`, `buffered`, `eventId` every 1 s, plus
|
||||
the media `duration` and its `mediaId` once the player knows it. The
|
||||
server stores `buffered` and liveness, and takes the first reported
|
||||
@@ -191,9 +192,11 @@ Loosely modeled on Syncplay, with a different readiness design.
|
||||
as a new member.
|
||||
- **Client policies**: no state broadcast for 5 s means connection lost, clear
|
||||
the server state and reconnect every 1.5 s until cancelled (an epoch counter makes
|
||||
a cancelled attempt stale). After a reconnect the ready flag is
|
||||
a cancelled attempt stale). A closed socket means lost at once (the
|
||||
server state stays until the 5 s pass). After a reconnect the ready flag is
|
||||
restored from the server's state. Cancel during setup aborts the join.
|
||||
Closing the window leaves gracefully (see "App close" under Desktop UI).
|
||||
Where it lives: see "Connection" below.
|
||||
- **Idle** (`IdleTracker`): measured from the later of the last input and the
|
||||
moment playback stopped. While HELD and self ready: warning at 8 min,
|
||||
auto-unready at 10 min, and never less than 2 min after the warning.
|
||||
@@ -213,6 +216,45 @@ Loosely modeled on Syncplay, with a different readiness design.
|
||||
|
||||
Additive wire changes only; bump `proto` and PROTOCOL.md in the same commit.
|
||||
|
||||
## Connection
|
||||
|
||||
`Connection` (`session/connection.dart`) is the link to the server, made
|
||||
in the composition root (`SessionState.connection`) and passed to the
|
||||
`SessionCoordinator`. It owns the `SyncClient` socket, the first connect
|
||||
(`begin` + `dial`), reconnects (`reconnectDelay`, 1.5 s; the previous
|
||||
token as `hello.resume`), the epoch (`invalidate`, `isCurrent`) that makes
|
||||
a cancelled connect or attempt stale, the 1 s heartbeat (clock `ping`,
|
||||
the diagnostics tick, the silence check: `silenceLimit`, 5 s), the
|
||||
diagnostics uploader (`diag`, started with the session, stopped by
|
||||
`close` before the socket), "Stop reconnecting" (`cancelReconnect`),
|
||||
"Retry" (`retry`) and setup cancel (`cancelConnect`). The `hello` is
|
||||
built by an injectable `HelloBuilder` (`defaultHello`). The incoming
|
||||
streams (`states`, `feeds`, `nudges`, `errors`), the send methods, the
|
||||
`token`, `memberId` and `serverNow()` are read through it.
|
||||
|
||||
`Connection.state` (a signal of the sealed `ConnState`): `ConnClosed`
|
||||
(before a connect, after leave or a cancel), `ConnConnecting` (the first
|
||||
dial), `ConnOnline` (welcome received), `ConnLost` (dropped: the socket
|
||||
closed or the silence limit passed; the next attempt is scheduled),
|
||||
`ConnReconnecting(attempt)` (an attempt dialling, and after it failed
|
||||
until the next one), `ConnRefused(code, message)` (the first dial failed:
|
||||
`version`, `secret` or empty, and the plain-words reason; nothing
|
||||
retries) and `ConnReplaced` (not produced yet). Every change notifies,
|
||||
a repeat too. The coordinator maps it to `health` (`connectionHealth`:
|
||||
"connecting…", "connected", "connection lost — retrying…",
|
||||
"reconnecting…", the reason; closed writes nothing, a cancel writes
|
||||
"cancelled", "Retry" "retrying…") and to `reconnecting` (lost or
|
||||
reconnecting). The first drop of a connection raises `ConnectionLost`,
|
||||
the reconnect that follows `ConnectionBack` (each once per drop, through
|
||||
the Notifier).
|
||||
|
||||
The session's part of a reconnect is the coordinator's
|
||||
`onReconnect` handler: before an attempt dials it reads what to restore
|
||||
(ready, sat out, the media seen), once online it opens the media again
|
||||
if its URL changed, sends `out`/`ready` back and restarts the session's
|
||||
ticker. The silence (`Connection.silence`) clears the coordinator's
|
||||
server state (`_onConnectionSilent`).
|
||||
|
||||
## Media
|
||||
|
||||
Three sources, same sync. Any of them can also go through the relay
|
||||
@@ -1109,7 +1151,7 @@ tab's card errors are still its own snack bars.
|
||||
**Ports.** UI and session code reach the platform through interfaces
|
||||
(`platform/ports/`), made once in the composition root
|
||||
(`SessionState.ports`, `desktopPorts()` in `platform/desktop/`) and
|
||||
handed on from there (the coordinator gets its `Notifier` and
|
||||
handed on from there (the coordinator gets its `Notifier`, `Connection` and
|
||||
`LinkResolver` as constructor arguments):
|
||||
- `ShellPort` (`DesktopShell`): window start (remembered bounds, the
|
||||
close request routed to `shutDownApp`), fullscreen, minimize/maximize
|
||||
@@ -1697,10 +1739,10 @@ dedicated server (`package:logging` loggers named by area: `relay`,
|
||||
- **The session log** (`packages/server/lib/src/session_log.dart`,
|
||||
`packages/core/lib/src/diag_share.dart`): one shared log per server so
|
||||
a problem can be read afterwards from every side. While in a session
|
||||
the app (`DiagUploader`, owned by the coordinator) keeps its own
|
||||
the app (`DiagUploader`, owned by the `Connection`) keeps its own
|
||||
records (not its hosted server's `server.*`, which the server logs
|
||||
itself), redacted, and sends them every 10 s from the session's 1 s
|
||||
ticker and when it leaves (before its token goes, at most 1.5 s), as
|
||||
itself), redacted, and sends them every 10 s from the connection's 1 s
|
||||
heartbeat and when it leaves (before its token goes, at most 1.5 s), as
|
||||
gzip batches to `POST /l/<sid>/<token>` (PROTOCOL
|
||||
"Diagnostics"), timestamps moved to the server's clock with the ping
|
||||
offset (`ClockSync`), the app version in every batch. Until it can
|
||||
|
||||
+4
-6
@@ -85,15 +85,13 @@ the automatic continuation and countdown, relay state), the device
|
||||
(client id, library and folders, match corrections, watch history,
|
||||
settings, notification prefs, what this app is sourcing) and the
|
||||
integrations (Jellyfin played/resume, AniList list).
|
||||
0. Safety net and seams, no visible change. Left: Connection extracted
|
||||
from the coordinator (one sealed connection state, hello built by a
|
||||
factory, reconnect delay and silence threshold as parameters); a
|
||||
`noticeFor` table test; a flaky-link test harness (pure-Dart TCP proxy:
|
||||
pass, blackhole, delay, reset, late frame); grace and silence through
|
||||
0. Safety net and seams, no visible change. Left: a `noticeFor` table
|
||||
test; a flaky-link test harness (pure-Dart TCP proxy: pass, blackhole,
|
||||
delay, reset, late frame); grace and silence through
|
||||
the engine clock; an env override for client id and data folder (two
|
||||
instances on one machine); the scenario list in DEVELOPMENT "Tests".
|
||||
(Done: tests audited and tagged, P27, AppEvent + Notifier, ports,
|
||||
protocol 16 planned.)
|
||||
protocol 16 planned, Connection extracted from the coordinator.)
|
||||
R. Rename (before step 1, so dev builds install side by side with 1.2.x):
|
||||
app id `dev.foxyz.starsync`, binary, Windows company `foxyz`; Hive out
|
||||
of `~/Documents` into the app's support folder, copied not moved;
|
||||
|
||||
Reference in new issue
Block a user