hush/vendor/github.com/redis/go-redis/v9/stream_commands.go
jx12n 4d9a26498e hush: one-time secret links the server cannot read
Paste a secret, get a link, send it. The first person to open it and press
Reveal sees the secret; the link dies at that moment. The recipient needs a
browser and nothing else — no account, no client, no installed tooling.

The server cannot read what it stores. AES-256-GCM happens in the browser and
the key lives in the URL fragment, which browsers never transmit, so hushd
holds ciphertext and no key material. That is a property of where the key sits
rather than a promise about our conduct, which is why there is deliberately no
endpoint accepting a plaintext secret and no server-side-encryption fallback:
two guarantees behind one URL would be worse than one honest guarantee.

Three decisions carry the design:

  * GET /s/{id} touches NO storage, not even to check existence. Slack, Teams,
    WhatsApp, iMessage and Outlook Safe Links all fetch a URL before a human
    sees it, so destroying on GET would destroy most secrets in transit and the
    recipient's "already used" would be indistinguishable from interception.
    Only POST /reveal consumes. Bot user-agent detection is an arms race;
    removing the side effect from GET is not. Pinned by
    TestGettingTheRevealPageNeverConsumesTheSecret.
  * Destruction is one Redis GETDEL, which is atomic. GET-then-DEL has a window
    where two simultaneous readers both win, and for a one-time secret that
    window is the product. The store contract demands atomicity and the same
    concurrency test runs against both implementations.
  * Missing, already-revealed, expired and evicted are ONE indistinguishable
    410. Separating them would confirm to a prober that a given link was real.

The secret id IS the capability, so secret.ID is a struct whose every
accidental path — %v, %s, String(), slog, json.Marshal — emits a redacted
handle or refuses, and the raw value needs an explicit Value(). The first
version tried to prevent leaks by implementing no String() at all; its own test
caught that Go's fmt prints unexported fields anyway, so forbidding the method
had removed the control rather than the leak.

Operationally: structured JSON on stdout in the fleet's wire format, which
Vector already collects with no annotation; six hush_* metrics on the chassis
registry with no id, IP or path in any label; five alert rules wired into
vmalert. The public Ingress enumerates /, /s/ and /api/ so /metrics, /healthz
and /readyz share the port but are unreachable from the internet — no
basic-auth middleware to maintain and get wrong.

Dependencies are vendored because go-chassis is private: the Woodpecker test
step and the in-cluster Kaniko build both run -mod=vendor with GOPROXY=off and
hold no git credential.

cmd/hush-mcp is a stdio MCP server doing the same client-side crypto locally,
so using hush from an agent preserves the same guarantee as using it from a
browser.
2026-09-03 00:08:38 -06:00

749 lines
24 KiB
Go

package redis
import (
"context"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9/internal/otel"
)
// XTrimLimitDisabled is a sentinel value for the LIMIT argument of stream
// trimming (XAddArgs.Limit and the XTrim*Approx* commands). Passing it emits
// an explicit "LIMIT 0", which tells Redis to disable the trimming effort cap
// entirely. This differs from passing 0, which keeps the historical behavior:
// no LIMIT clause is sent and Redis applies its implicit default
// (100 * stream-node-max-entries examined entries).
//
// LIMIT is only valid together with the "~" (approximate) trimming flag;
// Redis rejects LIMIT used with exact ("=") trimming.
const XTrimLimitDisabled = -1
// appendXTrimLimit appends the LIMIT clause used by XADD and XTRIM trimming:
// - limit > 0 emits "LIMIT <limit>";
// - limit < 0 (see XTrimLimitDisabled) emits "LIMIT 0", disabling the
// trimming effort cap;
// - limit == 0 omits the clause, so Redis applies its implicit default.
func appendXTrimLimit(args []interface{}, limit int64) []interface{} {
switch {
case limit > 0:
return append(args, "limit", limit)
case limit < 0:
return append(args, "limit", int64(0))
default:
return args
}
}
type StreamCmdable interface {
XAdd(ctx context.Context, a *XAddArgs) *StringCmd
XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd
XDel(ctx context.Context, stream string, ids ...string) *IntCmd
XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd
XLen(ctx context.Context, stream string) *IntCmd
XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd
XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd
XRevRange(ctx context.Context, stream string, start, stop string) *XMessageSliceCmd
XRevRangeN(ctx context.Context, stream string, start, stop string, count int64) *XMessageSliceCmd
XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd
XReadStreams(ctx context.Context, streams ...string) *XStreamSliceCmd
XGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd
XGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd
XGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd
XGroupDestroy(ctx context.Context, stream, group string) *IntCmd
XGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *IntCmd
XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd
XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd
XAck(ctx context.Context, stream, group string, ids ...string) *IntCmd
XNack(ctx context.Context, a *XNackArgs) *IntCmd
XPending(ctx context.Context, stream, group string) *XPendingCmd
XPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd
XClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd
XClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd
XAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimCmd
XAutoClaimWithDeleted(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimWithDeletedCmd
XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd
XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd
XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd
XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd
XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd
XTrimMinID(ctx context.Context, key string, minID string) *IntCmd
XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd
XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd
XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd
XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd
XInfoStream(ctx context.Context, key string) *XInfoStreamCmd
XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd
XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd
XCfgSet(ctx context.Context, a *XCfgSetArgs) *StatusCmd
}
// XAddArgs accepts values in the following formats:
// - XAddArgs.Values = []interface{}{"key1", "value1", "key2", "value2"}
// - XAddArgs.Values = []string("key1", "value1", "key2", "value2")
// - XAddArgs.Values = map[string]interface{}{"key1": "value1", "key2": "value2"}
//
// Note that map will not preserve the order of key-value pairs.
// MaxLen/MaxLenApprox and MinID are in conflict, only one of them can be used.
//
// For idempotent production (at-most-once production):
// - ProducerID: A unique identifier for the producer (required for both IDMP and IDMPAUTO)
// - IdempotentID: A unique identifier for the message (used with IDMP)
// - IdempotentAuto: If true, Redis will auto-generate an idempotent ID based on message content (IDMPAUTO)
//
// ProducerID and IdempotentID are mutually exclusive with IdempotentAuto.
// When using idempotent production, ID must be "*" or empty.
type XAddArgs struct {
Stream string
NoMkStream bool
MaxLen int64 // MAXLEN N
MinID string
// Approx causes MaxLen and MinID to use "~" matcher (instead of "=").
Approx bool
// Limit caps the trimming effort:
// - 0 omits the LIMIT clause (Redis applies its implicit default);
// - a positive value emits "LIMIT <n>";
// - a negative value (see XTrimLimitDisabled) emits "LIMIT 0",
// disabling the effort cap entirely.
// LIMIT requires Approx to be true; Redis rejects LIMIT together with
// exact ("=") trimming.
Limit int64
Mode string
ID string
Values interface{}
ProducerID string // Producer ID for idempotent production (IDMP or IDMPAUTO)
IdempotentID string // Idempotent ID for IDMP
IdempotentAuto bool // Use IDMPAUTO to auto-generate idempotent ID based on content
}
func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd {
args := make([]interface{}, 0, 15)
args = append(args, "xadd", a.Stream)
if a.NoMkStream {
args = append(args, "nomkstream")
}
if a.Mode != "" {
args = append(args, a.Mode)
}
if a.ProducerID != "" {
if a.IdempotentAuto {
// IDMPAUTO pid
args = append(args, "idmpauto", a.ProducerID)
} else if a.IdempotentID != "" {
// IDMP pid iid
args = append(args, "idmp", a.ProducerID, a.IdempotentID)
}
}
switch {
case a.MaxLen > 0:
if a.Approx {
args = append(args, "maxlen", "~", a.MaxLen)
} else {
args = append(args, "maxlen", "=", a.MaxLen)
}
case a.MinID != "":
if a.Approx {
args = append(args, "minid", "~", a.MinID)
} else {
args = append(args, "minid", "=", a.MinID)
}
}
args = appendXTrimLimit(args, a.Limit)
if a.ID != "" {
args = append(args, a.ID)
} else {
args = append(args, "*")
}
args = appendArg(args, a.Values)
cmd := NewStringCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd {
args := []interface{}{"xackdel", stream, group, mode, "ids", len(ids)}
for _, id := range ids {
args = append(args, id)
}
cmd := NewSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd {
args := []interface{}{"xdel", stream}
for _, id := range ids {
args = append(args, id)
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd {
args := []interface{}{"xdelex", stream, mode, "ids", len(ids)}
for _, id := range ids {
args = append(args, id)
}
cmd := NewSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd {
cmd := NewIntCmd(ctx, "xlen", stream)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd {
cmd := NewXMessageSliceCmd(ctx, "xrange", stream, start, stop)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd {
cmd := NewXMessageSliceCmd(ctx, "xrange", stream, start, stop, "count", count)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XRevRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd {
cmd := NewXMessageSliceCmd(ctx, "xrevrange", stream, start, stop)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XRevRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd {
cmd := NewXMessageSliceCmd(ctx, "xrevrange", stream, start, stop, "count", count)
_ = c(ctx, cmd)
return cmd
}
type XReadArgs struct {
Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2
Count int64
MaxCount int64 // cumulative cap on total entries across all streams (Redis >= 8.10)
MaxSize int64 // soft cumulative cap on total reply size in bytes across all streams (Redis >= 8.10)
Block time.Duration
ID string
}
func (c cmdable) XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd {
args := make([]interface{}, 0, 2*len(a.Streams)+10)
args = append(args, "xread")
keyPos := int8(1)
if a.Count > 0 {
args = append(args, "count")
args = append(args, a.Count)
keyPos += 2
}
if a.MaxCount > 0 {
args = append(args, "maxcount", a.MaxCount)
keyPos += 2
}
if a.MaxSize > 0 {
args = append(args, "maxsize", a.MaxSize)
keyPos += 2
}
if a.Block >= 0 {
args = append(args, "block")
args = append(args, int64(a.Block/time.Millisecond))
keyPos += 2
}
args = append(args, "streams")
keyPos++
for _, s := range a.Streams {
args = append(args, s)
}
if a.ID != "" {
for range a.Streams {
args = append(args, a.ID)
}
}
cmd := NewXStreamSliceCmd(ctx, args...)
if a.Block >= 0 {
cmd.setReadTimeout(a.Block)
}
cmd.SetFirstKeyPos(keyPos)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XReadStreams(ctx context.Context, streams ...string) *XStreamSliceCmd {
return c.XRead(ctx, &XReadArgs{
Streams: streams,
Block: -1,
})
}
func (c cmdable) XGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd {
cmd := NewStatusCmd(ctx, "xgroup", "create", stream, group, start)
cmd.SetFirstKeyPos(2)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd {
cmd := NewStatusCmd(ctx, "xgroup", "create", stream, group, start, "mkstream")
cmd.SetFirstKeyPos(2)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd {
cmd := NewStatusCmd(ctx, "xgroup", "setid", stream, group, start)
cmd.SetFirstKeyPos(2)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XGroupDestroy(ctx context.Context, stream, group string) *IntCmd {
cmd := NewIntCmd(ctx, "xgroup", "destroy", stream, group)
cmd.SetFirstKeyPos(2)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *IntCmd {
cmd := NewIntCmd(ctx, "xgroup", "createconsumer", stream, group, consumer)
cmd.SetFirstKeyPos(2)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd {
cmd := NewIntCmd(ctx, "xgroup", "delconsumer", stream, group, consumer)
cmd.SetFirstKeyPos(2)
_ = c(ctx, cmd)
return cmd
}
type XReadGroupArgs struct {
Group string
Consumer string
Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2
Count int64
MaxCount int64 // cumulative cap on total entries across all streams (Redis >= 8.10)
MaxSize int64 // soft cumulative cap on total reply size in bytes across all streams (Redis >= 8.10)
Block time.Duration
NoAck bool
Claim time.Duration // Claim idle pending entries older than this duration
}
func (c cmdable) XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd {
args := make([]interface{}, 0, 14+len(a.Streams))
args = append(args, "xreadgroup", "group", a.Group, a.Consumer)
keyPos := int8(4)
if a.Count > 0 {
args = append(args, "count", a.Count)
keyPos += 2
}
if a.MaxCount > 0 {
args = append(args, "maxcount", a.MaxCount)
keyPos += 2
}
if a.MaxSize > 0 {
args = append(args, "maxsize", a.MaxSize)
keyPos += 2
}
if a.Block >= 0 {
args = append(args, "block", int64(a.Block/time.Millisecond))
keyPos += 2
}
if a.NoAck {
args = append(args, "noack")
keyPos++
}
if a.Claim > 0 {
args = append(args, "claim", int64(a.Claim/time.Millisecond))
keyPos += 2
}
args = append(args, "streams")
keyPos++
for _, s := range a.Streams {
args = append(args, s)
}
cmd := NewXStreamSliceCmd(ctx, args...)
if a.Block >= 0 {
cmd.setReadTimeout(a.Block)
}
cmd.SetFirstKeyPos(keyPos)
_ = c(ctx, cmd)
// Record stream lag for each message (if command succeeded). Gated on the
// result being readable WITHOUT blocking: this command carries a
// read-timeout marker, so on the deferred autopipeline face it is diverted
// and still running when we get here — and the default Block: 0 form can
// wait indefinitely for messages, so reading the outcome would block the
// submit call instead of returning a future (review finding by codex on
// #3942). Skipped for a submission that has not executed yet; emitting
// this from the execution path is a follow-up in the OTel wiring.
if otel.Enabled() && cmd.resultReady() && cmd.rawErr() == nil {
streams := cmd.Val()
for _, stream := range streams {
for _, msg := range stream.Messages {
// Parse message ID to extract timestamp (format: "millisecondsTime-sequenceNumber")
if parts := strings.SplitN(msg.ID, "-", 2); len(parts) == 2 {
if timestampMs, err := strconv.ParseInt(parts[0], 10, 64); err == nil {
// Calculate lag (time since message was created)
messageTime := time.Unix(0, timestampMs*int64(time.Millisecond))
lag := time.Since(messageTime)
// Record lag metric
otel.RecordStreamLag(ctx, lag, nil, stream.Stream, a.Group, a.Consumer)
}
}
}
}
}
return cmd
}
func (c cmdable) XAck(ctx context.Context, stream, group string, ids ...string) *IntCmd {
args := []interface{}{"xack", stream, group}
for _, id := range ids {
args = append(args, id)
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// XNACK modes. See [XNackArgs.Mode].
const (
XNackModeSilent = "SILENT"
XNackModeFail = "FAIL"
XNackModeFatal = "FATAL"
)
// XNackArgs represents the arguments for the XNACK command (Redis >= 8.8).
//
// XNACK negatively acknowledges one or more messages in a consumer group's
// Pending Entries List (PEL), releasing them back to the group so they can be
// redelivered to another consumer via XREADGROUP.
type XNackArgs struct {
Stream string
Group string
// Mode controls how the delivery counter is adjusted for each NACKed entry.
// Must be one of [XNackModeSilent], [XNackModeFail], or [XNackModeFatal]:
// - SILENT: the consumer is shutting down or experiencing internal errors
// unrelated to the message. The delivery counter is decremented by 1,
// undoing the increment that happened when the message was delivered.
// - FAIL: the consumer could not process the message (e.g. insufficient
// memory), but another consumer might succeed. The delivery counter is
// left unchanged.
// - FATAL: the message is invalid or suspected malicious. The delivery
// counter is set to MAXINT, which will immediately move the message to
// the Dead Letter Queue (DLQ) if one is configured for the group.
Mode string
// IDs is the list of message IDs to NACK. All IDs must already be in the
// group's PEL (i.e. previously delivered via XREADGROUP), unless Force is set.
IDs []string
// RetryCount sets the delivery counter to an explicit value, overriding the
// counter adjustment that would otherwise be applied by Mode.
// Leave nil to let Mode control the counter (the common case).
RetryCount *uint64
// Force allows NACKing message IDs that are not yet in the group's PEL,
// creating new unowned NACKed PEL entries for them directly.
// This is analogous to the FORCE flag in XCLAIM.
// Primarily used internally by Redis during AOF rewrite to reconstruct
// NACKed entries, but can also be used to manually inject entries.
Force bool
}
// XNack executes the XNACK command. See [XNackArgs] for the full argument documentation.
// Requires Redis >= 8.8.
func (c cmdable) XNack(ctx context.Context, a *XNackArgs) *IntCmd {
args := make([]interface{}, 0, 9+len(a.IDs))
args = append(args, "xnack", a.Stream, a.Group, a.Mode, "ids", len(a.IDs))
for _, id := range a.IDs {
args = append(args, id)
}
if a.RetryCount != nil {
args = append(args, "retrycount", *a.RetryCount)
}
if a.Force {
args = append(args, "force")
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XPending(ctx context.Context, stream, group string) *XPendingCmd {
cmd := NewXPendingCmd(ctx, "xpending", stream, group)
_ = c(ctx, cmd)
return cmd
}
type XPendingExtArgs struct {
Stream string
Group string
Idle time.Duration
Start string
End string
Count int64
Consumer string
}
func (c cmdable) XPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd {
args := make([]interface{}, 0, 9)
args = append(args, "xpending", a.Stream, a.Group)
if a.Idle != 0 {
args = append(args, "idle", formatMs(ctx, a.Idle))
}
args = append(args, a.Start, a.End, a.Count)
if a.Consumer != "" {
args = append(args, a.Consumer)
}
cmd := NewXPendingExtCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
type XAutoClaimArgs struct {
Stream string
Group string
MinIdle time.Duration
Start string
Count int64
Consumer string
}
func (c cmdable) XAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimCmd {
args := xAutoClaimArgs(ctx, a)
cmd := NewXAutoClaimCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XAutoClaimWithDeleted(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimWithDeletedCmd {
args := xAutoClaimArgs(ctx, a)
cmd := NewXAutoClaimWithDeletedCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd {
args := xAutoClaimArgs(ctx, a)
args = append(args, "justid")
cmd := NewXAutoClaimJustIDCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func xAutoClaimArgs(ctx context.Context, a *XAutoClaimArgs) []interface{} {
args := make([]interface{}, 0, 8)
args = append(args, "xautoclaim", a.Stream, a.Group, a.Consumer, formatMs(ctx, a.MinIdle), a.Start)
if a.Count > 0 {
args = append(args, "count", a.Count)
}
return args
}
type XClaimArgs struct {
Stream string
Group string
Consumer string
MinIdle time.Duration
Messages []string
}
func (c cmdable) XClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd {
args := xClaimArgs(a)
cmd := NewXMessageSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd {
args := xClaimArgs(a)
args = append(args, "justid")
cmd := NewStringSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func xClaimArgs(a *XClaimArgs) []interface{} {
args := make([]interface{}, 0, 5+len(a.Messages))
args = append(args,
"xclaim",
a.Stream,
a.Group, a.Consumer,
int64(a.MinIdle/time.Millisecond))
for _, id := range a.Messages {
args = append(args, id)
}
return args
}
// TODO: refactor xTrim, xTrimMode and the wrappers over the functions
// xTrim If approx is true, add the "~" parameter, otherwise it is the default "=" (redis default).
// example:
//
// XTRIM key MAXLEN/MINID threshold LIMIT limit.
// XTRIM key MAXLEN/MINID ~ threshold LIMIT limit.
//
// The redis-server version is lower than 6.2, please set limit to 0.
//
// limit == 0 omits the LIMIT clause, a positive limit emits "LIMIT <limit>",
// and a negative limit (see XTrimLimitDisabled) emits "LIMIT 0" to disable
// the trimming effort cap. LIMIT requires approx; Redis rejects it otherwise.
func (c cmdable) xTrim(
ctx context.Context, key, strategy string,
approx bool, threshold interface{}, limit int64,
) *IntCmd {
args := make([]interface{}, 0, 7)
args = append(args, "xtrim", key, strategy)
if approx {
args = append(args, "~")
} else {
args = append(args, "=")
}
args = append(args, threshold)
args = appendXTrimLimit(args, limit)
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// XTrimMaxLen No `~` rules are used, `limit` cannot be used.
// cmd: XTRIM key MAXLEN maxLen
func (c cmdable) XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd {
return c.xTrim(ctx, key, "maxlen", false, maxLen, 0)
}
// XTrimMaxLenApprox trims the stream using the `~` rule.
// cmd: XTRIM key MAXLEN ~ maxLen [LIMIT limit]
//
// limit == 0 omits the LIMIT clause, limit > 0 emits "LIMIT <limit>", and a
// negative limit (see XTrimLimitDisabled) emits "LIMIT 0" to disable the
// trimming effort cap.
func (c cmdable) XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd {
return c.xTrim(ctx, key, "maxlen", true, maxLen, limit)
}
func (c cmdable) XTrimMinID(ctx context.Context, key string, minID string) *IntCmd {
return c.xTrim(ctx, key, "minid", false, minID, 0)
}
// XTrimMinIDApprox trims the stream using the `~` rule.
// cmd: XTRIM key MINID ~ minID [LIMIT limit]
//
// limit == 0 omits the LIMIT clause, limit > 0 emits "LIMIT <limit>", and a
// negative limit (see XTrimLimitDisabled) emits "LIMIT 0" to disable the
// trimming effort cap.
func (c cmdable) XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd {
return c.xTrim(ctx, key, "minid", true, minID, limit)
}
// xTrimMode is xTrim with a trailing trimming mode argument (e.g. KEEPREF).
// The limit semantics are the same as xTrim's.
func (c cmdable) xTrimMode(
ctx context.Context, key, strategy string,
approx bool, threshold interface{}, limit int64,
mode string,
) *IntCmd {
args := make([]interface{}, 0, 7)
args = append(args, "xtrim", key, strategy)
if approx {
args = append(args, "~")
} else {
args = append(args, "=")
}
args = append(args, threshold)
args = appendXTrimLimit(args, limit)
args = append(args, mode)
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd {
return c.xTrimMode(ctx, key, "maxlen", false, maxLen, 0, mode)
}
func (c cmdable) XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd {
return c.xTrimMode(ctx, key, "maxlen", true, maxLen, limit, mode)
}
func (c cmdable) XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd {
return c.xTrimMode(ctx, key, "minid", false, minID, 0, mode)
}
func (c cmdable) XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd {
return c.xTrimMode(ctx, key, "minid", true, minID, limit, mode)
}
func (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd {
cmd := NewXInfoConsumersCmd(ctx, key, group)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd {
cmd := NewXInfoGroupsCmd(ctx, key)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) XInfoStream(ctx context.Context, key string) *XInfoStreamCmd {
cmd := NewXInfoStreamCmd(ctx, key)
_ = c(ctx, cmd)
return cmd
}
// XInfoStreamFull XINFO STREAM FULL [COUNT count]
// redis-server >= 6.0.
func (c cmdable) XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd {
args := make([]interface{}, 0, 6)
args = append(args, "xinfo", "stream", key, "full")
if count > 0 {
args = append(args, "count", count)
}
cmd := NewXInfoStreamFullCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// XCfgSetArgs represents the arguments for the XCFGSET command.
// Duration is the duration, in seconds, that Redis keeps each idempotent ID.
// MaxSize is the maximum number of most recent idempotent IDs that Redis keeps for each producer ID.
type XCfgSetArgs struct {
Stream string
Duration int64
MaxSize int64
}
// XCfgSet sets the idempotent production configuration for a stream.
// XCFGSET key [IDMP-DURATION duration] [IDMP-MAXSIZE maxsize]
func (c cmdable) XCfgSet(ctx context.Context, a *XCfgSetArgs) *StatusCmd {
args := make([]interface{}, 0, 6)
args = append(args, "xcfgset", a.Stream)
if a.Duration > 0 {
args = append(args, "idmp-duration", a.Duration)
}
if a.MaxSize > 0 {
args = append(args, "idmp-maxsize", a.MaxSize)
}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}