package applog import ( "encoding/base64" "encoding/json" "time" ) // Cursor names a position in the merged timeline — the last record a reader // has seen, by exactly the (TS, Box, Seq) triple MergeRecords orders by. // Opaque base64 on the wire so its shape is the server's to evolve. type Cursor struct { TS time.Time `json:"ts"` Box string `json:"box"` Seq uint64 `json:"seq"` } // Before reports whether c sits strictly before r in merge order — the // follow test: records with c.Before(r) are new since the cursor. func (c Cursor) Before(r Record) bool { if !c.TS.Equal(r.TS) { return c.TS.Before(r.TS) } if c.Box != r.Box { return c.Box < r.Box } return c.Seq < r.Seq } // EncodeCursor renders the position just after r. func EncodeCursor(r Record) string { b, _ := json.Marshal(Cursor{TS: r.TS, Box: r.Box, Seq: r.Seq}) // cannot fail on this shape return base64.RawURLEncoding.EncodeToString(b) } // DecodeCursor parses what EncodeCursor produced; ok=false for anything // else (including future shapes this build cannot read — the caller treats // that as "no cursor", which re-tails rather than erroring a follow). func DecodeCursor(s string) (Cursor, bool) { b, err := base64.RawURLEncoding.DecodeString(s) if err != nil { return Cursor{}, false } var c Cursor if json.Unmarshal(b, &c) != nil || c.TS.IsZero() { return Cursor{}, false } return c, true }