You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

100 lines
2.8 KiB

  1. // Package cursor implements time-ordered item cursors for an event log.
  2. package cursor
  3. import (
  4. "errors"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. // A Source produces cursors based on a time index generator and a sequence
  11. // counter. A zero-valued Source is ready for use with defaults as described.
  12. type Source struct {
  13. // This function is called to produce the current time index.
  14. // If nil, it defaults to time.Now().UnixNano().
  15. TimeIndex func() int64
  16. // The current counter value used for sequence number generation. It is
  17. // incremented in-place each time a cursor is generated.
  18. Counter int64
  19. }
  20. func (s *Source) timeIndex() int64 {
  21. if s.TimeIndex == nil {
  22. return time.Now().UnixNano()
  23. }
  24. return s.TimeIndex()
  25. }
  26. func (s *Source) nextCounter() int64 {
  27. s.Counter++
  28. return s.Counter
  29. }
  30. // Cursor produces a fresh cursor from s at the current time index and counter.
  31. func (s *Source) Cursor() Cursor {
  32. return Cursor{
  33. timestamp: uint64(s.timeIndex()),
  34. sequence: uint16(s.nextCounter() & 0xffff),
  35. }
  36. }
  37. // A Cursor is a unique identifier for an item in a time-ordered event log.
  38. // It is safe to copy and compare cursors by value.
  39. type Cursor struct {
  40. timestamp uint64 // ns since Unix epoch
  41. sequence uint16 // sequence number
  42. }
  43. // Before reports whether c is prior to o in time ordering. This comparison
  44. // ignores sequence numbers.
  45. func (c Cursor) Before(o Cursor) bool { return c.timestamp < o.timestamp }
  46. // Diff returns the time duration between c and o. The duration is negative if
  47. // c is before o in time order.
  48. func (c Cursor) Diff(o Cursor) time.Duration {
  49. return time.Duration(c.timestamp) - time.Duration(o.timestamp)
  50. }
  51. // IsZero reports whether c is the zero cursor.
  52. func (c Cursor) IsZero() bool { return c == Cursor{} }
  53. // MarshalText implements the encoding.TextMarshaler interface.
  54. // A zero cursor marshals as "", otherwise the format used by the String method.
  55. func (c Cursor) MarshalText() ([]byte, error) {
  56. if c.IsZero() {
  57. return nil, nil
  58. }
  59. return []byte(c.String()), nil
  60. }
  61. // UnmarshalText implements the encoding.TextUnmarshaler interface.
  62. // An empty text unmarshals without error to a zero cursor.
  63. func (c *Cursor) UnmarshalText(data []byte) error {
  64. if len(data) == 0 {
  65. *c = Cursor{} // set zero
  66. return nil
  67. }
  68. ps := strings.SplitN(string(data), "-", 2)
  69. if len(ps) != 2 {
  70. return errors.New("invalid cursor format")
  71. }
  72. ts, err := strconv.ParseUint(ps[0], 16, 64)
  73. if err != nil {
  74. return fmt.Errorf("invalid timestamp: %w", err)
  75. }
  76. sn, err := strconv.ParseUint(ps[1], 16, 16)
  77. if err != nil {
  78. return fmt.Errorf("invalid sequence: %w", err)
  79. }
  80. c.timestamp = ts
  81. c.sequence = uint16(sn)
  82. return nil
  83. }
  84. // String returns a printable text representation of a cursor.
  85. func (c Cursor) String() string {
  86. return fmt.Sprintf("%016x-%04x", c.timestamp, c.sequence)
  87. }