Download this file
| 1 | package applog |
| 2 | |
| 3 | import ( |
| 4 | "encoding/base64" |
| 5 | "encoding/json" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | // Cursor names a position in the merged timeline — the last record a reader |
| 10 | // has seen, by exactly the (TS, Box, Seq) triple MergeRecords orders by. |
| 11 | // Opaque base64 on the wire so its shape is the server's to evolve. |
| 12 | type Cursor struct { |
| 13 | TS time.Time `json:"ts"` |
| 14 | Box string `json:"box"` |
| 15 | Seq uint64 `json:"seq"` |
| 16 | } |
| 17 | |
| 18 | // Before reports whether c sits strictly before r in merge order — the |
| 19 | // follow test: records with c.Before(r) are new since the cursor. |
| 20 | func (c Cursor) Before(r Record) bool { |
| 21 | if !c.TS.Equal(r.TS) { |
| 22 | return c.TS.Before(r.TS) |
| 23 | } |
| 24 | if c.Box != r.Box { |
| 25 | return c.Box < r.Box |
| 26 | } |
| 27 | return c.Seq < r.Seq |
| 28 | } |
| 29 | |
| 30 | // EncodeCursor renders the position just after r. |
| 31 | func EncodeCursor(r Record) string { |
| 32 | b, _ := json.Marshal(Cursor{TS: r.TS, Box: r.Box, Seq: r.Seq}) // cannot fail on this shape |
| 33 | return base64.RawURLEncoding.EncodeToString(b) |
| 34 | } |
| 35 | |
| 36 | // DecodeCursor parses what EncodeCursor produced; ok=false for anything |
| 37 | // else (including future shapes this build cannot read — the caller treats |
| 38 | // that as "no cursor", which re-tails rather than erroring a follow). |
| 39 | func DecodeCursor(s string) (Cursor, bool) { |
| 40 | b, err := base64.RawURLEncoding.DecodeString(s) |
| 41 | if err != nil { |
| 42 | return Cursor{}, false |
| 43 | } |
| 44 | var c Cursor |
| 45 | if json.Unmarshal(b, &c) != nil || c.TS.IsZero() { |
| 46 | return Cursor{}, false |
| 47 | } |
| 48 | return c, true |
| 49 | } |
| 50 | |