- `LIVE_RELOAD=false npm run dev` keeps the renderer alive across a machine suspend; the reload client otherwise destroys the session under test. - `dev-peer.sh` plus a separate userdata dir runs a second local peer. - `tools/voice-probe.js` samples peer state and RTP counters from a live window, persisting to localStorage so a renderer reload cannot erase it. - e2e helpers for voice pairs, peer-role election, and a TURN relay. - Electron single-instance and dev-client-load decisions move into rules files with colocated specs.
4.9 KiB
03 — Architecture map (for agents)
Bounded contexts (product client)
See toju-app/src/app/domains/README.md. Emergency-relevant domains:
| Domain | Owns |
|---|---|
authentication |
Login/register HTTP, provision secret, per-URL credentials, authorize navigation |
server-directory |
Endpoints, health, discovery, invites, room metadata affinity |
chat |
Message rules, sync rules, chat UI |
direct-message |
DMs, friends, offline queue, PeerDelivery usage |
direct-call |
Private call sessions / rings |
voice-session |
Join/leave bookkeeping, floating controls, settings storage |
voice-connection |
Facade over realtime for mic/camera/playback/VAD |
screen-share |
Picker / quality |
attachment |
Chunked P2P files |
access-control |
Permissions / bans |
Infrastructure (not a domain): infrastructure/realtime/ (WebRTC + signaling), infrastructure/persistence/, infrastructure/mobile/.
Global NgRx: store/users, store/rooms, store/messages — orchestration across domains.
Identity & credentials
Home profile (NgRx User)
id / username / displayName / homeSignalServerUrl
│
├─ AuthTokenStore (legacy per-URL token) metoyou.authTokens
├─ SignalServerCredentialStore metoyou.signalServerCredentials
│ { serverUrl → userId, token, provisioned? }
└─ ProvisionSecretStore (per home user id)
Electron safeStorage | web sessionStorage
Actor resolution for a room: SignalServerAuthService.resolveActorUserIdForServer(sourceUrl, homeOderId) — foreign URL must use provisioned userId.
Identify on socket: SignalingTransportHandler.getIdentifyCredentialsForSignalUrl(url) must resolve token+actor for that URL (store fallback).
Danger: getIdentifyCredentials() returns home credential — used for some localOderId / polite-peer paths → cross-signal initiator bugs (06, 09).
Room ↔ signal affinity
Room { id, channels[], sourceUrl, sourceId, ... }
│
▼
RoomSignalingConnection
ensureCredentialForServerUrl(sourceUrl)
identify(actor for sourceUrl)
join_server / view_server on that WS
│
▼
ServerSignalingCoordinator
maps serverId ↔ signalUrl ↔ peers
Fallback: try other online compatible endpoints on outage; do not treat Cloudflare 521/522 as “client incompatible”. Non-federated: peers must converge on same endpoint for that room.
Realtime composition root
WebRTCService (realtime-session.service.ts) wires:
SignalingTransportHandler→ manySignalingManager(one WS per URL)PeerConnectionManager→ negotiation, DC, recoveryMediaManager/ noise / screen shareWebRtcStateController(signals)
Inbound WS → SignalingMessageHandler → users/rooms/voice/chat effects.
Message send path (server channel)
Composer → MessagesActions.sendMessage({ id? })
→ local DB + NgRx
→ DC broadcast chat-message (+ message-revision)
→ WS chat_message fallback (room members)
→ account_sync to sibling devices
Receive gates: room must be current or saved; channel scoping for text.
Voice join path
UI join channel
→ VoiceSessionFacade startSession
→ leave previous exclusive target
→ enableVoice / heartbeat
→ voice_state on WS (+ DC control)
→ MediaManager.syncVoiceRouting (same channel only)
→ VoicePlaybackService for remote same-channel peers
Peer PC may already exist from presence mesh; join mainly attaches tracks + announces state.
Call / DM delivery path
DirectCallService / DirectMessageService
→ PeerDeliveryService
1) data channel if open
2) signaling forward (targetUserId)
3) offline queue (DM) / silent fail (call if ignored)
Inbound call admission: direct-call-participant-identity.rules.ts (aliases — implemented).
Outbound routable id pick: documented in LESSONS as fixed; symbols not in tree — see 10-code-lies.
Recovery constants (cheat sheet)
| Constant | Value |
|---|---|
| WS reconnect backoff | 1s → 30s |
| Connect timeout | 5s |
| Keepalive interval / ack timeout | 25s / 10s |
| Health probe | 5s |
| Peer disconnect grace | 10s |
| Peer reconnect | 5s × 12 then silent abandon |
| DC recovery grace (closing) | 2.5s |
| Non-initiator give-up | 5s |
| Offer-sent grace | 20s |
| Inventory / full sync limit | 20_000 |
| Sync batch | 200 |
| Sync poll | 10s fast / 15 min slow |
Source: realtime.constants.ts, message-sync.rules.ts.
Server responsibilities (only when needed)
Default agent scope excludes server/ unless packet expands. Know this:
- Serializes WS handlers per connection (identify-before-join).
- Relays RTC only when peers share membership (DM types exempt).
- No message persistence.
voiceActiveroutes offers to owning connection.
Canonical envelopes: agents-docs/features/signaling.md.