106 lines
2.9 KiB
Go
106 lines
2.9 KiB
Go
package redisstate
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
// StreamOffset stores the persisted progress of the plain-XREAD intent
|
|
// consumer.
|
|
type StreamOffset struct {
|
|
// Stream stores the Redis Stream name.
|
|
Stream string
|
|
|
|
// LastProcessedEntryID stores the last durably processed Redis Stream
|
|
// entry identifier.
|
|
LastProcessedEntryID string
|
|
|
|
// UpdatedAt stores when the offset record was last updated.
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Validate reports whether offset contains a complete persisted consumer
|
|
// progress record.
|
|
func (offset StreamOffset) Validate() error {
|
|
if offset.Stream == "" {
|
|
return fmt.Errorf("stream offset stream must not be empty")
|
|
}
|
|
if offset.LastProcessedEntryID == "" {
|
|
return fmt.Errorf("stream offset last processed entry id must not be empty")
|
|
}
|
|
if offset.UpdatedAt.IsZero() {
|
|
return fmt.Errorf("stream offset updated at must not be zero")
|
|
}
|
|
if !offset.UpdatedAt.Equal(offset.UpdatedAt.UTC()) {
|
|
return fmt.Errorf("stream offset updated at must be UTC")
|
|
}
|
|
if !offset.UpdatedAt.Equal(offset.UpdatedAt.Truncate(time.Millisecond)) {
|
|
return fmt.Errorf("stream offset updated at must use millisecond precision")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type streamOffsetJSON struct {
|
|
Stream string `json:"stream"`
|
|
LastProcessedEntryID string `json:"last_processed_entry_id"`
|
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
|
}
|
|
|
|
// MarshalStreamOffset marshals one stream-offset record into the strict JSON
|
|
// representation owned by Notification Service.
|
|
func MarshalStreamOffset(offset StreamOffset) ([]byte, error) {
|
|
if err := offset.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal stream offset: %w", err)
|
|
}
|
|
|
|
return marshalStrictJSON(streamOffsetJSON{
|
|
Stream: offset.Stream,
|
|
LastProcessedEntryID: offset.LastProcessedEntryID,
|
|
UpdatedAtMS: offset.UpdatedAt.UTC().UnixMilli(),
|
|
})
|
|
}
|
|
|
|
// UnmarshalStreamOffset unmarshals one strict JSON stream-offset record.
|
|
func UnmarshalStreamOffset(payload []byte) (StreamOffset, error) {
|
|
var wire streamOffsetJSON
|
|
if err := unmarshalStrictJSON(payload, &wire); err != nil {
|
|
return StreamOffset{}, fmt.Errorf("unmarshal stream offset: %w", err)
|
|
}
|
|
|
|
offset := StreamOffset{
|
|
Stream: wire.Stream,
|
|
LastProcessedEntryID: wire.LastProcessedEntryID,
|
|
UpdatedAt: time.UnixMilli(wire.UpdatedAtMS).UTC(),
|
|
}
|
|
if err := offset.Validate(); err != nil {
|
|
return StreamOffset{}, fmt.Errorf("unmarshal stream offset: %w", err)
|
|
}
|
|
|
|
return offset, nil
|
|
}
|
|
|
|
func marshalStrictJSON(value any) ([]byte, error) {
|
|
return json.Marshal(value)
|
|
}
|
|
|
|
func unmarshalStrictJSON(payload []byte, target any) error {
|
|
decoder := json.NewDecoder(bytes.NewBuffer(payload))
|
|
decoder.DisallowUnknownFields()
|
|
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return fmt.Errorf("unexpected trailing JSON input")
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|