package syncapi import ( "fmt" "strconv" "strings" "github.com/dariuszjaniak/warp/server/internal/storage" ) // Token is a position in every stream a sync response carries. // // A sync response is assembled from streams that move independently: room // events, read receipts, to-device messages, account data and presence. A // client that has seen receipt 7 but only event 4 has to be able to say so // in the one opaque string the API gives it, so the token names a position // in each. // // The wire form is // // s{pdu}_{receipt}_{to_device}_{account_data}_{presence} // // with trailing zeroes left off, so a client that has only ever seen room // events carries "s12" rather than "s12_0_0_0_0". The leading "s" is there // to make a token recognisable in a log and to leave room for a second // token shape later — Simplified Sliding Sync (PLAN.md phase 10) has its // own — without any ambiguity about which is which. // // The spec says only that the token is opaque, and clients treat it that // way. It is still worth being readable: a token is the first thing in // every bug report about /sync. type Token struct { PDU int64 Receipt int64 ToDevice int64 AccountData int64 Presence int64 } // tokenPrefix marks a classic /sync token. const tokenPrefix = "s" // IsInitial reports whether the token is the start of every stream, which // is what a sync with no ?since= means. func (t Token) IsInitial() bool { return t == Token{} } // String renders the wire form. func (t Token) String() string { parts := []int64{t.PDU, t.Receipt, t.ToDevice, t.AccountData, t.Presence} // Trailing zeroes carry no information, and leaving them off keeps the // common token short. last := 0 for i, p := range parts { if p != 0 { last = i } } var b strings.Builder b.WriteString(tokenPrefix) for i, p := range parts[:last+1] { if i > 0 { b.WriteByte('_') } b.WriteString(strconv.FormatInt(p, 10)) } return b.String() } // ParseToken reads a token a client sent back. // // An empty string is the initial token: /sync without ?since= and // /messages without ?from= both mean "from the beginning", and refusing // the empty string would make every caller special-case it. // // A token with fewer fields than this build writes is accepted and the // missing ones are zero. That is what lets a token stored by a client // before a stream existed go on working: the stream it did not know about // reads as "seen nothing", so the client is sent that stream in full once // and is in step from then on. func ParseToken(raw string) (Token, error) { if raw == "" { return Token{}, nil } body, ok := strings.CutPrefix(raw, tokenPrefix) if !ok { return Token{}, fmt.Errorf("%q is not a sync token: it must start with %q", raw, tokenPrefix) } fields := strings.Split(body, "_") if len(fields) > 5 { return Token{}, fmt.Errorf("%q is not a sync token: it has %d positions, this server writes at most 5", raw, len(fields)) } var positions [5]int64 for i, field := range fields { value, err := strconv.ParseInt(field, 10, 64) if err != nil || value < 0 { return Token{}, fmt.Errorf("%q is not a sync token: position %d is %q", raw, i+1, field) } positions[i] = value } return Token{ PDU: positions[0], Receipt: positions[1], ToDevice: positions[2], AccountData: positions[3], Presence: positions[4], }, nil } // tokenAt is the token naming the top of every stream, which is what a // sync response hands back when it has drained all of them. func tokenAt(positions storage.StreamPositions) Token { return Token{ PDU: positions.PDU, Receipt: positions.Receipt, ToDevice: positions.ToDevice, AccountData: positions.AccountData, Presence: positions.Presence, } } // pduToken is the token for a position in the room event stream alone. // // It is what /messages and a room's prev_batch hand out: both are about // room events only, and a token from either has to be usable as a ?since= // for /sync, where the other streams then read as unseen. That is the // conservative direction — the client is sent those streams again rather // than having them skipped. func pduToken(pos int64) Token { return Token{PDU: pos} }