// Package cursor implements time-ordered item cursors for an event log. package cursor import ( "errors" "fmt" "strconv" "strings" "time" ) // A Source produces cursors based on a time index generator and a sequence // counter. A zero-valued Source is ready for use with defaults as described. type Source struct { // This function is called to produce the current time index. // If nil, it defaults to time.Now().UnixNano(). TimeIndex func() int64 // The current counter value used for sequence number generation. It is // incremented in-place each time a cursor is generated. Counter int64 } func (s *Source) timeIndex() int64 { if s.TimeIndex == nil { return time.Now().UnixNano() } return s.TimeIndex() } func (s *Source) nextCounter() int64 { s.Counter++ return s.Counter } // Cursor produces a fresh cursor from s at the current time index and counter. func (s *Source) Cursor() Cursor { return Cursor{ timestamp: uint64(s.timeIndex()), sequence: uint16(s.nextCounter() & 0xffff), } } // A Cursor is a unique identifier for an item in a time-ordered event log. // It is safe to copy and compare cursors by value. type Cursor struct { timestamp uint64 // ns since Unix epoch sequence uint16 // sequence number } // Before reports whether c is prior to o in time ordering. This comparison // ignores sequence numbers. func (c Cursor) Before(o Cursor) bool { return c.timestamp < o.timestamp } // Diff returns the time duration between c and o. The duration is negative if // c is before o in time order. func (c Cursor) Diff(o Cursor) time.Duration { return time.Duration(c.timestamp) - time.Duration(o.timestamp) } // IsZero reports whether c is the zero cursor. func (c Cursor) IsZero() bool { return c == Cursor{} } // MarshalText implements the encoding.TextMarshaler interface. // A zero cursor marshals as "", otherwise the format used by the String method. func (c Cursor) MarshalText() ([]byte, error) { if c.IsZero() { return nil, nil } return []byte(c.String()), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface. // An empty text unmarshals without error to a zero cursor. func (c *Cursor) UnmarshalText(data []byte) error { if len(data) == 0 { *c = Cursor{} // set zero return nil } ps := strings.SplitN(string(data), "-", 2) if len(ps) != 2 { return errors.New("invalid cursor format") } ts, err := strconv.ParseUint(ps[0], 16, 64) if err != nil { return fmt.Errorf("invalid timestamp: %w", err) } sn, err := strconv.ParseUint(ps[1], 16, 16) if err != nil { return fmt.Errorf("invalid sequence: %w", err) } c.timestamp = ts c.sequence = uint16(sn) return nil } // String returns a printable text representation of a cursor. func (c Cursor) String() string { return fmt.Sprintf("%016x-%04x", c.timestamp, c.sequence) }