- `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.
155 lines
4.9 KiB
Markdown
155 lines
4.9 KiB
Markdown
# 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` → many `SignalingManager` (one WS per URL)
|
||
- `PeerConnectionManager` → negotiation, DC, recovery
|
||
- `MediaManager` / noise / screen share
|
||
- `WebRtcStateController` (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.
|
||
- `voiceActive` routes offers to owning connection.
|
||
|
||
Canonical envelopes: `agents-docs/features/signaling.md`.
|