CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Implements PLAN.md Phase 10. Simplified Sliding Sync (MSC4186) is served at the unstable org.matrix.simplified_msc3575 endpoint and advertised in /versions. Lists are sorted by activity and filtered by DM, invite, encryption, room type, space and tag; room subscriptions, required_state with wildcards, $ME and $LAZY, and the to_device, e2ee, account_data, receipts and typing extensions are supported. Per-connection state is kept in memory and an unknown pos answers M_UNKNOWN_POS. Threads and relations: m.relates_to is indexed on roomserver_events (migration 0012), /relations (with recursion) and /threads are served, and every event a timeline read returns carries its m.thread, m.replace and m.reference summaries in unsigned.m.relations. Notifications carry a thread ID (migration 0013): threaded receipts clear only their thread, and sync reports unread_thread_notifications when the filter asks. Spaces: /hierarchy walks a space breadth first, asking other servers about rooms this one is not in through the new federation hierarchy endpoint; room_summary describes one room. /search runs on FTS5 (sqlite, pocketbase) or tsvector (postgres), bounded to what each user may see, with context, profiles, state, grouping and paging. The PocketBase driver is real: the sqlite schema and store on an embedded PocketBase app's database, with media in its filesystem, local or S3 (database.pocketbase.s3). It runs the full storage test suite. Ops: OpenTelemetry tracing over OTLP/HTTP (tracing.*), with server spans per route, component spans, and trace propagation on federation, push and application service calls. New metrics for persisted events, waiting syncs, sliding sync connections and the database pool, with a Grafana dashboard. deploy/ adds a Compose setup (PostgreSQL, Prometheus, Grafana, Jaeger) and a Helm chart; cmd/warp-loadtest loads a server and reports request and delivery latencies. The load tester found a long-standing bug, fixed here: on a server where no account data had been written, every incremental /sync re-sent the push rules and returned at once, and an all-zero token was taken for an initial sync, so long polls became busy loops. The PostgreSQL migrations and search dialect compile but were not run against a live PostgreSQL in this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
4.8 KiB
Markdown
98 lines
4.8 KiB
Markdown
# Relations and threads
|
|
|
|
An event can point at another through `content.m.relates_to`: a thread
|
|
reply (`m.thread`), an edit (`m.replace`), a reaction (`m.annotation`), a
|
|
reference (`m.reference`). None of that is checked by the authorisation
|
|
rules. A relation is a client convention, not part of the room's DAG, and a
|
|
server can accept an event without understanding what it relates to. What
|
|
the server adds is three things:
|
|
|
|
1. **The index.** Every event's `m.relates_to` is written out with it, so
|
|
that "the children of this event" is an index lookup.
|
|
2. **The listings.** `/relations` lists an event's children and `/threads`
|
|
lists a room's threads.
|
|
3. **The summaries.** Wherever an event is served, its children are
|
|
summarised into `unsigned.m.relations`, so a client drawing a timeline
|
|
knows a message has a thread under it, or has been edited, without
|
|
asking about every event.
|
|
|
|
The code is in `internal/roomserver/relations.go` (the reads and the
|
|
summaries) and `internal/syncapi/relations.go` (the tokens), over the
|
|
relation reads of the room store in `internal/storage/relations.go`.
|
|
|
|
## The index
|
|
|
|
`roomserver_events` has two more columns, `relates_to_id` and `rel_type`,
|
|
taken from `content.m.relates_to.event_id` and `.rel_type` when both are
|
|
strings. Migration 0012 fills them in for the events already stored. An
|
|
`m.in_reply_to` on its own is not a relation in this sense: a reply points
|
|
at an event without being one of its children.
|
|
|
|
A redaction clears both columns. The redaction algorithm strips the
|
|
content they came from, so a redacted thread reply leaves its thread, and
|
|
a redacted edit stops being one.
|
|
|
|
## `GET /_matrix/client/v1/rooms/{roomID}/relations/{eventID}[/{relType}[/{eventType}]]`
|
|
|
|
The children of one event, newest first by default.
|
|
|
|
| Parameter | |
|
|
|---|---|
|
|
| `relType`, `eventType` | Narrow by relation type, then by event type. |
|
|
| `dir` | `b` (default) or `f`. |
|
|
| `from`, `to` | Pagination tokens. They are `/messages` tokens: a sync's `prev_batch` works as a `from`. |
|
|
| `limit` | 50 by default, 1000 at most. |
|
|
| `recurse` | Include the children's children, down to three levels. The response then carries `recursion_depth`: how deep it found anything. The filters apply at every level. |
|
|
|
|
The parent has to be visible to the user, and the children are bounded to
|
|
what they may see, by the same visible range as a timeline read
|
|
([rooms.md](rooms.md)). A parent the user may not see is `404`, not `403`:
|
|
whether a particular event exists in a room is itself not theirs to know.
|
|
|
|
## `GET /_matrix/client/v1/rooms/{roomID}/threads`
|
|
|
|
The room's threads, the most recently active first. A thread is a root
|
|
that has at least one `m.thread` child; its activity is its newest child.
|
|
`include=participated` keeps the threads the user started or replied in.
|
|
Each root carries its `m.thread` summary. `from` takes the `next_batch` of
|
|
the previous page, a token of its own (`t<position>`), because threads are
|
|
ordered by activity rather than by any one stream.
|
|
|
|
A thread whose root this server does not hold — sent before this server
|
|
joined the room and never backfilled — is not listed: there is no event to
|
|
show it under.
|
|
|
|
## Unread counts and receipts
|
|
|
|
Each notification is filed under its thread, a threaded read receipt reads
|
|
only that thread (`thread_id: "main"` only the main timeline), and a sync
|
|
whose filter sets `unread_thread_notifications` gets the counts per thread.
|
|
How, and what stays a room total, is in [push.md](push.md).
|
|
|
|
## Bundled aggregations
|
|
|
|
Every event a timeline read returns — `/sync`, sliding sync, `/messages`,
|
|
`/context`, `GET /event`, `/relations` and `/threads` — gets
|
|
`unsigned.m.relations` when it has children:
|
|
|
|
| Key | What |
|
|
|---|---|
|
|
| `m.thread` | `latest_event` (the newest reply, in full), `count`, and `current_user_participated` — true for a user who sent the root or any reply. |
|
|
| `m.replace` | The newest edit, in full. Only an edit from the original's sender counts, and only one of the same event type; a state event cannot be edited, and a redacted event's edits are not shown. |
|
|
| `m.reference` | `chunk`: the IDs of the events referencing it, oldest first, at most 50. |
|
|
|
|
Annotations (reactions) are not summarised. The spec dropped their
|
|
aggregation, and every client counts reactions from the events themselves.
|
|
|
|
The summaries are computed when the event is read, by one grouped query
|
|
over the batch of events being returned, bounded to what the user may
|
|
see: a user who has left the room sees a thread's count and latest reply
|
|
as of when they left.
|
|
|
|
## What is deliberately missing
|
|
|
|
| Thing | Why |
|
|
|---|---|
|
|
| Rejecting a thread whose root is itself a relation | The spec says a server *should*; Warp accepts it, and clients render it as an ordinary reply. |
|
|
| The relation filters of a room event filter (`related_by_rel_types`, `related_by_senders`) | Ignored, like every filter field Warp does not act on; the client gets a superset. |
|