libchat/docs/target-architecture.md
osmaczko c59f608ee6
docs: add target architecture reference
Design and review discussions have surfaced diverging views of how the
chat stack is layered. Add a normative reference for the target
architecture so those discussions share one picture.

- Defines the layers — Conversation Core, Client (Generic + Platform),
  Application — with a responsibilities table and call/event flow.
- States the governing principles: caller-driven sync core, sync results
  vs async observations, a data-not-behavioural boundary, and
  easy-edge/powerful-core.
- Documents runtime independence (Generic Client optional; runtime choice
  at the Platform Client) and the Services/ConversationTypes axis.
- Aligns terminology with the chat specifications.
2026-06-09 08:41:05 +02:00

206 lines
12 KiB
Markdown

# Target Architecture
> **Status:** Proposed — for alignment · **Last revised:** 2026-06-08
This document describes the **target architecture** for the chat stack: the role each
layer plays and the boundaries between them. It is a normative reference for design
discussions and reviews — a shared picture of the shape we are building toward, not a
description of any current implementation.
Terminology follows the [chat specifications](https://github.com/logos-messaging/specs)
(`chatdefs`, `chat-framework`, `privatev1`).
## Vocabulary
| Term | Meaning |
|---|---|
| **Content** | Opaque application bytes whose meaning is defined solely by the Application (which may structure them with an envelope such as a ContentFrame). The Core never inspects Content. |
| **Frame** | A structured protocol message exchanged between Clients. |
| **Payload** | The serialized binary form of a Frame; opaque to the transport. |
| **Conversation** | One instance of a chat protocol between participants; owns protocol operations (encryption, reliability, segmentation). |
| **ConversationType** | A versioned protocol implementation (`PrivateV1`, `GroupV1`, …). Self-contained: defines its own Frame types and storage needs. |
| **Delivery Service (DS)** | The transport boundary, both directions: accepts outbound Payloads by delivery address and delivers inbound Payloads to the subscribers of an address. |
| **Service** | An external effect the Core needs but does not implement — delivery, persistence, registration/identity. The Core defines it as a trait; an outer layer supplies the implementation. |
| **Event** | An asynchronous notification of something the Application could not learn as a return value: an inbound observation (message received, conversation started) or a deferred outcome (delivery acknowledged or timed out).
| **Client** | The component that manages Conversations and exposes messaging to Applications. |
| **Application** | Software that integrates a Client to send and receive Content. |
## Layers
Four responsibilities stack from the protocol engine up to the product. Dependencies
point downward: each layer knows only about the layer beneath it.
```mermaid
flowchart TB
App["<b>Application</b><br/>logos-chat-module + logos-chat-ui"]
subgraph Client["Client"]
direction TB
PC["<b>Platform Client</b><br/>concrete Services + ergonomic API"]
GC["<b>Generic Client</b> <i>(optional)</i><br/>worker thread + Event queue"]
end
Core["<b>Conversation Core</b><br/>synchronous protocol engine<br/>hosts ConversationTypes · defines Service traits"]
App -->|calls| PC
PC -->|forwards| GC
GC -->|locks + calls| Core
Core -->|"outcome"| GC
GC -. events .-> PC
PC -. events .-> App
style GC stroke-dasharray: 5 5
```
Solid arrows are synchronous — a call going down and its result coming back on the same
call; the Conversation Core hands its outcome straight back to its caller.
Dotted arrows carry Events asynchronously: the Generic Client fills a queue, which the
Platform Client adapts into the surface the Application consumes. The Application depends
only on the Platform Client, in both directions — it calls the Platform Client and consumes
the Event surface the Platform Client exposes. The **Generic Client** and **Platform
Client** are the two roles of the single *Client* the specs name, shown as the outer box —
a reusable runtime mechanism, and a concrete binding over it. The Generic Client is
**optional** (dashed border): a platform without OS threads omits it, and the Platform
Client drives the Core directly — see [Runtime independence](#runtime-independence).
### Responsibilities
| Layer | Role & owns | Execution model | Must not | Example | Repo |
|---|---|---|---|---|---|
| **Conversation Core** | The protocol engine. Hosts pluggable ConversationTypes, defines the Service trait contracts, routes an inbound Payload to the right Conversation, returns a structured outcome. | Strictly synchronous and caller-driven; runs on the caller's thread. | Spawn threads · do background work · call out via callbacks · depend on a runtime or transport. | `Core` + `PrivateV1` / `GroupV1` | `libchat` |
| **Generic Client** *(optional)* | The native runtime the Core lacks, **batteries included**: an OS worker that drives inbound processing, the Event queue, lifecycle/shutdown — **plus portable default Service implementations** (in-memory, SQLite) so a native app works out of the box. Parameterized over the Service traits. | Owns the worker thread and the Event queue (a buffer it fills). Outbound calls run on the caller's thread. | Call outward into its consumer · depend on an async runtime. | `ChatClient<D, R>` | `libchat` |
| **Platform Client** | The complete app-facing surface for one platform: selects which ConversationTypes to support, wires the platform-specific Services, exposes the easy API, and adapts the Generic Client's Event queue into its app-facing surface (e.g. an async API on Tokio, signals on Qt). | A facade over the Generic Client. It may adopt a platform runtime (e.g. Tokio) to shape its API and bridge the Event queue into it. Where there are no OS threads, it also takes on the runtime role and drives the Core on the host event loop. | Be generic · leak Service type parameters to the Application. | `LogosChatClient` | `logos-chat-module` |
| **Application** | Integrates the Platform Client; drives outbound on user action and renders/acts on the Events it receives. | Owns its own main / UI / async runtime; consumes Events on its own schedule, through the surface the Platform Client exposes. | Touch protocol internals · assume which path or thread produced an Event. | UI views + app glue | `logos-chat-module` + `logos-chat-ui` |
## Governing principles
1. **Simplicity of the core.** The Conversation Core is fully synchronous and
caller-driven: no background work, no callbacks out. External effects flow through
Services injected as parameters. This keeps the protocol code small and highly
reviewable — the basis for trust, audit, and contribution.
2. **Synchronous results, asynchronous observations.** The result of an action the
Application takes comes back synchronously, from the call that started it. Everything
the Application could not learn that way arrives as an Event — inbound observations (a
message arrived, a peer started a conversation) and outcomes that resolve only later
(such as a delivery confirmation). These share one asynchronous surface, so the
Application observes in a single place.
3. **A data boundary, not a behavioral one.** Events cross the Client's boundary as owned,
concrete values that the consumer *pulls*; the Client never calls outward. Concretely,
the Generic Client fills a queue that its consumer drains. Passing data rather than
behavior is what keeps the boundary FFI-safe — no closures, generics, or non-`'static`
references have to cross — and what lets the consumer choose its own threading and
runtime. Async or callback surfaces are conveniences the Platform Client builds over the
pulled data, never something the Client requires.
4. **Easy at the edge, powerful at the core.** Generality lives in the Generic Client (a
mechanism); ease of use lives in the Platform Client (a curated binding). The two
goals don't compete because they live in different layers.
## Runtime independence
Because the Core is synchronous and caller-driven (principle 1) and the boundary is
pull-based data (principle 3), the design commits to a runtime at neither end:
- **The Generic Client is optional.** It is just the native realization of the runtime
role, so a platform can skip it and drive the Core itself — for example one without OS
threads, which has no use for a worker thread.
- **The consumer picks the runtime, at the Platform Client.** Because Events are pulled
data rather than pushed callbacks, the Platform Client can adopt whatever runtime its
ecosystem expects and bridge the queue into it (e.g. an async API on Tokio) — without the
Core or Generic Client committing to one.
## Services and ConversationTypes
The Core **defines** the Service contracts and **hosts** the ConversationTypes; outer
layers **provide** the implementations and **choose** the types. This is the extensibility
axis — orthogonal to the runtime layering above.
```mermaid
flowchart LR
subgraph Core["Conversation Core"]
direction TB
Traits["Service traits<br/>Delivery · Persistence · Registration"]
subgraph Types["ConversationTypes (versioned, self-contained)"]
direction TB
P1["PrivateV1"]
G1["GroupV1"]
Vn["…Vn"]
end
end
subgraph GC["<b>Generic Client</b> <i>(optional)</i>"]
GC_P["SQLite"]
end
subgraph LogosChat["Platform Client — LogosChat"]
direction TB
LC_D["LogosDelivery<br/>(through LogosCore)"]
LC_A["λAccount"]
end
LogosChat -- implements --> Traits
LogosChat -- wraps --> GC
LogosChat -- selects --> Types
GC -- implements --> Traits
style GC stroke-dasharray: 5 5
```
- **ConversationTypes are self-contained.** Each owns its Frame types and its storage
requirements; adding one does not touch the others.
- **The Core routes each inbound Payload to the right ConversationType** (the framing
strategy), so types coexist without knowing about one another.
- **The Delivery Service is the whole transport boundary.** The Client publishes outbound
Payloads through it and receives inbound Payloads from it (delivered to its subscribed
addresses). There is one transport abstraction, not separate send and receive ones; the
FFI encoding of it (a push function plus a poll function) is an implementation detail of
that single Service.
- **Services are a palette.** Portable implementations (in-memory, SQLite) ship as
defaults at the Generic Client; platform-specific ones (network delivery, platform
identity) are supplied by the Platform Client. All are just implementations of
Core-defined traits, so a Platform overrides only what it must.
## Message flow
Outbound is synchronous; inbound is asynchronous. The asymmetry is deliberate: a send is
caller-driven and returns a result, while inbound Payloads arrive whenever the transport
receives them and surface later as Events.
```mermaid
sequenceDiagram
participant App as Application
participant Client
participant Core as Conversation Core
participant DS as Delivery Service
Note over App,DS: Outbound — synchronous, on the caller's thread
App->>Client: send(convo, content)
Client->>Core: send_content(...)
Core->>DS: publish(payload)
DS-->>Core: Ok / Err
Core-->>Client: Ok / Err
Client-->>App: Ok / Err
Note over App,DS: Inbound — asynchronous, on the worker thread
DS-)Client: inbound Payload (subscribed address)
Client->>Core: handle_payload(bytes)
Core-->>Client: outcome
Client->>Client: translate outcome → Event(s)
Client-)App: Event(s)
App->>App: consume on its own schedule
```
Synchronous failures (publish, parse, store, crypto) return on the triggering call as a
`Result`. Only what has no synchronous answer — an inbound message, a peer-started
conversation, a deferred delivery acknowledgement or timeout — becomes an Event.
## References
- [`informational/chatdefs.md`](https://github.com/logos-messaging/specs/blob/master/informational/chatdefs.md) — terminology
- [`standards/application/chat-framework.md`](https://github.com/logos-messaging/specs/blob/master/standards/application/chat-framework.md) — framework phases and components
- [`standards/application/privatev1.md`](https://github.com/logos-messaging/specs/blob/master/standards/application/privatev1.md) — a ConversationType in detail
- [`standards/application/contentframe.md`](https://github.com/logos-messaging/specs/blob/master/standards/application/contentframe.md) — the application-layer content envelope (Content typing; opaque to the Core)
- [`docs/adr/0001-client-event-system.md`](./adr/0001-client-event-system.md) — the Event/runtime decision in depth