diff --git a/CLAUDE.md b/CLAUDE.md
index f032f95a..bc210ee4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,6 +17,8 @@
- **IMPORTANT**: Example lint: `cd backend/_example/memory_store && golangci-lint run --config ../../.golangci.yml`
- Frontend: `cd frontend && pnpm lint`
- **Before committing**: Always run tests and linter on both main backend AND examples
+- **Dependency Updates**:
+ - When updating Go modules in `backend/`, also run `go mod tidy` (and `go mod vendor`) in `backend/_example/memory_store` to keep indirect deps in sync. The example module replaces `github.com/umputun/remark42/backend` with `../../` so stale indirect deps there will break the example build.
## Code Style
- **Backend**: Formatting with golangci-lint, strict error handling
diff --git a/backend/_example/memory_store/go.mod b/backend/_example/memory_store/go.mod
index ef7527a8..d3e47f6b 100644
--- a/backend/_example/memory_store/go.mod
+++ b/backend/_example/memory_store/go.mod
@@ -4,7 +4,7 @@ go 1.25.0
require (
github.com/go-pkgz/jrpc v0.4.0
- github.com/go-pkgz/lgr v0.12.1
+ github.com/go-pkgz/lgr v0.12.3
github.com/jessevdk/go-flags v1.6.1
github.com/stretchr/testify v1.11.1
github.com/umputun/remark42/backend v1.1000.0
@@ -12,7 +12,7 @@ require (
require (
github.com/Depado/bfchroma/v2 v2.0.0 // indirect
- github.com/PuerkitoBio/goquery v1.11.0 // indirect
+ github.com/PuerkitoBio/goquery v1.12.0 // indirect
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
@@ -30,10 +30,10 @@ require (
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
- golang.org/x/crypto v0.48.0 // indirect
- golang.org/x/image v0.38.0 // indirect
- golang.org/x/net v0.49.0 // indirect
- golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/crypto v0.50.0 // indirect
+ golang.org/x/image v0.39.0 // indirect
+ golang.org/x/net v0.53.0 // indirect
+ golang.org/x/sys v0.43.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/backend/_example/memory_store/go.sum b/backend/_example/memory_store/go.sum
index 036e8cb2..4fd379bd 100644
--- a/backend/_example/memory_store/go.sum
+++ b/backend/_example/memory_store/go.sum
@@ -1,7 +1,7 @@
github.com/Depado/bfchroma/v2 v2.0.0 h1:IRpN9BPkNwEpR6w1ectIcNWOuhDSLx+8f1pn83fzxx8=
github.com/Depado/bfchroma/v2 v2.0.0/go.mod h1:wFwW/Pw8Tnd0irzgO9Zxtxgzp3aPS8qBWlyadxujxmw=
-github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
-github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
+github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
+github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
@@ -19,8 +19,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/go-pkgz/jrpc v0.4.0 h1:oD7xiGrzDkndkuCjeHGugQXxbggLSV7O1QmHhoc5pYY=
github.com/go-pkgz/jrpc v0.4.0/go.mod h1:JFoY3bRjRyx4M3CbEVDFQStMB1m2gmQ7OjqFK7q3kOo=
-github.com/go-pkgz/lgr v0.12.1 h1:8GVfG2rSARq3Eaj5PP158rtBR2LHVGkwioIkQBGbvKg=
-github.com/go-pkgz/lgr v0.12.1/go.mod h1:A4AxjOthFVFK6jRnVYMeusno5SeDAxcLVHd0kI/lN/Y=
+github.com/go-pkgz/lgr v0.12.3 h1:QDug7kRkEsuQtruT9fNF5PVT2kZUqCDPc4GmsgS3fP8=
+github.com/go-pkgz/lgr v0.12.3/go.mod h1:lpCDgVvCIxBHZp8+sGCj9MPctIzKZyZ3QdE19ddqd54=
github.com/go-pkgz/rest v1.21.0 h1:Y/C4d/TpclJJDxqnH1RAcS6Hmox0RIReAlkwMcUWXK4=
github.com/go-pkgz/rest v1.21.0/go.mod h1:+AHzjHazq7Z3Tk/kRWOhbbAz/YZlUV40feC1Hf4NtbE=
github.com/go-pkgz/routegroup v1.6.0 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4=
@@ -62,10 +62,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
-golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
-golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
-golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
+golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
+golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
+golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
+golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -80,8 +80,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
-golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -89,8 +89,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -102,8 +102,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
diff --git a/backend/go.mod b/backend/go.mod
index 65a45b5d..5a4c2192 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -1,10 +1,10 @@
module github.com/umputun/remark42/backend
-go 1.25
+go 1.25.0
require (
github.com/Depado/bfchroma/v2 v2.0.0
- github.com/PuerkitoBio/goquery v1.11.0
+ github.com/PuerkitoBio/goquery v1.12.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/didip/tollbooth/v8 v8.0.1
github.com/go-chi/chi/v5 v5.2.5
@@ -12,7 +12,7 @@ require (
github.com/go-pkgz/auth/v2 v2.1.2-0.20260211003156-fbba7f2baa6b
github.com/go-pkgz/jrpc v0.4.0
github.com/go-pkgz/lcw/v2 v2.0.0
- github.com/go-pkgz/lgr v0.12.1
+ github.com/go-pkgz/lgr v0.12.3
github.com/go-pkgz/notify v1.3.0
github.com/go-pkgz/repeater/v2 v2.2.0
github.com/go-pkgz/rest v1.21.0
@@ -30,9 +30,9 @@ require (
github.com/stretchr/testify v1.11.1
go.etcd.io/bbolt v1.4.3
go.uber.org/goleak v1.3.0
- golang.org/x/crypto v0.48.0
- golang.org/x/image v0.36.0
- golang.org/x/net v0.49.0
+ golang.org/x/crypto v0.50.0
+ golang.org/x/image v0.39.0
+ golang.org/x/net v0.53.0
)
require (
@@ -54,20 +54,21 @@ require (
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
- github.com/klauspost/compress v1.18.2 // indirect
- github.com/montanaflynn/stats v0.7.1 // indirect
+ github.com/klauspost/compress v1.18.5 // indirect
+ github.com/montanaflynn/stats v0.9.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/redis/go-redis/v9 v9.17.2 // indirect
+ github.com/redis/go-redis/v9 v9.18.0 // indirect
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d // indirect
- github.com/slack-go/slack v0.17.3 // indirect
+ github.com/slack-go/slack v0.21.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
- go.mongodb.org/mongo-driver v1.17.6 // indirect
- golang.org/x/oauth2 v0.34.0 // indirect
- golang.org/x/sync v0.19.0 // indirect
- golang.org/x/sys v0.41.0 // indirect
- golang.org/x/text v0.34.0 // indirect
+ go.mongodb.org/mongo-driver v1.17.9 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
+ golang.org/x/oauth2 v0.36.0 // indirect
+ golang.org/x/sync v0.20.0 // indirect
+ golang.org/x/sys v0.43.0 // indirect
+ golang.org/x/text v0.36.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/backend/go.sum b/backend/go.sum
index 03174e45..594b94aa 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -2,8 +2,8 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/Depado/bfchroma/v2 v2.0.0 h1:IRpN9BPkNwEpR6w1ectIcNWOuhDSLx+8f1pn83fzxx8=
github.com/Depado/bfchroma/v2 v2.0.0/go.mod h1:wFwW/Pw8Tnd0irzgO9Zxtxgzp3aPS8qBWlyadxujxmw=
-github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
-github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
+github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
+github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
@@ -58,8 +58,8 @@ github.com/go-pkgz/jrpc v0.4.0 h1:oD7xiGrzDkndkuCjeHGugQXxbggLSV7O1QmHhoc5pYY=
github.com/go-pkgz/jrpc v0.4.0/go.mod h1:JFoY3bRjRyx4M3CbEVDFQStMB1m2gmQ7OjqFK7q3kOo=
github.com/go-pkgz/lcw/v2 v2.0.0 h1:gTwXpiJBhQeA1rXuqkRuLcV79uATFna8CckH8ZBBrH0=
github.com/go-pkgz/lcw/v2 v2.0.0/go.mod h1:yxJHOn+IbQBQHxUqkCtMrbGjIfdYcsBAZcVCBaL1Va8=
-github.com/go-pkgz/lgr v0.12.1 h1:8GVfG2rSARq3Eaj5PP158rtBR2LHVGkwioIkQBGbvKg=
-github.com/go-pkgz/lgr v0.12.1/go.mod h1:A4AxjOthFVFK6jRnVYMeusno5SeDAxcLVHd0kI/lN/Y=
+github.com/go-pkgz/lgr v0.12.3 h1:QDug7kRkEsuQtruT9fNF5PVT2kZUqCDPc4GmsgS3fP8=
+github.com/go-pkgz/lgr v0.12.3/go.mod h1:lpCDgVvCIxBHZp8+sGCj9MPctIzKZyZ3QdE19ddqd54=
github.com/go-pkgz/notify v1.3.0 h1:YxF/ThEoCetdcoghWdyeqaBpCkZ8mvyve7HXbCAOzYU=
github.com/go-pkgz/notify v1.3.0/go.mod h1:qdfi5OsViKlIFPryIOaINHTOtS9GFhOYXPqJmAMlaGU=
github.com/go-pkgz/repeater v1.2.0 h1:oJFvjyKdTDd5RCzpzxlzYIZFFj6Zfl17rE1aUfu6UjQ=
@@ -108,8 +108,10 @@ github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bB
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
-github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
+github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
+github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -118,14 +120,14 @@ github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
-github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
-github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
+github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
-github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
+github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
+github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d h1:l3+2LWCbVxn5itfvXAfH9n4YL9jh8l1g5zcncbIc1cs=
@@ -138,8 +140,8 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
-github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
-github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
+github.com/slack-go/slack v0.21.1 h1:vBHR+IkaXbv9RLY6w/RiN82D+5/OTI06CGqrlZ3Vyas=
+github.com/slack-go/slack v0.21.1/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE=
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@@ -189,10 +191,14 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
+github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
+github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
-go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
-go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
+go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
+go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -201,10 +207,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
-golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
-golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
-golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
+golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
+golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
+golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
+golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -219,10 +225,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
-golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
-golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
-golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -230,8 +236,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -243,8 +249,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -264,8 +270,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
-golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/backend/vendor/github.com/PuerkitoBio/goquery/README.md b/backend/vendor/github.com/PuerkitoBio/goquery/README.md
index 32147261..395314a0 100644
--- a/backend/vendor/github.com/PuerkitoBio/goquery/README.md
+++ b/backend/vendor/github.com/PuerkitoBio/goquery/README.md
@@ -24,6 +24,7 @@ Syntax-wise, it is as close as possible to jQuery, with the same function names
Required Go version:
+* Starting with version `v1.12.0` of goquery, Go 1.25+ is required due to its dependencies.
* Starting with version `v1.11.0` of goquery, Go 1.24+ is required due to its dependencies.
* Starting with version `v1.10.0` of goquery, Go 1.23+ is required due to the use of function-based iterators.
* For `v1.9.0` of goquery, Go 1.18+ is required due to the use of generics.
@@ -47,6 +48,7 @@ Ongoing goquery development is tested on the latest 2 versions of Go.
**Note that goquery's API is now stable, and will not break.**
+* **2026-03-15 (v1.12.0)** : Update `go.mod` dependencies, add go1.26 to the test matrix, **goquery now requires Go version 1.25+**.
* **2025-11-16 (v1.11.0)** : Update `go.mod` dependencies, add go1.25 to the test matrix, **goquery now requires Go version 1.24+**.
* **2025-04-11 (v1.10.3)** : Update `go.mod` dependencies, small optimization (thanks [@myxzlpltk](https://github.com/myxzlpltk)).
* **2025-02-13 (v1.10.2)** : Update `go.mod` dependencies, add go1.24 to the test matrix.
diff --git a/backend/vendor/github.com/go-pkgz/lgr/CLAUDE.md b/backend/vendor/github.com/go-pkgz/lgr/CLAUDE.md
new file mode 100644
index 00000000..76818245
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/lgr/CLAUDE.md
@@ -0,0 +1,28 @@
+# Go-PKGZ/LGR Development Guidelines
+
+## Build & Test Commands
+- Build: `go build -race`
+- Test all: `go test -timeout=60s -race -covermode=atomic -coverprofile=profile.cov`
+- Test single file: `go test -run TestName`
+- Benchmark: `go test -bench=. -run=Bench`
+- Lint: `golangci-lint run`
+
+## Code Style Guidelines
+- Go 1.21 compatibility required
+- Maximum line length: 140 characters
+- No package names with underscores
+- Use early returns (enforced by prealloc linter)
+- Test files use testify for assertions: `require` for fatal assertions, `assert` for non-fatal ones
+- Indent with tabs, not spaces
+
+## Error Handling
+- FATAL logs to stderr and calls os.Exit(1)
+- ERROR logs to both stdout and stderr
+- PANIC logs stack trace and runtime info to stderr
+- Stack traces for ERROR level can be enabled with StackTraceOnError option
+
+## Project Conventions
+- Public API follows interface-based design (`lgr.L` interface)
+- Avoid global loggers, prefer dependency injection
+- Functional options pattern for logger configuration
+- Secret logging sanitization with `lgr.Secret` option
\ No newline at end of file
diff --git a/backend/vendor/github.com/go-pkgz/lgr/logger.go b/backend/vendor/github.com/go-pkgz/lgr/logger.go
index 2d8471ca..14ae39ca 100644
--- a/backend/vendor/github.com/go-pkgz/lgr/logger.go
+++ b/backend/vendor/github.com/go-pkgz/lgr/logger.go
@@ -166,9 +166,10 @@ func (l *Logger) logf(format string, args ...interface{}) {
// if slog handler is set, use it
if l.slogHandler != nil {
- // use NewRecord for consistency with adapter setup
- // skip=0 because we don't need caller information from this context
- record := slog.NewRecord(l.now(), stringToLevel(lv), msg, 0)
+ // get the caller's PC so slog handlers can resolve source info when AddSource is enabled
+ var pcs [1]uintptr
+ runtime.Callers(3+l.callerDepth, pcs[:]) // skip runtime.Callers, logf, Logf (+ any extra depth)
+ record := slog.NewRecord(l.now(), stringToLevel(lv), msg, pcs[0])
_ = l.slogHandler.Handle(context.Background(), record)
// handle FATAL and PANIC levels as they have special behavior
diff --git a/backend/vendor/github.com/go-pkgz/lgr/slog.go b/backend/vendor/github.com/go-pkgz/lgr/slog.go
index b68af07a..dd9ac3ab 100644
--- a/backend/vendor/github.com/go-pkgz/lgr/slog.go
+++ b/backend/vendor/github.com/go-pkgz/lgr/slog.go
@@ -23,13 +23,13 @@ func FromSlogHandler(h slog.Handler) L {
// SetupWithSlog sets up the global logger with a slog logger
func SetupWithSlog(logger *slog.Logger) {
options := []Option{SlogHandler(logger.Handler())}
-
+
// check if the slog handler is enabled for debug level
// if so, enable debug mode in lgr to prevent filtering
if logger.Handler().Enabled(context.Background(), slog.LevelDebug) {
options = append(options, Debug)
}
-
+
Setup(options...)
}
@@ -59,12 +59,6 @@ func (h *lgrSlogHandler) Handle(_ context.Context, record slog.Record) error {
// build message with attributes
msg := record.Message
- // add time if record has it, otherwise current time is used by lgr
- var timeStr string
- if !record.Time.IsZero() {
- timeStr = record.Time.Format("2006/01/02 15:04:05.000 ")
- }
-
// format attributes as key=value pairs
var attrs strings.Builder
if len(h.attrs) > 0 || record.NumAttrs() > 0 {
@@ -82,8 +76,8 @@ func (h *lgrSlogHandler) Handle(_ context.Context, record slog.Record) error {
return true
})
- // combine everything into final message
- logMsg := fmt.Sprintf("%s%s %s%s", timeStr, level, msg, attrs.String())
+ // combine level prefix and message; lgr.Logf adds its own timestamp and level formatting
+ logMsg := fmt.Sprintf("%s %s%s", level, msg, attrs.String())
h.lgr.Logf(logMsg)
return nil
}
@@ -115,39 +109,15 @@ type slogLgrAdapter struct {
// Logf implements lgr.L interface
func (a *slogLgrAdapter) Logf(format string, args ...interface{}) {
- // parse log level from the beginning of the message
msg := fmt.Sprintf(format, args...)
level, msg := extractLevel(msg)
- // create a record with caller information
- // skip level is critical:
- // - 0 = this line
- // - 1 = this function (Logf)
- // - 2 = caller of Logf (user code)
- //
- // note: We use PC=0 to ensure slog.Record.PC() returns 0,
- // which causes slog to skip obtaining the caller info itself
- record := slog.NewRecord(time.Now(), stringToLevel(level), msg, 2)
+ // get the caller's PC so slog handlers can resolve source info when AddSource is enabled
+ var pcs [1]uintptr
+ runtime.Callers(2, pcs[:]) // skip runtime.Callers and Logf
+ record := slog.NewRecord(time.Now(), stringToLevel(level), msg, pcs[0])
- // we need to manually add the source information ourselves, since
- // slog.Handler might have AddSource=true but won't get the caller
- // right due to how we're adapting lgr → slog
- pc, file, line, ok := runtime.Caller(2) // skip to caller of Logf
- if ok {
- // only add source info if we can find it
- funcName := runtime.FuncForPC(pc).Name()
- record.AddAttrs(
- slog.Group("source",
- slog.String("function", funcName),
- slog.String("file", file),
- slog.Int("line", line),
- ),
- )
- }
-
- // handle the record
if err := a.handler.Handle(context.Background(), record); err != nil {
- // if handling fails, fallback to stderr
fmt.Fprintf(os.Stderr, "slog handler error: %v\n", err)
}
}
diff --git a/backend/vendor/github.com/klauspost/compress/.goreleaser.yml b/backend/vendor/github.com/klauspost/compress/.goreleaser.yml
index 4528059c..804a2018 100644
--- a/backend/vendor/github.com/klauspost/compress/.goreleaser.yml
+++ b/backend/vendor/github.com/klauspost/compress/.goreleaser.yml
@@ -31,6 +31,9 @@ builds:
- mips64le
goarm:
- 7
+ ignore:
+ - goos: windows
+ goarch: arm
-
id: "s2d"
binary: s2d
@@ -57,6 +60,9 @@ builds:
- mips64le
goarm:
- 7
+ ignore:
+ - goos: windows
+ goarch: arm
-
id: "s2sx"
binary: s2sx
@@ -84,6 +90,9 @@ builds:
- mips64le
goarm:
- 7
+ ignore:
+ - goos: windows
+ goarch: arm
archives:
-
@@ -91,7 +100,7 @@ archives:
name_template: "s2-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: windows
- format: zip
+ formats: ['zip']
files:
- unpack/*
- s2/LICENSE
diff --git a/backend/vendor/github.com/klauspost/compress/README.md b/backend/vendor/github.com/klauspost/compress/README.md
index af2ef639..e839fe9c 100644
--- a/backend/vendor/github.com/klauspost/compress/README.md
+++ b/backend/vendor/github.com/klauspost/compress/README.md
@@ -7,7 +7,7 @@ This package provides various compression algorithms.
* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
* [snappy](https://github.com/klauspost/compress/tree/master/snappy) is a drop-in replacement for `github.com/golang/snappy` offering better compression and concurrent streams.
* [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding.
-* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped requests efficiently.
+* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped/zstd HTTP requests efficiently.
* [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation.
[](https://pkg.go.dev/github.com/klauspost/compress?tab=subdirectories)
@@ -27,7 +27,19 @@ Use the links above for more information on each.
# changelog
-* Oct 20, 2025 - [1.18.1](https://github.com/klauspost/compress/releases/tag/v1.18.1)
+* Feb 9th, 2026 [1.18.4](https://github.com/klauspost/compress/releases/tag/v1.18.4)
+ * gzhttp: Add zstandard to server handler wrapper https://github.com/klauspost/compress/pull/1121
+ * zstd: Add ResetWithOptions to encoder/decoder https://github.com/klauspost/compress/pull/1122
+ * gzhttp: preserve qvalue when extra parameters follow in Accept-Encoding by @analytically in https://github.com/klauspost/compress/pull/1116
+
+* Jan 16th, 2026 [1.18.3](https://github.com/klauspost/compress/releases/tag/v1.18.3)
+ * Downstream CVE-2025-61728. See [golang/go#77102](https://github.com/golang/go/issues/77102).
+
+* Dec 1st, 2025 - [1.18.2](https://github.com/klauspost/compress/releases/tag/v1.18.2)
+ * flate: Fix invalid encoding on level 9 with single value input in https://github.com/klauspost/compress/pull/1115
+ * flate: reduce stateless allocations by @RXamzin in https://github.com/klauspost/compress/pull/1106
+
+* Oct 20, 2025 - [1.18.1](https://github.com/klauspost/compress/releases/tag/v1.18.1) - RETRACTED
* zstd: Add simple zstd EncodeTo/DecodeTo functions https://github.com/klauspost/compress/pull/1079
* zstd: Fix incorrect buffer size in dictionary encodes https://github.com/klauspost/compress/pull/1059
* s2: check for cap, not len of buffer in EncodeBetter/Best by @vdarulis in https://github.com/klauspost/compress/pull/1080
@@ -603,7 +615,7 @@ While the release has been extensively tested, it is recommended to testing when
# deflate usage
-The packages are drop-in replacements for standard libraries. Simply replace the import path to use them:
+The packages are drop-in replacements for standard library [deflate](https://godoc.org/github.com/klauspost/compress/flate), [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip), and [zlib](https://godoc.org/github.com/klauspost/compress/zlib). Simply replace the import path to use them:
Typical speed is about 2x of the standard library packages.
@@ -614,17 +626,15 @@ Typical speed is about 2x of the standard library packages.
| `archive/zip` | `github.com/klauspost/compress/zip` | [zip](https://pkg.go.dev/github.com/klauspost/compress/zip?tab=doc) |
| `compress/flate` | `github.com/klauspost/compress/flate` | [flate](https://pkg.go.dev/github.com/klauspost/compress/flate?tab=doc) |
-* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
+You may also be interested in [pgzip](https://github.com/klauspost/pgzip), which is a drop-in replacement for gzip, which support multithreaded compression on big files and the optimized [crc32](https://github.com/klauspost/crc32) package used by these packages.
-You may also be interested in [pgzip](https://github.com/klauspost/pgzip), which is a drop in replacement for gzip, which support multithreaded compression on big files and the optimized [crc32](https://github.com/klauspost/crc32) package used by these packages.
-
-The packages contains the same as the standard library, so you can use the godoc for that: [gzip](http://golang.org/pkg/compress/gzip/), [zip](http://golang.org/pkg/archive/zip/), [zlib](http://golang.org/pkg/compress/zlib/), [flate](http://golang.org/pkg/compress/flate/).
+The packages implement the same API as the standard library, so you can use the original godoc documentation: [gzip](http://golang.org/pkg/compress/gzip/), [zip](http://golang.org/pkg/archive/zip/), [zlib](http://golang.org/pkg/compress/zlib/), [flate](http://golang.org/pkg/compress/flate/).
Currently there is only minor speedup on decompression (mostly CRC32 calculation).
Memory usage is typically 1MB for a Writer. stdlib is in the same range.
If you expect to have a lot of concurrently allocated Writers consider using
-the stateless compress described below.
+the stateless compression described below.
For compression performance, see: [this spreadsheet](https://docs.google.com/spreadsheets/d/1nuNE2nPfuINCZJRMt6wFWhKpToF95I47XjSsc-1rbPQ/edit?usp=sharing).
@@ -684,3 +694,7 @@ Here are other packages of good quality and pure Go (no cgo wrappers or autoconv
This code is licensed under the same conditions as the original Go code. See LICENSE file.
+
+
+
+
diff --git a/backend/vendor/github.com/klauspost/compress/huff0/decompress_amd64.go b/backend/vendor/github.com/klauspost/compress/huff0/decompress_amd64.go
index 99ddd4af..2d6ef64b 100644
--- a/backend/vendor/github.com/klauspost/compress/huff0/decompress_amd64.go
+++ b/backend/vendor/github.com/klauspost/compress/huff0/decompress_amd64.go
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
-// +build amd64,!appengine,!noasm,gc
// This file contains the specialisation of Decoder.Decompress4X
// and Decoder.Decompress1X that use an asm implementation of thir main loops.
diff --git a/backend/vendor/github.com/klauspost/compress/huff0/decompress_generic.go b/backend/vendor/github.com/klauspost/compress/huff0/decompress_generic.go
index 908c17de..61039232 100644
--- a/backend/vendor/github.com/klauspost/compress/huff0/decompress_generic.go
+++ b/backend/vendor/github.com/klauspost/compress/huff0/decompress_generic.go
@@ -1,5 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
-// +build !amd64 appengine !gc noasm
// This file contains a generic implementation of Decoder.Decompress4X.
package huff0
diff --git a/backend/vendor/github.com/klauspost/compress/internal/cpuinfo/cpuinfo_amd64.go b/backend/vendor/github.com/klauspost/compress/internal/cpuinfo/cpuinfo_amd64.go
index e802579c..b97f9056 100644
--- a/backend/vendor/github.com/klauspost/compress/internal/cpuinfo/cpuinfo_amd64.go
+++ b/backend/vendor/github.com/klauspost/compress/internal/cpuinfo/cpuinfo_amd64.go
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
-// +build amd64,!appengine,!noasm,gc
package cpuinfo
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/blockenc.go b/backend/vendor/github.com/klauspost/compress/zstd/blockenc.go
index fd35ea14..0e33aea4 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/blockenc.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/blockenc.go
@@ -78,6 +78,7 @@ func (b *blockEnc) initNewEncode() {
b.recentOffsets = [3]uint32{1, 4, 8}
b.litEnc.Reuse = huff0.ReusePolicyNone
b.coders.setPrev(nil, nil, nil)
+ b.dictLitEnc = nil
}
// reset will reset the block for a new encode, but in the same stream,
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/decoder.go b/backend/vendor/github.com/klauspost/compress/zstd/decoder.go
index 30df5513..c7e500f0 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/decoder.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/decoder.go
@@ -39,9 +39,6 @@ type Decoder struct {
frame *frameDec
- // Custom dictionaries.
- dicts map[uint32]*dict
-
// streamWg is the waitgroup for all streams
streamWg sync.WaitGroup
}
@@ -101,12 +98,10 @@ func NewReader(r io.Reader, opts ...DOption) (*Decoder, error) {
d.current.err = ErrDecoderNilInput
}
- // Transfer option dicts.
- d.dicts = make(map[uint32]*dict, len(d.o.dicts))
- for _, dc := range d.o.dicts {
- d.dicts[dc.id] = dc
+ // Initialize dict map if needed.
+ if d.o.dicts == nil {
+ d.o.dicts = make(map[uint32]*dict)
}
- d.o.dicts = nil
// Create decoders
d.decoders = make(chan *blockDec, d.o.concurrent)
@@ -238,6 +233,21 @@ func (d *Decoder) Reset(r io.Reader) error {
return nil
}
+// ResetWithOptions will reset the decoder and apply the given options
+// for the next stream or DecodeAll operation.
+// Options are applied on top of the existing options.
+// Some options cannot be changed on reset and will return an error.
+func (d *Decoder) ResetWithOptions(r io.Reader, opts ...DOption) error {
+ d.o.resetOpt = true
+ defer func() { d.o.resetOpt = false }()
+ for _, o := range opts {
+ if err := o(&d.o); err != nil {
+ return err
+ }
+ }
+ return d.Reset(r)
+}
+
// drainOutput will drain the output until errEndOfStream is sent.
func (d *Decoder) drainOutput() {
if d.current.cancel != nil {
@@ -930,7 +940,7 @@ decodeStream:
}
func (d *Decoder) setDict(frame *frameDec) (err error) {
- dict, ok := d.dicts[frame.DictionaryID]
+ dict, ok := d.o.dicts[frame.DictionaryID]
if ok {
if debugDecoder {
println("setting dict", frame.DictionaryID)
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/decoder_options.go b/backend/vendor/github.com/klauspost/compress/zstd/decoder_options.go
index 774c5f00..537627a0 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/decoder_options.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/decoder_options.go
@@ -20,10 +20,11 @@ type decoderOptions struct {
concurrent int
maxDecodedSize uint64
maxWindowSize uint64
- dicts []*dict
+ dicts map[uint32]*dict
ignoreChecksum bool
limitToCap bool
decodeBufsBelow int
+ resetOpt bool
}
func (o *decoderOptions) setDefault() {
@@ -42,8 +43,15 @@ func (o *decoderOptions) setDefault() {
// WithDecoderLowmem will set whether to use a lower amount of memory,
// but possibly have to allocate more while running.
+// Cannot be changed with ResetWithOptions.
func WithDecoderLowmem(b bool) DOption {
- return func(o *decoderOptions) error { o.lowMem = b; return nil }
+ return func(o *decoderOptions) error {
+ if o.resetOpt && b != o.lowMem {
+ return errors.New("WithDecoderLowmem cannot be changed on Reset")
+ }
+ o.lowMem = b
+ return nil
+ }
}
// WithDecoderConcurrency sets the number of created decoders.
@@ -53,18 +61,23 @@ func WithDecoderLowmem(b bool) DOption {
// inflight blocks.
// When decoding streams and setting maximum to 1,
// no async decoding will be done.
+// The value supplied must be at least 0.
// When a value of 0 is provided GOMAXPROCS will be used.
// By default this will be set to 4 or GOMAXPROCS, whatever is lower.
+// Cannot be changed with ResetWithOptions.
func WithDecoderConcurrency(n int) DOption {
return func(o *decoderOptions) error {
if n < 0 {
- return errors.New("concurrency must be at least 1")
+ return errors.New("concurrency must be at least 0")
}
+ newVal := n
if n == 0 {
- o.concurrent = runtime.GOMAXPROCS(0)
- } else {
- o.concurrent = n
+ newVal = runtime.GOMAXPROCS(0)
}
+ if o.resetOpt && newVal != o.concurrent {
+ return errors.New("WithDecoderConcurrency cannot be changed on Reset")
+ }
+ o.concurrent = newVal
return nil
}
}
@@ -73,6 +86,7 @@ func WithDecoderConcurrency(n int) DOption {
// non-streaming operations or maximum window size for streaming operations.
// This can be used to control memory usage of potentially hostile content.
// Maximum is 1 << 63 bytes. Default is 64GiB.
+// Can be changed with ResetWithOptions.
func WithDecoderMaxMemory(n uint64) DOption {
return func(o *decoderOptions) error {
if n == 0 {
@@ -92,16 +106,20 @@ func WithDecoderMaxMemory(n uint64) DOption {
// "zstd --train" from the Zstandard reference implementation.
//
// If several dictionaries with the same ID are provided, the last one will be used.
+// Can be changed with ResetWithOptions.
//
// [dictionary format]: https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#dictionary-format
func WithDecoderDicts(dicts ...[]byte) DOption {
return func(o *decoderOptions) error {
+ if o.dicts == nil {
+ o.dicts = make(map[uint32]*dict)
+ }
for _, b := range dicts {
d, err := loadDict(b)
if err != nil {
return err
}
- o.dicts = append(o.dicts, d)
+ o.dicts[d.id] = d
}
return nil
}
@@ -109,12 +127,16 @@ func WithDecoderDicts(dicts ...[]byte) DOption {
// WithDecoderDictRaw registers a dictionary that may be used by the decoder.
// The slice content can be arbitrary data.
+// Can be changed with ResetWithOptions.
func WithDecoderDictRaw(id uint32, content []byte) DOption {
return func(o *decoderOptions) error {
if bits.UintSize > 32 && uint(len(content)) > dictMaxLength {
return fmt.Errorf("dictionary of size %d > 2GiB too large", len(content))
}
- o.dicts = append(o.dicts, &dict{id: id, content: content, offsets: [3]int{1, 4, 8}})
+ if o.dicts == nil {
+ o.dicts = make(map[uint32]*dict)
+ }
+ o.dicts[id] = &dict{id: id, content: content, offsets: [3]int{1, 4, 8}}
return nil
}
}
@@ -124,6 +146,7 @@ func WithDecoderDictRaw(id uint32, content []byte) DOption {
// The Decoder will likely allocate more memory based on the WithDecoderLowmem setting.
// If WithDecoderMaxMemory is set to a lower value, that will be used.
// Default is 512MB, Maximum is ~3.75 TB as per zstandard spec.
+// Can be changed with ResetWithOptions.
func WithDecoderMaxWindow(size uint64) DOption {
return func(o *decoderOptions) error {
if size < MinWindowSize {
@@ -141,6 +164,7 @@ func WithDecoderMaxWindow(size uint64) DOption {
// or any size set in WithDecoderMaxMemory.
// This can be used to limit decoding to a specific maximum output size.
// Disabled by default.
+// Can be changed with ResetWithOptions.
func WithDecodeAllCapLimit(b bool) DOption {
return func(o *decoderOptions) error {
o.limitToCap = b
@@ -153,17 +177,37 @@ func WithDecodeAllCapLimit(b bool) DOption {
// This typically uses less allocations but will have the full decompressed object in memory.
// Note that DecodeAllCapLimit will disable this, as well as giving a size of 0 or less.
// Default is 128KiB.
+// Cannot be changed with ResetWithOptions.
func WithDecodeBuffersBelow(size int) DOption {
return func(o *decoderOptions) error {
+ if o.resetOpt && size != o.decodeBufsBelow {
+ return errors.New("WithDecodeBuffersBelow cannot be changed on Reset")
+ }
o.decodeBufsBelow = size
return nil
}
}
// IgnoreChecksum allows to forcibly ignore checksum checking.
+// Can be changed with ResetWithOptions.
func IgnoreChecksum(b bool) DOption {
return func(o *decoderOptions) error {
o.ignoreChecksum = b
return nil
}
}
+
+// WithDecoderDictDelete removes dictionaries by ID.
+// If no ids are passed, all dictionaries are deleted.
+// Should be used with ResetWithOptions.
+func WithDecoderDictDelete(ids ...uint32) DOption {
+ return func(o *decoderOptions) error {
+ if len(ids) == 0 {
+ clear(o.dicts)
+ }
+ for _, id := range ids {
+ delete(o.dicts, id)
+ }
+ return nil
+ }
+}
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/enc_base.go b/backend/vendor/github.com/klauspost/compress/zstd/enc_base.go
index c1192ec3..c4de134a 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/enc_base.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/enc_base.go
@@ -21,7 +21,7 @@ type fastBase struct {
crc *xxhash.Digest
tmp [8]byte
blk *blockEnc
- lastDictID uint32
+ lastDict *dict
lowMem bool
}
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/enc_best.go b/backend/vendor/github.com/klauspost/compress/zstd/enc_best.go
index c1581cfc..85179932 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/enc_best.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/enc_best.go
@@ -479,10 +479,13 @@ func (e *bestFastEncoder) Reset(d *dict, singleBlock bool) {
if d == nil {
return
}
+ dictChanged := d != e.lastDict
// Init or copy dict table
- if len(e.dictTable) != len(e.table) || d.id != e.lastDictID {
+ if len(e.dictTable) != len(e.table) || dictChanged {
if len(e.dictTable) != len(e.table) {
e.dictTable = make([]prevEntry, len(e.table))
+ } else {
+ clear(e.dictTable)
}
end := int32(len(d.content)) - 8 + e.maxMatchOff
for i := e.maxMatchOff; i < end; i += 4 {
@@ -510,13 +513,14 @@ func (e *bestFastEncoder) Reset(d *dict, singleBlock bool) {
offset: i + 3,
}
}
- e.lastDictID = d.id
}
- // Init or copy dict table
- if len(e.dictLongTable) != len(e.longTable) || d.id != e.lastDictID {
+ // Init or copy dict long table
+ if len(e.dictLongTable) != len(e.longTable) || dictChanged {
if len(e.dictLongTable) != len(e.longTable) {
e.dictLongTable = make([]prevEntry, len(e.longTable))
+ } else {
+ clear(e.dictLongTable)
}
if len(d.content) >= 8 {
cv := load6432(d.content, 0)
@@ -538,8 +542,8 @@ func (e *bestFastEncoder) Reset(d *dict, singleBlock bool) {
off++
}
}
- e.lastDictID = d.id
}
+ e.lastDict = d
// Reset table to initial state
copy(e.longTable[:], e.dictLongTable)
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/enc_better.go b/backend/vendor/github.com/klauspost/compress/zstd/enc_better.go
index 85dcd28c..3305f092 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/enc_better.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/enc_better.go
@@ -1102,10 +1102,13 @@ func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) {
if d == nil {
return
}
+ dictChanged := d != e.lastDict
// Init or copy dict table
- if len(e.dictTable) != len(e.table) || d.id != e.lastDictID {
+ if len(e.dictTable) != len(e.table) || dictChanged {
if len(e.dictTable) != len(e.table) {
e.dictTable = make([]tableEntry, len(e.table))
+ } else {
+ clear(e.dictTable)
}
end := int32(len(d.content)) - 8 + e.maxMatchOff
for i := e.maxMatchOff; i < end; i += 4 {
@@ -1133,14 +1136,15 @@ func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) {
offset: i + 3,
}
}
- e.lastDictID = d.id
e.allDirty = true
}
- // Init or copy dict table
- if len(e.dictLongTable) != len(e.longTable) || d.id != e.lastDictID {
+ // Init or copy dict long table
+ if len(e.dictLongTable) != len(e.longTable) || dictChanged {
if len(e.dictLongTable) != len(e.longTable) {
e.dictLongTable = make([]prevEntry, len(e.longTable))
+ } else {
+ clear(e.dictLongTable)
}
if len(d.content) >= 8 {
cv := load6432(d.content, 0)
@@ -1162,9 +1166,9 @@ func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) {
off++
}
}
- e.lastDictID = d.id
e.allDirty = true
}
+ e.lastDict = d
// Reset table to initial state
{
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/enc_dfast.go b/backend/vendor/github.com/klauspost/compress/zstd/enc_dfast.go
index cf8cad00..2fb6da11 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/enc_dfast.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/enc_dfast.go
@@ -1040,15 +1040,18 @@ func (e *doubleFastEncoder) Reset(d *dict, singleBlock bool) {
// ResetDict will reset and set a dictionary if not nil
func (e *doubleFastEncoderDict) Reset(d *dict, singleBlock bool) {
allDirty := e.allDirty
+ dictChanged := d != e.lastDict
e.fastEncoderDict.Reset(d, singleBlock)
if d == nil {
return
}
// Init or copy dict table
- if len(e.dictLongTable) != len(e.longTable) || d.id != e.lastDictID {
+ if len(e.dictLongTable) != len(e.longTable) || dictChanged {
if len(e.dictLongTable) != len(e.longTable) {
e.dictLongTable = make([]tableEntry, len(e.longTable))
+ } else {
+ clear(e.dictLongTable)
}
if len(d.content) >= 8 {
cv := load6432(d.content, 0)
@@ -1065,7 +1068,6 @@ func (e *doubleFastEncoderDict) Reset(d *dict, singleBlock bool) {
}
}
}
- e.lastDictID = d.id
allDirty = true
}
// Reset table to initial state
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/enc_fast.go b/backend/vendor/github.com/klauspost/compress/zstd/enc_fast.go
index 9180a3a5..5e104f1a 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/enc_fast.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/enc_fast.go
@@ -805,9 +805,11 @@ func (e *fastEncoderDict) Reset(d *dict, singleBlock bool) {
}
// Init or copy dict table
- if len(e.dictTable) != len(e.table) || d.id != e.lastDictID {
+ if len(e.dictTable) != len(e.table) || d != e.lastDict {
if len(e.dictTable) != len(e.table) {
e.dictTable = make([]tableEntry, len(e.table))
+ } else {
+ clear(e.dictTable)
}
if true {
end := e.maxMatchOff + int32(len(d.content)) - 8
@@ -827,7 +829,7 @@ func (e *fastEncoderDict) Reset(d *dict, singleBlock bool) {
}
}
}
- e.lastDictID = d.id
+ e.lastDict = d
e.allDirty = true
}
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/encoder.go b/backend/vendor/github.com/klauspost/compress/zstd/encoder.go
index 8f8223cd..0f2a00a0 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/encoder.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/encoder.go
@@ -131,6 +131,29 @@ func (e *Encoder) Reset(w io.Writer) {
s.frameContentSize = 0
}
+// ResetWithOptions will re-initialize the writer and apply the given options
+// as a new, independent stream.
+// Options are applied on top of the existing options.
+// Some options cannot be changed on reset and will return an error.
+func (e *Encoder) ResetWithOptions(w io.Writer, opts ...EOption) error {
+ e.o.resetOpt = true
+ defer func() { e.o.resetOpt = false }()
+ hadDict := e.o.dict != nil
+ for _, o := range opts {
+ if err := o(&e.o); err != nil {
+ return err
+ }
+ }
+ hasDict := e.o.dict != nil
+ if hadDict != hasDict {
+ // Dict presence changed — encoder type must be recreated.
+ e.state.encoder = nil
+ e.init = sync.Once{}
+ }
+ e.Reset(w)
+ return nil
+}
+
// ResetContentSize will reset and set a content size for the next stream.
// If the bytes written does not match the size given an error will be returned
// when calling Close().
@@ -432,6 +455,12 @@ func (e *Encoder) Close() error {
if s.encoder == nil {
return nil
}
+ if s.w == nil {
+ if len(s.filling) == 0 && !s.headerWritten && !s.eofWritten && s.nInput == 0 {
+ return nil
+ }
+ return errors.New("zstd: encoder has no writer")
+ }
err := e.nextBlock(true)
if err != nil {
if errors.Is(s.err, ErrEncoderClosed) {
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/encoder_options.go b/backend/vendor/github.com/klauspost/compress/zstd/encoder_options.go
index 20671dcb..e217be0a 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/encoder_options.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/encoder_options.go
@@ -14,6 +14,7 @@ type EOption func(*encoderOptions) error
// options retains accumulated state of multiple options.
type encoderOptions struct {
+ resetOpt bool
concurrent int
level EncoderLevel
single *bool
@@ -41,6 +42,7 @@ func (o *encoderOptions) setDefault() {
level: SpeedDefault,
allLitEntropy: false,
lowMem: false,
+ fullZero: true,
}
}
@@ -71,19 +73,28 @@ func (o encoderOptions) encoder() encoder {
// WithEncoderCRC will add CRC value to output.
// Output will be 4 bytes larger.
+// Can be changed with ResetWithOptions.
func WithEncoderCRC(b bool) EOption {
return func(o *encoderOptions) error { o.crc = b; return nil }
}
// WithEncoderConcurrency will set the concurrency,
// meaning the maximum number of encoders to run concurrently.
-// The value supplied must be at least 1.
+// The value supplied must be at least 0.
+// When a value of 0 is provided GOMAXPROCS will be used.
// For streams, setting a value of 1 will disable async compression.
// By default this will be set to GOMAXPROCS.
+// Cannot be changed with ResetWithOptions.
func WithEncoderConcurrency(n int) EOption {
return func(o *encoderOptions) error {
- if n <= 0 {
- return fmt.Errorf("concurrency must be at least 1")
+ if n < 0 {
+ return errors.New("concurrency must at least 0")
+ }
+ if n == 0 {
+ n = runtime.GOMAXPROCS(0)
+ }
+ if o.resetOpt && n != o.concurrent {
+ return errors.New("WithEncoderConcurrency cannot be changed on Reset")
}
o.concurrent = n
return nil
@@ -95,6 +106,7 @@ func WithEncoderConcurrency(n int) EOption {
// A larger value will enable better compression but allocate more memory and,
// for above-default values, take considerably longer.
// The default value is determined by the compression level and max 8MB.
+// Cannot be changed with ResetWithOptions.
func WithWindowSize(n int) EOption {
return func(o *encoderOptions) error {
switch {
@@ -105,6 +117,9 @@ func WithWindowSize(n int) EOption {
case (n & (n - 1)) != 0:
return errors.New("window size must be a power of 2")
}
+ if o.resetOpt && n != o.windowSize {
+ return errors.New("WithWindowSize cannot be changed on Reset")
+ }
o.windowSize = n
o.customWindow = true
@@ -122,6 +137,7 @@ func WithWindowSize(n int) EOption {
// n must be > 0 and <= 1GB, 1<<30 bytes.
// The padded area will be filled with data from crypto/rand.Reader.
// If `EncodeAll` is used with data already in the destination, the total size will be multiple of this.
+// Can be changed with ResetWithOptions.
func WithEncoderPadding(n int) EOption {
return func(o *encoderOptions) error {
if n <= 0 {
@@ -215,12 +231,16 @@ func (e EncoderLevel) String() string {
}
// WithEncoderLevel specifies a predefined compression level.
+// Cannot be changed with ResetWithOptions.
func WithEncoderLevel(l EncoderLevel) EOption {
return func(o *encoderOptions) error {
switch {
case l <= speedNotSet || l >= speedLast:
return fmt.Errorf("unknown encoder level")
}
+ if o.resetOpt && l != o.level {
+ return errors.New("WithEncoderLevel cannot be changed on Reset")
+ }
o.level = l
if !o.customWindow {
switch o.level {
@@ -248,6 +268,7 @@ func WithEncoderLevel(l EncoderLevel) EOption {
// WithZeroFrames will encode 0 length input as full frames.
// This can be needed for compatibility with zstandard usage,
// but is not needed for this package.
+// Can be changed with ResetWithOptions.
func WithZeroFrames(b bool) EOption {
return func(o *encoderOptions) error {
o.fullZero = b
@@ -259,6 +280,7 @@ func WithZeroFrames(b bool) EOption {
// Disabling this will skip incompressible data faster, but in cases with no matches but
// skewed character distribution compression is lost.
// Default value depends on the compression level selected.
+// Can be changed with ResetWithOptions.
func WithAllLitEntropyCompression(b bool) EOption {
return func(o *encoderOptions) error {
o.customALEntropy = true
@@ -270,6 +292,7 @@ func WithAllLitEntropyCompression(b bool) EOption {
// WithNoEntropyCompression will always skip entropy compression of literals.
// This can be useful if content has matches, but unlikely to benefit from entropy
// compression. Usually the slight speed improvement is not worth enabling this.
+// Can be changed with ResetWithOptions.
func WithNoEntropyCompression(b bool) EOption {
return func(o *encoderOptions) error {
o.noEntropy = b
@@ -287,6 +310,7 @@ func WithNoEntropyCompression(b bool) EOption {
// This is only a recommendation, each decoder is free to support higher or lower limits, depending on local limitations.
// If this is not specified, block encodes will automatically choose this based on the input size and the window size.
// This setting has no effect on streamed encodes.
+// Can be changed with ResetWithOptions.
func WithSingleSegment(b bool) EOption {
return func(o *encoderOptions) error {
o.single = &b
@@ -298,8 +322,12 @@ func WithSingleSegment(b bool) EOption {
// slower encoding speed.
// This will not change the window size which is the primary function for reducing
// memory usage. See WithWindowSize.
+// Cannot be changed with ResetWithOptions.
func WithLowerEncoderMem(b bool) EOption {
return func(o *encoderOptions) error {
+ if o.resetOpt && b != o.lowMem {
+ return errors.New("WithLowerEncoderMem cannot be changed on Reset")
+ }
o.lowMem = b
return nil
}
@@ -311,6 +339,7 @@ func WithLowerEncoderMem(b bool) EOption {
// "zstd --train" from the Zstandard reference implementation.
//
// The encoder *may* choose to use no dictionary instead for certain payloads.
+// Can be changed with ResetWithOptions.
//
// [dictionary format]: https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#dictionary-format
func WithEncoderDict(dict []byte) EOption {
@@ -328,6 +357,7 @@ func WithEncoderDict(dict []byte) EOption {
//
// The slice content may contain arbitrary data. It will be used as an initial
// history.
+// Can be changed with ResetWithOptions.
func WithEncoderDictRaw(id uint32, content []byte) EOption {
return func(o *encoderOptions) error {
if bits.UintSize > 32 && uint(len(content)) > dictMaxLength {
@@ -337,3 +367,12 @@ func WithEncoderDictRaw(id uint32, content []byte) EOption {
return nil
}
}
+
+// WithEncoderDictDelete clears the dictionary, so no dictionary will be used.
+// Should be used with ResetWithOptions.
+func WithEncoderDictDelete() EOption {
+ return func(o *encoderOptions) error {
+ o.dict = nil
+ return nil
+ }
+}
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.go b/backend/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.go
index d04a829b..b8c8607b 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/fse_decoder_amd64.go
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
-// +build amd64,!appengine,!noasm,gc
package zstd
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go b/backend/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go
index 8adfebb0..2138f809 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go
@@ -1,5 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
-// +build !amd64 appengine !gc noasm
package zstd
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/internal/xxhash/xxhash_other.go b/backend/vendor/github.com/klauspost/compress/zstd/internal/xxhash/xxhash_other.go
index 0be16cef..9576426e 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/internal/xxhash/xxhash_other.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/internal/xxhash/xxhash_other.go
@@ -1,5 +1,4 @@
//go:build (!amd64 && !arm64) || appengine || !gc || purego || noasm
-// +build !amd64,!arm64 appengine !gc purego noasm
package xxhash
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/matchlen_amd64.go b/backend/vendor/github.com/klauspost/compress/zstd/matchlen_amd64.go
index f41932b7..1ed18927 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/matchlen_amd64.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/matchlen_amd64.go
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
-// +build amd64,!appengine,!noasm,gc
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/matchlen_generic.go b/backend/vendor/github.com/klauspost/compress/zstd/matchlen_generic.go
index bea1779e..379746c9 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/matchlen_generic.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/matchlen_generic.go
@@ -1,5 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
-// +build !amd64 appengine !gc noasm
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.go b/backend/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.go
index 1f8c3cec..18c3703d 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.go
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
-// +build amd64,!appengine,!noasm,gc
package zstd
diff --git a/backend/vendor/github.com/klauspost/compress/zstd/seqdec_generic.go b/backend/vendor/github.com/klauspost/compress/zstd/seqdec_generic.go
index 7cec2197..516cd9b0 100644
--- a/backend/vendor/github.com/klauspost/compress/zstd/seqdec_generic.go
+++ b/backend/vendor/github.com/klauspost/compress/zstd/seqdec_generic.go
@@ -1,5 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
-// +build !amd64 appengine !gc noasm
package zstd
diff --git a/backend/vendor/github.com/montanaflynn/stats/.github-write-test b/backend/vendor/github.com/montanaflynn/stats/.github-write-test
new file mode 100644
index 00000000..30d74d25
--- /dev/null
+++ b/backend/vendor/github.com/montanaflynn/stats/.github-write-test
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/backend/vendor/github.com/montanaflynn/stats/.goreleaser.yml b/backend/vendor/github.com/montanaflynn/stats/.goreleaser.yml
new file mode 100644
index 00000000..030841d8
--- /dev/null
+++ b/backend/vendor/github.com/montanaflynn/stats/.goreleaser.yml
@@ -0,0 +1,14 @@
+version: 2
+
+builds:
+ - skip: true
+
+changelog:
+ sort: asc
+ groups:
+ - title: Fix
+ regexp: '^fix:'
+ - title: Features
+ regexp: '^feat:'
+ - title: Other
+ order: 999
diff --git a/backend/vendor/github.com/montanaflynn/stats/CHANGELOG.md b/backend/vendor/github.com/montanaflynn/stats/CHANGELOG.md
index 73c3b782..580ce3e3 100644
--- a/backend/vendor/github.com/montanaflynn/stats/CHANGELOG.md
+++ b/backend/vendor/github.com/montanaflynn/stats/CHANGELOG.md
@@ -2,6 +2,16 @@
## [Unreleased]
+
+## [v0.8.0] - 2026-03-11
+### Fix
+- Fix Percentile to use standard NIST linear interpolation method ([#92](https://github.com/montanaflynn/stats/issues/92))
+- Fix Percentile underflow bug ([#88](https://github.com/montanaflynn/stats/issues/88))
+
+### Update
+- Update Codecov upload to v4 and pass token
+
+
## [v0.7.1] - 2023-05-11
### Add
@@ -517,7 +527,8 @@
- Merge pull request [#4](https://github.com/montanaflynn/stats/issues/4) from saromanov/sample
-[Unreleased]: https://github.com/montanaflynn/stats/compare/v0.7.1...HEAD
+[Unreleased]: https://github.com/montanaflynn/stats/compare/v0.8.0...HEAD
+[v0.8.0]: https://github.com/montanaflynn/stats/compare/v0.7.1...v0.8.0
[v0.7.1]: https://github.com/montanaflynn/stats/compare/v0.7.0...v0.7.1
[v0.7.0]: https://github.com/montanaflynn/stats/compare/v0.6.6...v0.7.0
[v0.6.6]: https://github.com/montanaflynn/stats/compare/v0.6.5...v0.6.6
diff --git a/backend/vendor/github.com/montanaflynn/stats/DOCUMENTATION.md b/backend/vendor/github.com/montanaflynn/stats/DOCUMENTATION.md
index 978df2ff..32ec0754 100644
--- a/backend/vendor/github.com/montanaflynn/stats/DOCUMENTATION.md
+++ b/backend/vendor/github.com/montanaflynn/stats/DOCUMENTATION.md
@@ -29,7 +29,7 @@ Example Usage:
roundedMedian, _ := stats.Round(median, 0)
fmt.Println(roundedMedian) // 4
-MIT License Copyright (c) 2014-2020 Montana Flynn (https://montanaflynn.com)
+MIT License Copyright (c) 2014-2026 Montana Flynn (https://montanaflynn.com)
@@ -104,6 +104,10 @@ MIT License Copyright (c) 2014-2020 Montana Flynn (Package files
-[correlation.go](/src/github.com/montanaflynn/stats/correlation.go) [cumulative_sum.go](/src/github.com/montanaflynn/stats/cumulative_sum.go) [data.go](/src/github.com/montanaflynn/stats/data.go) [deviation.go](/src/github.com/montanaflynn/stats/deviation.go) [distances.go](/src/github.com/montanaflynn/stats/distances.go) [doc.go](/src/github.com/montanaflynn/stats/doc.go) [entropy.go](/src/github.com/montanaflynn/stats/entropy.go) [errors.go](/src/github.com/montanaflynn/stats/errors.go) [geometric_distribution.go](/src/github.com/montanaflynn/stats/geometric_distribution.go) [legacy.go](/src/github.com/montanaflynn/stats/legacy.go) [load.go](/src/github.com/montanaflynn/stats/load.go) [max.go](/src/github.com/montanaflynn/stats/max.go) [mean.go](/src/github.com/montanaflynn/stats/mean.go) [median.go](/src/github.com/montanaflynn/stats/median.go) [min.go](/src/github.com/montanaflynn/stats/min.go) [mode.go](/src/github.com/montanaflynn/stats/mode.go) [norm.go](/src/github.com/montanaflynn/stats/norm.go) [outlier.go](/src/github.com/montanaflynn/stats/outlier.go) [percentile.go](/src/github.com/montanaflynn/stats/percentile.go) [quartile.go](/src/github.com/montanaflynn/stats/quartile.go) [ranksum.go](/src/github.com/montanaflynn/stats/ranksum.go) [regression.go](/src/github.com/montanaflynn/stats/regression.go) [round.go](/src/github.com/montanaflynn/stats/round.go) [sample.go](/src/github.com/montanaflynn/stats/sample.go) [sigmoid.go](/src/github.com/montanaflynn/stats/sigmoid.go) [softmax.go](/src/github.com/montanaflynn/stats/softmax.go) [sum.go](/src/github.com/montanaflynn/stats/sum.go) [util.go](/src/github.com/montanaflynn/stats/util.go) [variance.go](/src/github.com/montanaflynn/stats/variance.go)
+[correlation.go](/src/github.com/montanaflynn/stats/correlation.go) [cumulative_sum.go](/src/github.com/montanaflynn/stats/cumulative_sum.go) [data.go](/src/github.com/montanaflynn/stats/data.go) [describe.go](/src/github.com/montanaflynn/stats/describe.go) [deviation.go](/src/github.com/montanaflynn/stats/deviation.go) [distances.go](/src/github.com/montanaflynn/stats/distances.go) [doc.go](/src/github.com/montanaflynn/stats/doc.go) [entropy.go](/src/github.com/montanaflynn/stats/entropy.go) [errors.go](/src/github.com/montanaflynn/stats/errors.go) [geometric_distribution.go](/src/github.com/montanaflynn/stats/geometric_distribution.go) [legacy.go](/src/github.com/montanaflynn/stats/legacy.go) [load.go](/src/github.com/montanaflynn/stats/load.go) [max.go](/src/github.com/montanaflynn/stats/max.go) [mean.go](/src/github.com/montanaflynn/stats/mean.go) [median.go](/src/github.com/montanaflynn/stats/median.go) [min.go](/src/github.com/montanaflynn/stats/min.go) [mode.go](/src/github.com/montanaflynn/stats/mode.go) [norm.go](/src/github.com/montanaflynn/stats/norm.go) [outlier.go](/src/github.com/montanaflynn/stats/outlier.go) [percentile.go](/src/github.com/montanaflynn/stats/percentile.go) [quartile.go](/src/github.com/montanaflynn/stats/quartile.go) [ranksum.go](/src/github.com/montanaflynn/stats/ranksum.go) [regression.go](/src/github.com/montanaflynn/stats/regression.go) [round.go](/src/github.com/montanaflynn/stats/round.go) [sample.go](/src/github.com/montanaflynn/stats/sample.go) [sigmoid.go](/src/github.com/montanaflynn/stats/sigmoid.go) [softmax.go](/src/github.com/montanaflynn/stats/softmax.go) [sum.go](/src/github.com/montanaflynn/stats/sum.go) [util.go](/src/github.com/montanaflynn/stats/util.go) [variance.go](/src/github.com/montanaflynn/stats/variance.go)
@@ -380,7 +384,7 @@ Min finds the lowest number in a set of data
-## func [MinkowskiDistance](/distances.go?s=2152:2256#L75)
+## func [MinkowskiDistance](/distances.go?s=2133:2237#L78)
``` go
func MinkowskiDistance(dataPointX, dataPointY Float64Data, lambda float64) (distance float64, err error)
```
@@ -593,15 +597,28 @@ Pearson calculates the Pearson product-moment correlation coefficient between tw
-## func [Percentile](/percentile.go?s=98:181#L8)
+## func [Percentile](/percentile.go?s=598:681#L20)
``` go
func Percentile(input Float64Data, percent float64) (percentile float64, err error)
```
-Percentile finds the relative standing in a slice of floats
+Percentile finds the relative standing in a slice of floats.
+
+The function uses the Linear Interpolation Between Closest Ranks method
+as recommended by NIST [1] and used by Excel (PERCENTILE), Google Sheets,
+NumPy (default), and other standard tools.
+
+Algorithm (for percent p and sorted data of length n):
+
+
+ 1. Compute the rank: rank = (p / 100) * (n - 1)
+ 2. Split into integer part k and fractional part f
+ 3. Result = data[k] + f * (data[k+1] - data[k])
+
+[1] https://www.itl.nist.gov/div898/handbook/prc/section2/prc262.htm
-## func [PercentileNearestRank](/percentile.go?s=1079:1173#L54)
+## func [PercentileNearestRank](/percentile.go?s=1382:1476#L55)
``` go
func PercentileNearestRank(input Float64Data, percent float64) (percentile float64, err error)
```
@@ -809,6 +826,65 @@ LogReg is a shortcut to LogarithmicRegression
+## type [Description](/describe.go?s=89:349#L6)
+``` go
+type Description struct {
+ Count int
+ Mean float64
+ Std float64
+ Max float64
+ Min float64
+ DescriptionPercentiles []descriptionPercentile
+ AllowedNaN bool
+}
+
+```
+Holds information about the dataset provided to Describe
+
+
+
+
+
+
+
+### func [Describe](/describe.go?s=579:672#L23)
+``` go
+func Describe(input Float64Data, allowNaN bool, percentiles *[]float64) (*Description, error)
+```
+Describe generates descriptive statistics about a provided dataset, similar to python's pandas.describe()
+
+
+### func [DescribePercentileFunc](/describe.go?s=917:1084#L29)
+``` go
+func DescribePercentileFunc(input Float64Data, allowNaN bool, percentiles *[]float64, percentileFunc func(Float64Data, float64) (float64, error)) (*Description, error)
+```
+Describe generates descriptive statistics about a provided dataset, similar to python's pandas.describe()
+Takes in a function to use for percentile calculation
+
+
+
+
+
+### func (\*Description) [String](/describe.go?s=2078:2127#L68)
+``` go
+func (d *Description) String(decimals int) string
+```
+Represents the Description instance in a string format with specified number of decimals
+
+
+ count 3
+ mean 2.00
+ std 0.82
+ max 3.00
+ min 1.00
+ 25.00% NaN
+ 50.00% 1.50
+ 75.00% 2.50
+ NaN OK true
+
+
+
+
## type [Float64Data](/data.go?s=80:106#L4)
``` go
type Float64Data []float64
diff --git a/backend/vendor/github.com/montanaflynn/stats/LICENSE b/backend/vendor/github.com/montanaflynn/stats/LICENSE
index 3162cb1a..5656ce8b 100644
--- a/backend/vendor/github.com/montanaflynn/stats/LICENSE
+++ b/backend/vendor/github.com/montanaflynn/stats/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2014-2023 Montana Flynn (https://montanaflynn.com)
+Copyright (c) 2014-2026 Montana Flynn (https://montanaflynn.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/backend/vendor/github.com/montanaflynn/stats/README.md b/backend/vendor/github.com/montanaflynn/stats/README.md
index 9c188907..1cd4895b 100644
--- a/backend/vendor/github.com/montanaflynn/stats/README.md
+++ b/backend/vendor/github.com/montanaflynn/stats/README.md
@@ -115,9 +115,12 @@ func NormVar(loc float64, scale float64) float64 {}
func Pearson(data1, data2 Float64Data) (float64, error) {}
func Percentile(input Float64Data, percent float64) (percentile float64, err error) {}
func PercentileNearestRank(input Float64Data, percent float64) (percentile float64, err error) {}
+func PopulationSkewness(input Float64Data) (float64, error) {}
func PopulationVariance(input Float64Data) (pvar float64, err error) {}
func Sample(input Float64Data, takenum int, replacement bool) ([]float64, error) {}
+func SampleSkewness(input Float64Data) (float64, error) {}
func SampleVariance(input Float64Data) (svar float64, err error) {}
+func Skewness(input Float64Data) (float64, error) {}
func Sigmoid(input Float64Data) ([]float64, error) {}
func SoftMax(input Float64Data) ([]float64, error) {}
func StableSample(input Float64Data, takenum int) ([]float64, error) {}
@@ -182,35 +185,16 @@ To make things as seamless as possible please also consider the following steps:
## Releasing
-This is not required by contributors and mostly here as a reminder to myself as the maintainer of this repo. To release a new version we should update the [CHANGELOG.md](/CHANGELOG.md) and [DOCUMENTATION.md](/DOCUMENTATION.md).
-
-First install the tools used to generate the markdown files and release:
+Releases are automated with [GoReleaser](https://goreleaser.com/) via GitHub Actions. To create a new release, push a version tag:
```
-go install github.com/davecheney/godoc2md@latest
-go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
-brew tap git-chglog/git-chglog
-brew install gnu-sed hub git-chglog
+git tag v0.x.x
+git push origin v0.x.x
```
-Then you can run these `make` directives:
-
-```
-# Generate DOCUMENTATION.md
-make docs
-```
-
-Then we can create a [CHANGELOG.md](/CHANGELOG.md) a new git tag and a github release:
-
-```
-make release TAG=v0.x.x
-```
-
-To authenticate `hub` for the release you will need to create a personal access token and use it as the password when it's requested.
-
## MIT License
-Copyright (c) 2014-2023 Montana Flynn (https://montanaflynn.com)
+Copyright (c) 2014-2026 Montana Flynn (https://montanaflynn.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/backend/vendor/github.com/montanaflynn/stats/doc.go b/backend/vendor/github.com/montanaflynn/stats/doc.go
index facb8d57..ab339d76 100644
--- a/backend/vendor/github.com/montanaflynn/stats/doc.go
+++ b/backend/vendor/github.com/montanaflynn/stats/doc.go
@@ -18,6 +18,6 @@ Example Usage:
roundedMedian, _ := stats.Round(median, 0)
fmt.Println(roundedMedian) // 4
-MIT License Copyright (c) 2014-2020 Montana Flynn (https://montanaflynn.com)
+MIT License Copyright (c) 2014-2026 Montana Flynn (https://montanaflynn.com)
*/
package stats
diff --git a/backend/vendor/github.com/montanaflynn/stats/percentile.go b/backend/vendor/github.com/montanaflynn/stats/percentile.go
index f5641783..5bb4d7b3 100644
--- a/backend/vendor/github.com/montanaflynn/stats/percentile.go
+++ b/backend/vendor/github.com/montanaflynn/stats/percentile.go
@@ -4,7 +4,19 @@ import (
"math"
)
-// Percentile finds the relative standing in a slice of floats
+// Percentile finds the relative standing in a slice of floats.
+//
+// The function uses the Linear Interpolation Between Closest Ranks method
+// as recommended by NIST [1] and used by Excel (PERCENTILE), Google Sheets,
+// NumPy (default), and other standard tools.
+//
+// Algorithm (for percent p and sorted data of length n):
+//
+// 1. Compute the rank: rank = (p / 100) * (n - 1)
+// 2. Split into integer part k and fractional part f
+// 3. Result = data[k] + f * (data[k+1] - data[k])
+//
+// [1] https://www.itl.nist.gov/div898/handbook/prc/section2/prc262.htm
func Percentile(input Float64Data, percent float64) (percentile float64, err error) {
length := input.Len()
if length == 0 {
@@ -22,28 +34,17 @@ func Percentile(input Float64Data, percent float64) (percentile float64, err err
// Start by sorting a copy of the slice
c := sortedCopy(input)
- // Multiply percent by length of input
- index := (percent / 100) * float64(len(c))
-
- // Check if the index is a whole number
- if index == float64(int64(index)) {
-
- // Convert float to int
- i := int(index)
-
- // Find the value at the index
- percentile = c[i-1]
-
- } else if index > 1 {
-
- // Convert float to int via truncation
- i := int(index)
-
- // Find the average of the index and following values
- percentile, _ = Mean(Float64Data{c[i-1], c[i]})
+ // Use the standard linear interpolation method:
+ // rank = (percent / 100) * (n - 1)
+ // result = c[k] + f * (c[k+1] - c[k])
+ rank := (percent / 100) * float64(length-1)
+ k := int(rank)
+ f := rank - float64(k)
+ if k+1 < length {
+ percentile = c[k] + f*(c[k+1]-c[k])
} else {
- return math.NaN(), BoundsErr
+ percentile = c[k]
}
return percentile, nil
diff --git a/backend/vendor/github.com/montanaflynn/stats/regression.go b/backend/vendor/github.com/montanaflynn/stats/regression.go
index 401d9512..c883cd68 100644
--- a/backend/vendor/github.com/montanaflynn/stats/regression.go
+++ b/backend/vendor/github.com/montanaflynn/stats/regression.go
@@ -18,7 +18,7 @@ func LinearRegression(s Series) (regressions Series, err error) {
}
// Placeholder for the math to be done
- var sum [5]float64
+ var sum [4]float64
// Loop over data keeping index in place
i := 0
@@ -27,7 +27,6 @@ func LinearRegression(s Series) (regressions Series, err error) {
sum[1] += s[i].Y
sum[2] += s[i].X * s[i].X
sum[3] += s[i].X * s[i].Y
- sum[4] += s[i].Y * s[i].Y
}
// Find gradient and intercept
diff --git a/backend/vendor/github.com/montanaflynn/stats/skewness.go b/backend/vendor/github.com/montanaflynn/stats/skewness.go
new file mode 100644
index 00000000..dcbdcea8
--- /dev/null
+++ b/backend/vendor/github.com/montanaflynn/stats/skewness.go
@@ -0,0 +1,62 @@
+package stats
+
+import "math"
+
+// Skewness computes the population skewness of the dataset
+func Skewness(input Float64Data) (float64, error) {
+ return PopulationSkewness(input)
+}
+
+// PopulationSkewness computes the population skewness using the third
+// central moment normalized by the cube of the standard deviation.
+func PopulationSkewness(input Float64Data) (float64, error) {
+ if input.Len() < 2 {
+ return math.NaN(), ErrEmptyInput
+ }
+
+ mean, _ := Mean(input)
+
+ // Compute sum of squared and cubed differences from the mean
+ var sumOfSquares, sumOfCubes float64
+ for _, v := range input {
+ d := v - mean
+ sumOfSquares += d * d
+ sumOfCubes += d * d * d
+ }
+
+ if sumOfSquares == 0 {
+ return math.NaN(), ErrEmptyInput
+ }
+
+ if sumOfCubes == 0 {
+ return 0.0, nil
+ }
+
+ n := float64(input.Len())
+ variance := sumOfSquares / n
+ stdDevCubed := math.Pow(variance, 3.0/2.0)
+
+ return (sumOfCubes / n) / stdDevCubed, nil
+}
+
+// SampleSkewness computes the adjusted Fisher-Pearson standardized moment
+// coefficient, correcting for bias in small samples.
+func SampleSkewness(input Float64Data) (float64, error) {
+ n := input.Len()
+ if n < 3 {
+ return math.NaN(), ErrEmptyInput
+ }
+
+ g1, err := PopulationSkewness(input)
+ if err != nil {
+ return math.NaN(), err
+ }
+
+ if g1 == 0 {
+ return 0.0, nil
+ }
+
+ // Adjusted Fisher-Pearson: G1 = g1 * sqrt(n*(n-1)) / (n-2)
+ nf := float64(n)
+ return g1 * math.Sqrt(nf*(nf-1)) / (nf - 2), nil
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/.gitignore b/backend/vendor/github.com/redis/go-redis/v9/.gitignore
index 00710d50..93affec7 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/.gitignore
+++ b/backend/vendor/github.com/redis/go-redis/v9/.gitignore
@@ -10,6 +10,10 @@ coverage.txt
.vscode
tmp/*
*.test
-
+extra/redisotel-native/metrics-collector-app/
# maintenanceNotifications upgrade documentation (temporary)
maintenanceNotifications/docs/
+
+# Docker-generated files (TLS certificates, cluster data, etc.)
+dockers/*/tls/
+dockers/osscluster-tls/
diff --git a/backend/vendor/github.com/redis/go-redis/v9/.golangci.yml b/backend/vendor/github.com/redis/go-redis/v9/.golangci.yml
index 872454ff..dd13c2c2 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/.golangci.yml
+++ b/backend/vendor/github.com/redis/go-redis/v9/.golangci.yml
@@ -26,6 +26,8 @@ linters:
- builtin$
- examples$
formatters:
+ enable:
+ - gofmt
exclusions:
generated: lax
paths:
diff --git a/backend/vendor/github.com/redis/go-redis/v9/Makefile b/backend/vendor/github.com/redis/go-redis/v9/Makefile
index c2264a4e..370f3880 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/Makefile
+++ b/backend/vendor/github.com/redis/go-redis/v9/Makefile
@@ -1,8 +1,8 @@
GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
-REDIS_VERSION ?= 8.4
+REDIS_VERSION ?= 8.6
RE_CLUSTER ?= false
RCE_DOCKER ?= true
-CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:8.4.0
+CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:custom-21860421418-debian-amd64
docker.start:
export RE_CLUSTER=$(RE_CLUSTER) && \
@@ -14,6 +14,17 @@ docker.start:
docker.stop:
docker compose --profile all down
+docker.e2e.start:
+ @echo "Starting Redis and cae-resp-proxy for E2E tests..."
+ docker compose --profile e2e up -d --quiet-pull
+ @echo "Waiting for services to be ready..."
+ @sleep 3
+ @echo "Services ready!"
+
+docker.e2e.stop:
+ @echo "Stopping E2E services..."
+ docker compose --profile e2e down
+
test:
$(MAKE) docker.start
@if [ -z "$(REDIS_VERSION)" ]; then \
@@ -66,7 +77,31 @@ bench:
export REDIS_VERSION=$(REDIS_VERSION) && \
go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example
-.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt
+test.e2e:
+ @echo "Running E2E tests with auto-start proxy..."
+ $(MAKE) docker.e2e.start
+ @echo "Running tests..."
+ @E2E_SCENARIO_TESTS=true go test -v ./maintnotifications/e2e/ -timeout 30m || ($(MAKE) docker.e2e.stop && exit 1)
+ $(MAKE) docker.e2e.stop
+ @echo "E2E tests completed!"
+
+test.e2e.docker:
+ @echo "Running Docker-compatible E2E tests..."
+ $(MAKE) docker.e2e.start
+ @echo "Running unified injector tests..."
+ @E2E_SCENARIO_TESTS=true go test -v -run "TestUnifiedInjector|TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/ -timeout 10m || ($(MAKE) docker.e2e.stop && exit 1)
+ $(MAKE) docker.e2e.stop
+ @echo "Docker E2E tests completed!"
+
+test.e2e.logic:
+ @echo "Running E2E logic tests (no proxy required)..."
+ @E2E_SCENARIO_TESTS=true \
+ REDIS_ENDPOINTS_CONFIG_PATH=/tmp/test_endpoints_verify.json \
+ FAULT_INJECTION_API_URL=http://localhost:8080 \
+ go test -v -run "TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/
+ @echo "Logic tests completed!"
+
+.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt test.e2e test.e2e.logic docker.e2e.start docker.e2e.stop
build:
export RE_CLUSTER=$(RE_CLUSTER) && \
diff --git a/backend/vendor/github.com/redis/go-redis/v9/README.md b/backend/vendor/github.com/redis/go-redis/v9/README.md
index 38bd17b5..160714ab 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/README.md
+++ b/backend/vendor/github.com/redis/go-redis/v9/README.md
@@ -21,13 +21,12 @@ In `go-redis` we are aiming to support the last three releases of Redis. Current
- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2
- [Redis 8.4](https://raw.githubusercontent.com/redis/redis/8.4/00-RELEASENOTES) - using Redis CE 8.4
-Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three
-versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0),
-[1.24](https://go.dev/doc/devel/release#go1.24.0)). We observe that some modules related test may not pass with
+Although the `go.mod` states it requires at minimum `go 1.21`, our CI is configured to run the tests against all three
+versions of Redis and multiple versions of Go ([1.21](https://go.dev/doc/devel/release#go1.21.0),
+[1.23](https://go.dev/doc/devel/release#go1.23.0), oldstable, and stable). We observe that some modules related test may not pass with
Redis Stack 7.2 and some commands are changed with Redis CE 8.0.
Although it is not officially supported, `go-redis/v9` should be able to work with any Redis 7.0+.
-Please do refer to the documentation and the tests if you experience any issues. We do plan to update the go version
-in the `go.mod` to `go 1.24` in one of the next releases.
+Please do refer to the documentation and the tests if you experience any issues.
## How do I Redis?
@@ -111,6 +110,7 @@ func ExampleClient() {
Password: "", // no password set
DB: 0, // use default DB
})
+ defer rdb.Close()
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md b/backend/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md
index e38ade44..7b705ee6 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md
+++ b/backend/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md
@@ -1,24 +1,178 @@
# Release Notes
-# 9.17.2 (2025-12-01)
+# 9.18.0 (2026-02-16)
+
+## 🚀 Highlights
+
+### Redis 8.6 Support
+
+Added support for Redis 8.6, including new commands and features for streams idempotent production and HOTKEYS.
+
+### Smart Client Handoff (Maintenance Notifications) for Cluster
+
+This release introduces comprehensive support for Redis Cluster maintenance notifications via SMIGRATING/SMIGRATED push notifications. The client now automatically handles slot migrations by:
+- **Relaxing timeouts during migration** (SMIGRATING) to prevent false failures
+- **Triggering lazy cluster state reloads** upon completion (SMIGRATED)
+- Enabling seamless operations during Redis Enterprise maintenance windows
+
+([#3643](https://github.com/redis/go-redis/pull/3643)) by [@ndyakov](https://github.com/ndyakov)
+
+### OpenTelemetry Native Metrics Support
+
+Added comprehensive OpenTelemetry metrics support following the [OpenTelemetry Database Client Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/database/database-metrics/). The implementation uses a Bridge Pattern to keep the core library dependency-free while providing optional metrics instrumentation through the new `extra/redisotel-native` package.
+
+**Metric groups include:**
+- Command metrics: Operation duration with retry tracking
+- Connection basic: Connection count and creation time
+- Resiliency: Errors, handoffs, timeout relaxation
+- Connection advanced: Wait time and use time
+- Pubsub metrics: Published and received messages
+- Stream metrics: Processing duration and maintenance notifications
+
+([#3637](https://github.com/redis/go-redis/pull/3637)) by [@ofekshenawa](https://github.com/ofekshenawa)
+
+## ✨ New Features
+
+- **HOTKEYS Commands**: Added support for Redis HOTKEYS feature for identifying hot keys based on CPU consumption and network utilization ([#3695](https://github.com/redis/go-redis/pull/3695)) by [@ofekshenawa](https://github.com/ofekshenawa)
+- **Streams Idempotent Production**: Added support for Redis 8.6+ Streams Idempotent Production with `ProducerID`, `IdempotentID`, `IdempotentAuto` in `XAddArgs` and new `XCFGSET` command ([#3693](https://github.com/redis/go-redis/pull/3693)) by [@ofekshenawa](https://github.com/ofekshenawa)
+- **NaN Values for TimeSeries**: Added support for NaN (Not a Number) values in Redis time series commands ([#3687](https://github.com/redis/go-redis/pull/3687)) by [@ofekshenawa](https://github.com/ofekshenawa)
+- **DialerRetries Options**: Added `DialerRetries` and `DialerRetryTimeout` to `ClusterOptions`, `RingOptions`, and `FailoverOptions` ([#3686](https://github.com/redis/go-redis/pull/3686)) by [@naveenchander30](https://github.com/naveenchander30)
+- **ConnMaxLifetimeJitter**: Added jitter configuration to distribute connection expiration times and prevent thundering herd ([#3666](https://github.com/redis/go-redis/pull/3666)) by [@cyningsun](https://github.com/cyningsun)
+- **Digest Helper Functions**: Added `DigestString` and `DigestBytes` helper functions for client-side xxh3 hashing compatible with Redis DIGEST command ([#3679](https://github.com/redis/go-redis/pull/3679)) by [@ofekshenawa](https://github.com/ofekshenawa)
+- **SMIGRATED New Format**: Updated SMIGRATED parser to support new format and remember original host:port ([#3697](https://github.com/redis/go-redis/pull/3697)) by [@ndyakov](https://github.com/ndyakov)
+- **Cluster State Reload Interval**: Added cluster state reload interval option for maintenance notifications ([#3663](https://github.com/redis/go-redis/pull/3663)) by [@ndyakov](https://github.com/ndyakov)
## 🐛 Bug Fixes
-- **Connection Pool**: Fixed critical race condition in turn management that could cause connection leaks when dial goroutines complete after request timeout ([#3626](https://github.com/redis/go-redis/pull/3626)) by [@cyningsun](https://github.com/cyningsun)
-- **Context Timeout**: Improved context timeout calculation to use minimum of remaining time and DialTimeout, preventing goroutines from waiting longer than necessary ([#3626](https://github.com/redis/go-redis/pull/3626)) by [@cyningsun](https://github.com/cyningsun)
+- **PubSub nil pointer dereference**: Fixed nil pointer dereference in PubSub after `WithTimeout()` - `pubSubPool` is now properly cloned ([#3710](https://github.com/redis/go-redis/pull/3710)) by [@Copilot](https://github.com/apps/copilot-swe-agent)
+- **MaintNotificationsConfig nil check**: Guard against nil `MaintNotificationsConfig` in `initConn` ([#3707](https://github.com/redis/go-redis/pull/3707)) by [@veeceey](https://github.com/veeceey)
+- **wantConnQueue zombie elements**: Fixed zombie `wantConn` elements accumulation in `wantConnQueue` ([#3680](https://github.com/redis/go-redis/pull/3680)) by [@cyningsun](https://github.com/cyningsun)
+- **XADD/XTRIM approx flag**: Fixed XADD and XTRIM to use `=` when approx is false ([#3684](https://github.com/redis/go-redis/pull/3684)) by [@ndyakov](https://github.com/ndyakov)
+- **Sentinel timeout retry**: When connection to a sentinel times out, attempt to connect to other sentinels ([#3654](https://github.com/redis/go-redis/pull/3654)) by [@cxljs](https://github.com/cxljs)
+
+## ⚡ Performance
+
+- **Fuzz test optimization**: Eliminated repeated string conversions, used functional approach for cleaner operation selection ([#3692](https://github.com/redis/go-redis/pull/3692)) by [@feiguoL](https://github.com/feiguoL)
+- **Pre-allocate capacity**: Pre-allocate slice capacity to prevent multiple capacity expansions ([#3689](https://github.com/redis/go-redis/pull/3689)) by [@feelshu](https://github.com/feelshu)
+
+## 🧪 Testing
+
+- **Comprehensive TLS tests**: Added comprehensive TLS tests and example for standalone, cluster, and certificate authentication ([#3681](https://github.com/redis/go-redis/pull/3681)) by [@ndyakov](https://github.com/ndyakov)
+- **Redis 8.6**: Updated CI to use Redis 8.6-pre ([#3685](https://github.com/redis/go-redis/pull/3685)) by [@ndyakov](https://github.com/ndyakov)
+
+## 🧰 Maintenance
+
+- **Deprecation warnings**: Added deprecation warnings for commands based on Redis documentation ([#3673](https://github.com/redis/go-redis/pull/3673)) by [@ndyakov](https://github.com/ndyakov)
+- **Use errors.Join()**: Replaced custom error join function with standard library `errors.Join()` ([#3653](https://github.com/redis/go-redis/pull/3653)) by [@cxljs](https://github.com/cxljs)
+- **Use Go 1.21 min/max**: Use Go 1.21's built-in min/max functions ([#3656](https://github.com/redis/go-redis/pull/3656)) by [@cxljs](https://github.com/cxljs)
+- **Proper formatting**: Code formatting improvements ([#3670](https://github.com/redis/go-redis/pull/3670)) by [@12ya](https://github.com/12ya)
+- **Set commands documentation**: Added comprehensive documentation to all set command methods ([#3642](https://github.com/redis/go-redis/pull/3642)) by [@iamamirsalehi](https://github.com/iamamirsalehi)
+- **MaxActiveConns docs**: Added default value documentation for `MaxActiveConns` ([#3674](https://github.com/redis/go-redis/pull/3674)) by [@codykaup](https://github.com/codykaup)
+- **README example update**: Updated README example ([#3657](https://github.com/redis/go-redis/pull/3657)) by [@cxljs](https://github.com/cxljs)
+- **Cluster maintnotif example**: Added example application for cluster maintenance notifications ([#3651](https://github.com/redis/go-redis/pull/3651)) by [@ndyakov](https://github.com/ndyakov)
+
+## 👥 Contributors
+
+We'd like to thank all the contributors who worked on this release!
+
+[@12ya](https://github.com/12ya), [@Copilot](https://github.com/apps/copilot-swe-agent), [@codykaup](https://github.com/codykaup), [@cxljs](https://github.com/cxljs), [@cyningsun](https://github.com/cyningsun), [@feelshu](https://github.com/feelshu), [@feiguoL](https://github.com/feiguoL), [@iamamirsalehi](https://github.com/iamamirsalehi), [@naveenchander30](https://github.com/naveenchander30), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@veeceey](https://github.com/veeceey)
+
+---
+
+**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.0...v9.18.0
+
+# 9.18.0-beta.2 (2025-12-09)
+
+## 🚀 Highlights
+
+### Go Version Update
+
+This release updates the minimum required Go version to 1.21. This is part of a gradual migration strategy where the minimum supported Go version will be three versions behind the latest release. With each new Go version release, we will bump the minimum version by one, ensuring compatibility while staying current with the Go ecosystem.
+
+### Stability Improvements
+
+This release includes several important stability fixes:
+- Fixed a critical panic in the handoff worker manager that could occur when handling nil errors
+- Improved test reliability for Smart Client Handoff functionality
+- Fixed logging format issues that could cause runtime errors
+
+## ✨ New Features
+
+- OpenTelemetry metrics improvements for nil response handling ([#3638](https://github.com/redis/go-redis/pull/3638)) by [@fengve](https://github.com/fengve)
+
+## 🐛 Bug Fixes
+
+- Fixed panic on nil error in handoffWorkerManager closeConnFromRequest ([#3633](https://github.com/redis/go-redis/pull/3633)) by [@ccoVeille](https://github.com/ccoVeille)
+- Fixed bad sprintf syntax in logging ([#3632](https://github.com/redis/go-redis/pull/3632)) by [@ccoVeille](https://github.com/ccoVeille)
+
+## 🧰 Maintenance
+
+- Updated minimum Go version to 1.21 ([#3640](https://github.com/redis/go-redis/pull/3640)) by [@ndyakov](https://github.com/ndyakov)
+- Use Go 1.20 idiomatic string<->byte conversion ([#3435](https://github.com/redis/go-redis/pull/3435)) by [@justinhwang](https://github.com/justinhwang)
+- Reduce flakiness of Smart Client Handoff test ([#3641](https://github.com/redis/go-redis/pull/3641)) by [@kiryazovi-redis](https://github.com/kiryazovi-redis)
+- Revert PR #3634 (Observability metrics phase1) ([#3635](https://github.com/redis/go-redis/pull/3635)) by [@ofekshenawa](https://github.com/ofekshenawa)
+
+## 👥 Contributors
+
+We'd like to thank all the contributors who worked on this release!
+
+[@justinhwang](https://github.com/justinhwang), [@ndyakov](https://github.com/ndyakov), [@kiryazovi-redis](https://github.com/kiryazovi-redis), [@fengve](https://github.com/fengve), [@ccoVeille](https://github.com/ccoVeille), [@ofekshenawa](https://github.com/ofekshenawa)
+
+---
+
+**Full Changelog**: https://github.com/redis/go-redis/compare/v9.18.0-beta.1...v9.18.0-beta.2
+
+# 9.18.0-beta.1 (2025-12-01)
+
+## 🚀 Highlights
+
+### Request and Response Policy Based Routing in Cluster Mode
+
+This beta release introduces comprehensive support for Redis COMMAND-based request and response policy routing for cluster clients. This feature enables intelligent command routing and response aggregation based on Redis command metadata.
+
+**Key Features:**
+- **Command Policy Loader**: Automatically parses and caches COMMAND metadata with routing/aggregation hints
+- **Enhanced Routing Engine**: Supports all request policies including:
+ - `default(keyless)` - Commands without keys
+ - `default(hashslot)` - Commands with hash slot routing
+ - `all_shards` - Commands that need to run on all shards
+ - `all_nodes` - Commands that need to run on all nodes
+ - `multi_shard` - Commands that span multiple shards
+ - `special` - Commands with custom routing logic
+- **Response Aggregator**: Intelligently combines multi-shard replies based on response policies:
+ - `all_succeeded` - All shards must succeed
+ - `one_succeeded` - At least one shard must succeed
+ - `agg_sum` - Aggregate numeric responses
+ - `special` - Custom aggregation logic (e.g., FT.CURSOR)
+- **Raw Command Support**: Policies are enforced on `Client.Do(ctx, args...)`
+
+This feature is particularly useful for Redis Stack commands like RediSearch that need to operate across multiple shards in a cluster.
+
+### Connection Pool Improvements
+
+Fixed a critical defect in the connection pool's turn management mechanism that could lead to connection leaks under certain conditions. The fix ensures proper 1:1 correspondence between turns and connections.
+
+## ✨ New Features
+
+- Request and Response Policy Based Routing in Cluster Mode ([#3422](https://github.com/redis/go-redis/pull/3422)) by [@ofekshenawa](https://github.com/ofekshenawa)
+
+## 🐛 Bug Fixes
+
+- Fixed connection pool turn management to prevent connection leaks ([#3626](https://github.com/redis/go-redis/pull/3626)) by [@cyningsun](https://github.com/cyningsun)
## 🧰 Maintenance
- chore(deps): bump rojopolis/spellcheck-github-actions from 0.54.0 to 0.55.0 ([#3627](https://github.com/redis/go-redis/pull/3627))
-## Contributors
+## 👥 Contributors
+
We'd like to thank all the contributors who worked on this release!
-[@cyningsun](https://github.com/cyningsun) and [@ndyakov](https://github.com/ndyakov)
+[@cyningsun](https://github.com/cyningsun), [@ofekshenawa](https://github.com/ofekshenawa), [@ndyakov](https://github.com/ndyakov)
---
-**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.1...v9.17.2
+**Full Changelog**: https://github.com/redis/go-redis/compare/v9.17.1...v9.18.0-beta.1
# 9.17.1 (2025-11-25)
diff --git a/backend/vendor/github.com/redis/go-redis/v9/adapters.go b/backend/vendor/github.com/redis/go-redis/v9/adapters.go
index 4146153b..952a4c26 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/adapters.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/adapters.go
@@ -61,6 +61,13 @@ func (oa *optionsAdapter) GetAddr() string {
return oa.options.Addr
}
+// GetNodeAddress returns the address of the Redis node as reported by the server.
+// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation.
+// For standalone clients, this defaults to Addr.
+func (oa *optionsAdapter) GetNodeAddress() string {
+ return oa.options.NodeAddress
+}
+
// IsTLSEnabled returns true if TLS is enabled.
func (oa *optionsAdapter) IsTLSEnabled() bool {
return oa.options.TLSConfig != nil
diff --git a/backend/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go b/backend/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go
index f4b31983..40076a0b 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/auth/reauth_credentials_listener.go
@@ -44,4 +44,4 @@ func NewReAuthCredentialsListener(reAuth func(credentials Credentials) error, on
}
// Ensure ReAuthCredentialsListener implements the CredentialsListener interface.
-var _ CredentialsListener = (*ReAuthCredentialsListener)(nil)
\ No newline at end of file
+var _ CredentialsListener = (*ReAuthCredentialsListener)(nil)
diff --git a/backend/vendor/github.com/redis/go-redis/v9/cluster_commands.go b/backend/vendor/github.com/redis/go-redis/v9/cluster_commands.go
index 4857b01e..a02683f2 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/cluster_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/cluster_commands.go
@@ -42,6 +42,9 @@ func (c cmdable) ClusterMyID(ctx context.Context) *StringCmd {
return cmd
}
+// ClusterSlots returns the mapping of cluster slots to nodes.
+//
+// Deprecated: Use ClusterShards instead as of Redis 7.0.0.
func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {
cmd := NewClusterSlotsCmd(ctx, "cluster", "slots")
_ = c(ctx, cmd)
@@ -153,6 +156,9 @@ func (c cmdable) ClusterSaveConfig(ctx context.Context) *StatusCmd {
return cmd
}
+// ClusterSlaves lists the replica nodes of a master node.
+//
+// Deprecated: Use ClusterReplicas instead as of Redis 5.0.0.
func (c cmdable) ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "cluster", "slaves", nodeID)
_ = c(ctx, cmd)
diff --git a/backend/vendor/github.com/redis/go-redis/v9/command.go b/backend/vendor/github.com/redis/go-redis/v9/command.go
index 2dbc2ad8..a2a2f051 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/command.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/command.go
@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
+ "maps"
"net"
"regexp"
"strconv"
@@ -14,6 +15,7 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/hscan"
"github.com/redis/go-redis/v9/internal/proto"
+ "github.com/redis/go-redis/v9/internal/routing"
"github.com/redis/go-redis/v9/internal/util"
)
@@ -35,6 +37,7 @@ var keylessCommands = map[string]struct{}{
"failover": {},
"function": {},
"hello": {},
+ "hotkeys": {},
"latency": {},
"lolwut": {},
"module": {},
@@ -67,6 +70,118 @@ var keylessCommands = map[string]struct{}{
"wait": {},
}
+// CmdTyper interface for getting command type
+type CmdTyper interface {
+ GetCmdType() CmdType
+}
+
+// CmdTypeGetter interface for getting command type without circular imports
+type CmdTypeGetter interface {
+ GetCmdType() CmdType
+}
+
+type CmdType uint8
+
+const (
+ CmdTypeGeneric CmdType = iota
+ CmdTypeString
+ CmdTypeInt
+ CmdTypeBool
+ CmdTypeFloat
+ CmdTypeStringSlice
+ CmdTypeIntSlice
+ CmdTypeFloatSlice
+ CmdTypeBoolSlice
+ CmdTypeMapStringString
+ CmdTypeMapStringInt
+ CmdTypeMapStringInterface
+ CmdTypeMapStringInterfaceSlice
+ CmdTypeSlice
+ CmdTypeStatus
+ CmdTypeDuration
+ CmdTypeTime
+ CmdTypeKeyValueSlice
+ CmdTypeStringStructMap
+ CmdTypeXMessageSlice
+ CmdTypeXStreamSlice
+ CmdTypeXPending
+ CmdTypeXPendingExt
+ CmdTypeXAutoClaim
+ CmdTypeXAutoClaimJustID
+ CmdTypeXInfoConsumers
+ CmdTypeXInfoGroups
+ CmdTypeXInfoStream
+ CmdTypeXInfoStreamFull
+ CmdTypeZSlice
+ CmdTypeZWithKey
+ CmdTypeScan
+ CmdTypeClusterSlots
+ CmdTypeGeoLocation
+ CmdTypeGeoSearchLocation
+ CmdTypeGeoPos
+ CmdTypeCommandsInfo
+ CmdTypeSlowLog
+ CmdTypeMapStringStringSlice
+ CmdTypeMapMapStringInterface
+ CmdTypeKeyValues
+ CmdTypeZSliceWithKey
+ CmdTypeFunctionList
+ CmdTypeFunctionStats
+ CmdTypeLCS
+ CmdTypeKeyFlags
+ CmdTypeClusterLinks
+ CmdTypeClusterShards
+ CmdTypeRankWithScore
+ CmdTypeClientInfo
+ CmdTypeACLLog
+ CmdTypeInfo
+ CmdTypeMonitor
+ CmdTypeJSON
+ CmdTypeJSONSlice
+ CmdTypeIntPointerSlice
+ CmdTypeScanDump
+ CmdTypeBFInfo
+ CmdTypeCFInfo
+ CmdTypeCMSInfo
+ CmdTypeTopKInfo
+ CmdTypeTDigestInfo
+ CmdTypeFTSynDump
+ CmdTypeAggregate
+ CmdTypeFTInfo
+ CmdTypeFTSpellCheck
+ CmdTypeFTSearch
+ CmdTypeTSTimestampValue
+ CmdTypeTSTimestampValueSlice
+ CmdTypeHotKeys
+)
+
+type (
+ CmdTypeXAutoClaimValue struct {
+ messages []XMessage
+ start string
+ }
+
+ CmdTypeXAutoClaimJustIDValue struct {
+ ids []string
+ start string
+ }
+
+ CmdTypeScanValue struct {
+ keys []string
+ cursor uint64
+ }
+
+ CmdTypeKeyValuesValue struct {
+ key string
+ values []string
+ }
+
+ CmdTypeZSliceWithKeyValue struct {
+ key string
+ zSlice []Z
+ }
+)
+
type Cmder interface {
// command name.
// e.g. "set k v ex 10" -> "set", "cluster info" -> "cluster".
@@ -84,15 +199,23 @@ type Cmder interface {
// e.g. "set k v ex 10" -> "set k v ex 10: OK", "get k" -> "get k: v".
String() string
+ // Clone creates a copy of the command.
+ Clone() Cmder
+
stringArg(int) string
firstKeyPos() int8
SetFirstKeyPos(int8)
+ stepCount() int8
+ SetStepCount(int8)
readTimeout() *time.Duration
readReply(rd *proto.Reader) error
readRawReply(rd *proto.Reader) error
SetErr(error)
Err() error
+
+ // GetCmdType returns the command type for fast value extraction
+ GetCmdType() CmdType
}
func setCmdsErr(cmds []Cmder, e error) {
@@ -186,8 +309,10 @@ type baseCmd struct {
args []interface{}
err error
keyPos int8
+ _stepCount int8
rawVal interface{}
_readTimeout *time.Duration
+ cmdType CmdType
}
var _ Cmder = (*Cmd)(nil)
@@ -243,6 +368,14 @@ func (cmd *baseCmd) SetFirstKeyPos(keyPos int8) {
cmd.keyPos = keyPos
}
+func (cmd *baseCmd) stepCount() int8 {
+ return cmd._stepCount
+}
+
+func (cmd *baseCmd) SetStepCount(stepCount int8) {
+ cmd._stepCount = stepCount
+}
+
func (cmd *baseCmd) SetErr(e error) {
cmd.err = e
}
@@ -264,6 +397,33 @@ func (cmd *baseCmd) readRawReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *baseCmd) GetCmdType() CmdType {
+ return cmd.cmdType
+}
+
+func (cmd *baseCmd) cloneBaseCmd() baseCmd {
+ var readTimeout *time.Duration
+ if cmd._readTimeout != nil {
+ timeout := *cmd._readTimeout
+ readTimeout = &timeout
+ }
+
+ // Create a copy of args slice
+ args := make([]interface{}, len(cmd.args))
+ copy(args, cmd.args)
+
+ return baseCmd{
+ ctx: cmd.ctx,
+ args: args,
+ err: cmd.err,
+ keyPos: cmd.keyPos,
+ _stepCount: cmd._stepCount,
+ rawVal: cmd.rawVal,
+ _readTimeout: readTimeout,
+ cmdType: cmd.cmdType,
+ }
+}
+
//------------------------------------------------------------------------------
type Cmd struct {
@@ -275,8 +435,9 @@ type Cmd struct {
func NewCmd(ctx context.Context, args ...interface{}) *Cmd {
return &Cmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeGeneric,
},
}
}
@@ -549,6 +710,13 @@ func (cmd *Cmd) readReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *Cmd) Clone() Cmder {
+ return &Cmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
//------------------------------------------------------------------------------
type SliceCmd struct {
@@ -562,8 +730,9 @@ var _ Cmder = (*SliceCmd)(nil)
func NewSliceCmd(ctx context.Context, args ...interface{}) *SliceCmd {
return &SliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeSlice,
},
}
}
@@ -609,6 +778,18 @@ func (cmd *SliceCmd) readReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *SliceCmd) Clone() Cmder {
+ var val []interface{}
+ if cmd.val != nil {
+ val = make([]interface{}, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &SliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type StatusCmd struct {
@@ -622,8 +803,9 @@ var _ Cmder = (*StatusCmd)(nil)
func NewStatusCmd(ctx context.Context, args ...interface{}) *StatusCmd {
return &StatusCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeStatus,
},
}
}
@@ -653,6 +835,13 @@ func (cmd *StatusCmd) readReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *StatusCmd) Clone() Cmder {
+ return &StatusCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
//------------------------------------------------------------------------------
type IntCmd struct {
@@ -666,8 +855,9 @@ var _ Cmder = (*IntCmd)(nil)
func NewIntCmd(ctx context.Context, args ...interface{}) *IntCmd {
return &IntCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeInt,
},
}
}
@@ -697,6 +887,13 @@ func (cmd *IntCmd) readReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *IntCmd) Clone() Cmder {
+ return &IntCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
//------------------------------------------------------------------------------
// DigestCmd is a command that returns a uint64 xxh3 hash digest.
@@ -745,6 +942,13 @@ func (cmd *DigestCmd) String() string {
return cmdString(cmd, cmd.val)
}
+func (cmd *DigestCmd) Clone() Cmder {
+ return &DigestCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
func (cmd *DigestCmd) readReply(rd *proto.Reader) (err error) {
// Redis DIGEST command returns a hex string (e.g., "a1b2c3d4e5f67890")
// We parse it as a uint64 xxh3 hash value
@@ -772,8 +976,9 @@ var _ Cmder = (*IntSliceCmd)(nil)
func NewIntSliceCmd(ctx context.Context, args ...interface{}) *IntSliceCmd {
return &IntSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeIntSlice,
},
}
}
@@ -808,6 +1013,18 @@ func (cmd *IntSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *IntSliceCmd) Clone() Cmder {
+ var val []int64
+ if cmd.val != nil {
+ val = make([]int64, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &IntSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type DurationCmd struct {
@@ -822,8 +1039,9 @@ var _ Cmder = (*DurationCmd)(nil)
func NewDurationCmd(ctx context.Context, precision time.Duration, args ...interface{}) *DurationCmd {
return &DurationCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeDuration,
},
precision: precision,
}
@@ -861,6 +1079,14 @@ func (cmd *DurationCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *DurationCmd) Clone() Cmder {
+ return &DurationCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ precision: cmd.precision,
+ }
+}
+
//------------------------------------------------------------------------------
type TimeCmd struct {
@@ -874,8 +1100,9 @@ var _ Cmder = (*TimeCmd)(nil)
func NewTimeCmd(ctx context.Context, args ...interface{}) *TimeCmd {
return &TimeCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeTime,
},
}
}
@@ -912,6 +1139,13 @@ func (cmd *TimeCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *TimeCmd) Clone() Cmder {
+ return &TimeCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
//------------------------------------------------------------------------------
type BoolCmd struct {
@@ -925,8 +1159,9 @@ var _ Cmder = (*BoolCmd)(nil)
func NewBoolCmd(ctx context.Context, args ...interface{}) *BoolCmd {
return &BoolCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeBool,
},
}
}
@@ -959,6 +1194,13 @@ func (cmd *BoolCmd) readReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *BoolCmd) Clone() Cmder {
+ return &BoolCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
//------------------------------------------------------------------------------
type StringCmd struct {
@@ -972,8 +1214,9 @@ var _ Cmder = (*StringCmd)(nil)
func NewStringCmd(ctx context.Context, args ...interface{}) *StringCmd {
return &StringCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeString,
},
}
}
@@ -1005,28 +1248,28 @@ func (cmd *StringCmd) Int() (int, error) {
if cmd.err != nil {
return 0, cmd.err
}
- return strconv.Atoi(cmd.Val())
+ return strconv.Atoi(cmd.val)
}
func (cmd *StringCmd) Int64() (int64, error) {
if cmd.err != nil {
return 0, cmd.err
}
- return strconv.ParseInt(cmd.Val(), 10, 64)
+ return strconv.ParseInt(cmd.val, 10, 64)
}
func (cmd *StringCmd) Uint64() (uint64, error) {
if cmd.err != nil {
return 0, cmd.err
}
- return strconv.ParseUint(cmd.Val(), 10, 64)
+ return strconv.ParseUint(cmd.val, 10, 64)
}
func (cmd *StringCmd) Float32() (float32, error) {
if cmd.err != nil {
return 0, cmd.err
}
- f, err := strconv.ParseFloat(cmd.Val(), 32)
+ f, err := strconv.ParseFloat(cmd.val, 32)
if err != nil {
return 0, err
}
@@ -1037,14 +1280,14 @@ func (cmd *StringCmd) Float64() (float64, error) {
if cmd.err != nil {
return 0, cmd.err
}
- return strconv.ParseFloat(cmd.Val(), 64)
+ return strconv.ParseFloat(cmd.val, 64)
}
func (cmd *StringCmd) Time() (time.Time, error) {
if cmd.err != nil {
return time.Time{}, cmd.err
}
- return time.Parse(time.RFC3339Nano, cmd.Val())
+ return time.Parse(time.RFC3339Nano, cmd.val)
}
func (cmd *StringCmd) Scan(val interface{}) error {
@@ -1063,6 +1306,13 @@ func (cmd *StringCmd) readReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *StringCmd) Clone() Cmder {
+ return &StringCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
//------------------------------------------------------------------------------
type FloatCmd struct {
@@ -1076,8 +1326,9 @@ var _ Cmder = (*FloatCmd)(nil)
func NewFloatCmd(ctx context.Context, args ...interface{}) *FloatCmd {
return &FloatCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeFloat,
},
}
}
@@ -1103,6 +1354,13 @@ func (cmd *FloatCmd) readReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *FloatCmd) Clone() Cmder {
+ return &FloatCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
//------------------------------------------------------------------------------
type FloatSliceCmd struct {
@@ -1116,8 +1374,9 @@ var _ Cmder = (*FloatSliceCmd)(nil)
func NewFloatSliceCmd(ctx context.Context, args ...interface{}) *FloatSliceCmd {
return &FloatSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeFloatSlice,
},
}
}
@@ -1158,6 +1417,18 @@ func (cmd *FloatSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *FloatSliceCmd) Clone() Cmder {
+ var val []float64
+ if cmd.val != nil {
+ val = make([]float64, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &FloatSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type StringSliceCmd struct {
@@ -1171,8 +1442,9 @@ var _ Cmder = (*StringSliceCmd)(nil)
func NewStringSliceCmd(ctx context.Context, args ...interface{}) *StringSliceCmd {
return &StringSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeStringSlice,
},
}
}
@@ -1194,7 +1466,7 @@ func (cmd *StringSliceCmd) String() string {
}
func (cmd *StringSliceCmd) ScanSlice(container interface{}) error {
- return proto.ScanSlice(cmd.Val(), container)
+ return proto.ScanSlice(cmd.val, container)
}
func (cmd *StringSliceCmd) readReply(rd *proto.Reader) error {
@@ -1216,6 +1488,18 @@ func (cmd *StringSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *StringSliceCmd) Clone() Cmder {
+ var val []string
+ if cmd.val != nil {
+ val = make([]string, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &StringSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type KeyValue struct {
@@ -1234,8 +1518,9 @@ var _ Cmder = (*KeyValueSliceCmd)(nil)
func NewKeyValueSliceCmd(ctx context.Context, args ...interface{}) *KeyValueSliceCmd {
return &KeyValueSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeKeyValueSlice,
},
}
}
@@ -1310,6 +1595,18 @@ func (cmd *KeyValueSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl
return nil
}
+func (cmd *KeyValueSliceCmd) Clone() Cmder {
+ var val []KeyValue
+ if cmd.val != nil {
+ val = make([]KeyValue, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &KeyValueSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type BoolSliceCmd struct {
@@ -1323,8 +1620,9 @@ var _ Cmder = (*BoolSliceCmd)(nil)
func NewBoolSliceCmd(ctx context.Context, args ...interface{}) *BoolSliceCmd {
return &BoolSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeBoolSlice,
},
}
}
@@ -1359,6 +1657,18 @@ func (cmd *BoolSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *BoolSliceCmd) Clone() Cmder {
+ var val []bool
+ if cmd.val != nil {
+ val = make([]bool, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &BoolSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type MapStringStringCmd struct {
@@ -1372,8 +1682,9 @@ var _ Cmder = (*MapStringStringCmd)(nil)
func NewMapStringStringCmd(ctx context.Context, args ...interface{}) *MapStringStringCmd {
return &MapStringStringCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeMapStringString,
},
}
}
@@ -1438,6 +1749,20 @@ func (cmd *MapStringStringCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *MapStringStringCmd) Clone() Cmder {
+ var val map[string]string
+ if cmd.val != nil {
+ val = make(map[string]string, len(cmd.val))
+ for k, v := range cmd.val {
+ val[k] = v
+ }
+ }
+ return &MapStringStringCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type MapStringIntCmd struct {
@@ -1451,8 +1776,9 @@ var _ Cmder = (*MapStringIntCmd)(nil)
func NewMapStringIntCmd(ctx context.Context, args ...interface{}) *MapStringIntCmd {
return &MapStringIntCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeMapStringInt,
},
}
}
@@ -1495,6 +1821,20 @@ func (cmd *MapStringIntCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *MapStringIntCmd) Clone() Cmder {
+ var val map[string]int64
+ if cmd.val != nil {
+ val = make(map[string]int64, len(cmd.val))
+ for k, v := range cmd.val {
+ val[k] = v
+ }
+ }
+ return &MapStringIntCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// ------------------------------------------------------------------------------
type MapStringSliceInterfaceCmd struct {
baseCmd
@@ -1504,8 +1844,9 @@ type MapStringSliceInterfaceCmd struct {
func NewMapStringSliceInterfaceCmd(ctx context.Context, args ...interface{}) *MapStringSliceInterfaceCmd {
return &MapStringSliceInterfaceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeMapStringInterfaceSlice,
},
}
}
@@ -1591,6 +1932,24 @@ func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *MapStringSliceInterfaceCmd) Clone() Cmder {
+ var val map[string][]interface{}
+ if cmd.val != nil {
+ val = make(map[string][]interface{}, len(cmd.val))
+ for k, v := range cmd.val {
+ if v != nil {
+ newSlice := make([]interface{}, len(v))
+ copy(newSlice, v)
+ val[k] = newSlice
+ }
+ }
+ }
+ return &MapStringSliceInterfaceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type StringStructMapCmd struct {
@@ -1604,8 +1963,9 @@ var _ Cmder = (*StringStructMapCmd)(nil)
func NewStringStructMapCmd(ctx context.Context, args ...interface{}) *StringStructMapCmd {
return &StringStructMapCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeStringStructMap,
},
}
}
@@ -1643,6 +2003,17 @@ func (cmd *StringStructMapCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *StringStructMapCmd) Clone() Cmder {
+ var val map[string]struct{}
+ if cmd.val != nil {
+ val = maps.Clone(cmd.val)
+ }
+ return &StringStructMapCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XMessage struct {
@@ -1667,8 +2038,9 @@ var _ Cmder = (*XMessageSliceCmd)(nil)
func NewXMessageSliceCmd(ctx context.Context, args ...interface{}) *XMessageSliceCmd {
return &XMessageSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeXMessageSlice,
},
}
}
@@ -1694,6 +2066,28 @@ func (cmd *XMessageSliceCmd) readReply(rd *proto.Reader) (err error) {
return err
}
+func (cmd *XMessageSliceCmd) Clone() Cmder {
+ var val []XMessage
+ if cmd.val != nil {
+ val = make([]XMessage, len(cmd.val))
+ for i, msg := range cmd.val {
+ val[i] = XMessage{
+ ID: msg.ID,
+ }
+ if msg.Values != nil {
+ val[i].Values = make(map[string]interface{}, len(msg.Values))
+ for k, v := range msg.Values {
+ val[i].Values[k] = v
+ }
+ }
+ }
+ }
+ return &XMessageSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
func readXMessageSlice(rd *proto.Reader) ([]XMessage, error) {
n, err := rd.ReadArrayLen()
if err != nil {
@@ -1793,8 +2187,9 @@ var _ Cmder = (*XStreamSliceCmd)(nil)
func NewXStreamSliceCmd(ctx context.Context, args ...interface{}) *XStreamSliceCmd {
return &XStreamSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeXStreamSlice,
},
}
}
@@ -1847,6 +2242,36 @@ func (cmd *XStreamSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *XStreamSliceCmd) Clone() Cmder {
+ var val []XStream
+ if cmd.val != nil {
+ val = make([]XStream, len(cmd.val))
+ for i, stream := range cmd.val {
+ val[i] = XStream{
+ Stream: stream.Stream,
+ }
+ if stream.Messages != nil {
+ val[i].Messages = make([]XMessage, len(stream.Messages))
+ for j, msg := range stream.Messages {
+ val[i].Messages[j] = XMessage{
+ ID: msg.ID,
+ }
+ if msg.Values != nil {
+ val[i].Messages[j].Values = make(map[string]interface{}, len(msg.Values))
+ for k, v := range msg.Values {
+ val[i].Messages[j].Values[k] = v
+ }
+ }
+ }
+ }
+ }
+ }
+ return &XStreamSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XPending struct {
@@ -1866,8 +2291,9 @@ var _ Cmder = (*XPendingCmd)(nil)
func NewXPendingCmd(ctx context.Context, args ...interface{}) *XPendingCmd {
return &XPendingCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeXPending,
},
}
}
@@ -1930,6 +2356,27 @@ func (cmd *XPendingCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *XPendingCmd) Clone() Cmder {
+ var val *XPending
+ if cmd.val != nil {
+ val = &XPending{
+ Count: cmd.val.Count,
+ Lower: cmd.val.Lower,
+ Higher: cmd.val.Higher,
+ }
+ if cmd.val.Consumers != nil {
+ val.Consumers = make(map[string]int64, len(cmd.val.Consumers))
+ for k, v := range cmd.val.Consumers {
+ val.Consumers[k] = v
+ }
+ }
+ }
+ return &XPendingCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XPendingExt struct {
@@ -1949,8 +2396,9 @@ var _ Cmder = (*XPendingExtCmd)(nil)
func NewXPendingExtCmd(ctx context.Context, args ...interface{}) *XPendingExtCmd {
return &XPendingExtCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeXPendingExt,
},
}
}
@@ -2005,6 +2453,18 @@ func (cmd *XPendingExtCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *XPendingExtCmd) Clone() Cmder {
+ var val []XPendingExt
+ if cmd.val != nil {
+ val = make([]XPendingExt, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &XPendingExtCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XAutoClaimCmd struct {
@@ -2019,8 +2479,9 @@ var _ Cmder = (*XAutoClaimCmd)(nil)
func NewXAutoClaimCmd(ctx context.Context, args ...interface{}) *XAutoClaimCmd {
return &XAutoClaimCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeXAutoClaim,
},
}
}
@@ -2075,6 +2536,29 @@ func (cmd *XAutoClaimCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *XAutoClaimCmd) Clone() Cmder {
+ var val []XMessage
+ if cmd.val != nil {
+ val = make([]XMessage, len(cmd.val))
+ for i, msg := range cmd.val {
+ val[i] = XMessage{
+ ID: msg.ID,
+ }
+ if msg.Values != nil {
+ val[i].Values = make(map[string]interface{}, len(msg.Values))
+ for k, v := range msg.Values {
+ val[i].Values[k] = v
+ }
+ }
+ }
+ }
+ return &XAutoClaimCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ start: cmd.start,
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XAutoClaimJustIDCmd struct {
@@ -2089,8 +2573,9 @@ var _ Cmder = (*XAutoClaimJustIDCmd)(nil)
func NewXAutoClaimJustIDCmd(ctx context.Context, args ...interface{}) *XAutoClaimJustIDCmd {
return &XAutoClaimJustIDCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeXAutoClaimJustID,
},
}
}
@@ -2153,6 +2638,19 @@ func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *XAutoClaimJustIDCmd) Clone() Cmder {
+ var val []string
+ if cmd.val != nil {
+ val = make([]string, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &XAutoClaimJustIDCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ start: cmd.start,
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XInfoConsumersCmd struct {
@@ -2172,8 +2670,9 @@ var _ Cmder = (*XInfoConsumersCmd)(nil)
func NewXInfoConsumersCmd(ctx context.Context, stream string, group string) *XInfoConsumersCmd {
return &XInfoConsumersCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: []interface{}{"xinfo", "consumers", stream, group},
+ ctx: ctx,
+ args: []interface{}{"xinfo", "consumers", stream, group},
+ cmdType: CmdTypeXInfoConsumers,
},
}
}
@@ -2239,6 +2738,18 @@ func (cmd *XInfoConsumersCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *XInfoConsumersCmd) Clone() Cmder {
+ var val []XInfoConsumer
+ if cmd.val != nil {
+ val = make([]XInfoConsumer, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &XInfoConsumersCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XInfoGroupsCmd struct {
@@ -2262,8 +2773,9 @@ var _ Cmder = (*XInfoGroupsCmd)(nil)
func NewXInfoGroupsCmd(ctx context.Context, stream string) *XInfoGroupsCmd {
return &XInfoGroupsCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: []interface{}{"xinfo", "groups", stream},
+ ctx: ctx,
+ args: []interface{}{"xinfo", "groups", stream},
+ cmdType: CmdTypeXInfoGroups,
},
}
}
@@ -2352,6 +2864,18 @@ func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *XInfoGroupsCmd) Clone() Cmder {
+ var val []XInfoGroup
+ if cmd.val != nil {
+ val = make([]XInfoGroup, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &XInfoGroupsCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XInfoStreamCmd struct {
@@ -2370,6 +2894,13 @@ type XInfoStream struct {
FirstEntry XMessage
LastEntry XMessage
RecordedFirstEntryID string
+
+ IDMPDuration int64
+ IDMPMaxSize int64
+ PIDsTracked int64
+ IIDsTracked int64
+ IIDsAdded int64
+ IIDsDuplicates int64
}
var _ Cmder = (*XInfoStreamCmd)(nil)
@@ -2377,8 +2908,9 @@ var _ Cmder = (*XInfoStreamCmd)(nil)
func NewXInfoStreamCmd(ctx context.Context, stream string) *XInfoStreamCmd {
return &XInfoStreamCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: []interface{}{"xinfo", "stream", stream},
+ ctx: ctx,
+ args: []interface{}{"xinfo", "stream", stream},
+ cmdType: CmdTypeXInfoStream,
},
}
}
@@ -2462,6 +2994,36 @@ func (cmd *XInfoStreamCmd) readReply(rd *proto.Reader) error {
if err != nil {
return err
}
+ case "idmp-duration":
+ cmd.val.IDMPDuration, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "idmp-maxsize":
+ cmd.val.IDMPMaxSize, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "pids-tracked":
+ cmd.val.PIDsTracked, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "iids-tracked":
+ cmd.val.IIDsTracked, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "iids-added":
+ cmd.val.IIDsAdded, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "iids-duplicates":
+ cmd.val.IIDsDuplicates, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
default:
return fmt.Errorf("redis: unexpected key %q in XINFO STREAM reply", key)
}
@@ -2469,6 +3031,45 @@ func (cmd *XInfoStreamCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *XInfoStreamCmd) Clone() Cmder {
+ var val *XInfoStream
+ if cmd.val != nil {
+ val = &XInfoStream{
+ Length: cmd.val.Length,
+ RadixTreeKeys: cmd.val.RadixTreeKeys,
+ RadixTreeNodes: cmd.val.RadixTreeNodes,
+ Groups: cmd.val.Groups,
+ LastGeneratedID: cmd.val.LastGeneratedID,
+ MaxDeletedEntryID: cmd.val.MaxDeletedEntryID,
+ EntriesAdded: cmd.val.EntriesAdded,
+ RecordedFirstEntryID: cmd.val.RecordedFirstEntryID,
+ }
+ // Clone XMessage fields
+ val.FirstEntry = XMessage{
+ ID: cmd.val.FirstEntry.ID,
+ }
+ if cmd.val.FirstEntry.Values != nil {
+ val.FirstEntry.Values = make(map[string]interface{}, len(cmd.val.FirstEntry.Values))
+ for k, v := range cmd.val.FirstEntry.Values {
+ val.FirstEntry.Values[k] = v
+ }
+ }
+ val.LastEntry = XMessage{
+ ID: cmd.val.LastEntry.ID,
+ }
+ if cmd.val.LastEntry.Values != nil {
+ val.LastEntry.Values = make(map[string]interface{}, len(cmd.val.LastEntry.Values))
+ for k, v := range cmd.val.LastEntry.Values {
+ val.LastEntry.Values[k] = v
+ }
+ }
+ }
+ return &XInfoStreamCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type XInfoStreamFullCmd struct {
@@ -2486,6 +3087,12 @@ type XInfoStreamFull struct {
Entries []XMessage
Groups []XInfoStreamGroup
RecordedFirstEntryID string
+ IDMPDuration int64
+ IDMPMaxSize int64
+ PIDsTracked int64
+ IIDsTracked int64
+ IIDsAdded int64
+ IIDsDuplicates int64
}
type XInfoStreamGroup struct {
@@ -2524,8 +3131,9 @@ var _ Cmder = (*XInfoStreamFullCmd)(nil)
func NewXInfoStreamFullCmd(ctx context.Context, args ...interface{}) *XInfoStreamFullCmd {
return &XInfoStreamFullCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeXInfoStreamFull,
},
}
}
@@ -2606,6 +3214,36 @@ func (cmd *XInfoStreamFullCmd) readReply(rd *proto.Reader) error {
if err != nil {
return err
}
+ case "idmp-duration":
+ cmd.val.IDMPDuration, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "idmp-maxsize":
+ cmd.val.IDMPMaxSize, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "pids-tracked":
+ cmd.val.PIDsTracked, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "iids-tracked":
+ cmd.val.IIDsTracked, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "iids-added":
+ cmd.val.IIDsAdded, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
+ case "iids-duplicates":
+ cmd.val.IIDsDuplicates, err = rd.ReadInt()
+ if err != nil {
+ return err
+ }
default:
return fmt.Errorf("redis: unexpected key %q in XINFO STREAM FULL reply", key)
}
@@ -2810,6 +3448,45 @@ func readXInfoStreamConsumers(rd *proto.Reader) ([]XInfoStreamConsumer, error) {
return consumers, nil
}
+func (cmd *XInfoStreamFullCmd) Clone() Cmder {
+ var val *XInfoStreamFull
+ if cmd.val != nil {
+ val = &XInfoStreamFull{
+ Length: cmd.val.Length,
+ RadixTreeKeys: cmd.val.RadixTreeKeys,
+ RadixTreeNodes: cmd.val.RadixTreeNodes,
+ LastGeneratedID: cmd.val.LastGeneratedID,
+ MaxDeletedEntryID: cmd.val.MaxDeletedEntryID,
+ EntriesAdded: cmd.val.EntriesAdded,
+ RecordedFirstEntryID: cmd.val.RecordedFirstEntryID,
+ }
+ // Clone Entries
+ if cmd.val.Entries != nil {
+ val.Entries = make([]XMessage, len(cmd.val.Entries))
+ for i, msg := range cmd.val.Entries {
+ val.Entries[i] = XMessage{
+ ID: msg.ID,
+ }
+ if msg.Values != nil {
+ val.Entries[i].Values = make(map[string]interface{}, len(msg.Values))
+ for k, v := range msg.Values {
+ val.Entries[i].Values[k] = v
+ }
+ }
+ }
+ }
+ // Clone Groups - simplified copy for now due to complexity
+ if cmd.val.Groups != nil {
+ val.Groups = make([]XInfoStreamGroup, len(cmd.val.Groups))
+ copy(val.Groups, cmd.val.Groups)
+ }
+ }
+ return &XInfoStreamFullCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type ZSliceCmd struct {
@@ -2823,8 +3500,9 @@ var _ Cmder = (*ZSliceCmd)(nil)
func NewZSliceCmd(ctx context.Context, args ...interface{}) *ZSliceCmd {
return &ZSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeZSlice,
},
}
}
@@ -2888,6 +3566,18 @@ func (cmd *ZSliceCmd) readReply(rd *proto.Reader) error { // nolint:dupl
return nil
}
+func (cmd *ZSliceCmd) Clone() Cmder {
+ var val []Z
+ if cmd.val != nil {
+ val = make([]Z, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &ZSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type ZWithKeyCmd struct {
@@ -2901,8 +3591,9 @@ var _ Cmder = (*ZWithKeyCmd)(nil)
func NewZWithKeyCmd(ctx context.Context, args ...interface{}) *ZWithKeyCmd {
return &ZWithKeyCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeZWithKey,
},
}
}
@@ -2942,6 +3633,23 @@ func (cmd *ZWithKeyCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *ZWithKeyCmd) Clone() Cmder {
+ var val *ZWithKey
+ if cmd.val != nil {
+ val = &ZWithKey{
+ Z: Z{
+ Score: cmd.val.Score,
+ Member: cmd.val.Member,
+ },
+ Key: cmd.val.Key,
+ }
+ }
+ return &ZWithKeyCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type ScanCmd struct {
@@ -2958,8 +3666,9 @@ var _ Cmder = (*ScanCmd)(nil)
func NewScanCmd(ctx context.Context, process cmdable, args ...interface{}) *ScanCmd {
return &ScanCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeScan,
},
process: process,
}
@@ -3007,6 +3716,20 @@ func (cmd *ScanCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *ScanCmd) Clone() Cmder {
+ var page []string
+ if cmd.page != nil {
+ page = make([]string, len(cmd.page))
+ copy(page, cmd.page)
+ }
+ return &ScanCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ page: page,
+ cursor: cmd.cursor,
+ process: cmd.process,
+ }
+}
+
// Iterator creates a new ScanIterator.
func (cmd *ScanCmd) Iterator() *ScanIterator {
return &ScanIterator{
@@ -3039,8 +3762,9 @@ var _ Cmder = (*ClusterSlotsCmd)(nil)
func NewClusterSlotsCmd(ctx context.Context, args ...interface{}) *ClusterSlotsCmd {
return &ClusterSlotsCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeClusterSlots,
},
}
}
@@ -3153,6 +3877,38 @@ func (cmd *ClusterSlotsCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *ClusterSlotsCmd) Clone() Cmder {
+ var val []ClusterSlot
+ if cmd.val != nil {
+ val = make([]ClusterSlot, len(cmd.val))
+ for i, slot := range cmd.val {
+ val[i] = ClusterSlot{
+ Start: slot.Start,
+ End: slot.End,
+ }
+ if slot.Nodes != nil {
+ val[i].Nodes = make([]ClusterNode, len(slot.Nodes))
+ for j, node := range slot.Nodes {
+ val[i].Nodes[j] = ClusterNode{
+ ID: node.ID,
+ Addr: node.Addr,
+ }
+ if node.NetworkingMetadata != nil {
+ val[i].Nodes[j].NetworkingMetadata = make(map[string]string, len(node.NetworkingMetadata))
+ for k, v := range node.NetworkingMetadata {
+ val[i].Nodes[j].NetworkingMetadata[k] = v
+ }
+ }
+ }
+ }
+ }
+ }
+ return &ClusterSlotsCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
// GeoLocation is used with GeoAdd to add geospatial location.
@@ -3192,8 +3948,9 @@ var _ Cmder = (*GeoLocationCmd)(nil)
func NewGeoLocationCmd(ctx context.Context, q *GeoRadiusQuery, args ...interface{}) *GeoLocationCmd {
return &GeoLocationCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: geoLocationArgs(q, args...),
+ ctx: ctx,
+ args: geoLocationArgs(q, args...),
+ cmdType: CmdTypeGeoLocation,
},
q: q,
}
@@ -3301,6 +4058,34 @@ func (cmd *GeoLocationCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *GeoLocationCmd) Clone() Cmder {
+ var q *GeoRadiusQuery
+ if cmd.q != nil {
+ q = &GeoRadiusQuery{
+ Radius: cmd.q.Radius,
+ Unit: cmd.q.Unit,
+ WithCoord: cmd.q.WithCoord,
+ WithDist: cmd.q.WithDist,
+ WithGeoHash: cmd.q.WithGeoHash,
+ Count: cmd.q.Count,
+ Sort: cmd.q.Sort,
+ Store: cmd.q.Store,
+ StoreDist: cmd.q.StoreDist,
+ withLen: cmd.q.withLen,
+ }
+ }
+ var locations []GeoLocation
+ if cmd.locations != nil {
+ locations = make([]GeoLocation, len(cmd.locations))
+ copy(locations, cmd.locations)
+ }
+ return &GeoLocationCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ q: q,
+ locations: locations,
+ }
+}
+
//------------------------------------------------------------------------------
// GeoSearchQuery is used for GEOSearch/GEOSearchStore command query.
@@ -3408,8 +4193,9 @@ func NewGeoSearchLocationCmd(
) *GeoSearchLocationCmd {
return &GeoSearchLocationCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: geoSearchLocationArgs(opt, args),
+ cmdType: CmdTypeGeoSearchLocation,
},
opt: opt,
}
@@ -3482,6 +4268,40 @@ func (cmd *GeoSearchLocationCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *GeoSearchLocationCmd) Clone() Cmder {
+ var opt *GeoSearchLocationQuery
+ if cmd.opt != nil {
+ opt = &GeoSearchLocationQuery{
+ GeoSearchQuery: GeoSearchQuery{
+ Member: cmd.opt.Member,
+ Longitude: cmd.opt.Longitude,
+ Latitude: cmd.opt.Latitude,
+ Radius: cmd.opt.Radius,
+ RadiusUnit: cmd.opt.RadiusUnit,
+ BoxWidth: cmd.opt.BoxWidth,
+ BoxHeight: cmd.opt.BoxHeight,
+ BoxUnit: cmd.opt.BoxUnit,
+ Sort: cmd.opt.Sort,
+ Count: cmd.opt.Count,
+ CountAny: cmd.opt.CountAny,
+ },
+ WithCoord: cmd.opt.WithCoord,
+ WithDist: cmd.opt.WithDist,
+ WithHash: cmd.opt.WithHash,
+ }
+ }
+ var val []GeoLocation
+ if cmd.val != nil {
+ val = make([]GeoLocation, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &GeoSearchLocationCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ opt: opt,
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type GeoPos struct {
@@ -3499,8 +4319,9 @@ var _ Cmder = (*GeoPosCmd)(nil)
func NewGeoPosCmd(ctx context.Context, args ...interface{}) *GeoPosCmd {
return &GeoPosCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeGeoPos,
},
}
}
@@ -3556,17 +4377,37 @@ func (cmd *GeoPosCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *GeoPosCmd) Clone() Cmder {
+ var val []*GeoPos
+ if cmd.val != nil {
+ val = make([]*GeoPos, len(cmd.val))
+ for i, pos := range cmd.val {
+ if pos != nil {
+ val[i] = &GeoPos{
+ Longitude: pos.Longitude,
+ Latitude: pos.Latitude,
+ }
+ }
+ }
+ }
+ return &GeoPosCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type CommandInfo struct {
- Name string
- Arity int8
- Flags []string
- ACLFlags []string
- FirstKeyPos int8
- LastKeyPos int8
- StepCount int8
- ReadOnly bool
+ Name string
+ Arity int8
+ Flags []string
+ ACLFlags []string
+ FirstKeyPos int8
+ LastKeyPos int8
+ StepCount int8
+ ReadOnly bool
+ CommandPolicy *routing.CommandPolicy
}
type CommandsInfoCmd struct {
@@ -3580,8 +4421,9 @@ var _ Cmder = (*CommandsInfoCmd)(nil)
func NewCommandsInfoCmd(ctx context.Context, args ...interface{}) *CommandsInfoCmd {
return &CommandsInfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeCommandsInfo,
},
}
}
@@ -3605,7 +4447,7 @@ func (cmd *CommandsInfoCmd) String() string {
func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error {
const numArgRedis5 = 6
const numArgRedis6 = 7
- const numArgRedis7 = 10
+ const numArgRedis7 = 10 // Also matches redis 8
n, err := rd.ReadArrayLen()
if err != nil {
@@ -3693,9 +4535,33 @@ func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error {
}
if nn >= numArgRedis7 {
- if err := rd.DiscardNext(); err != nil {
+ // The 8th argument is an array of tips.
+ tipsLen, err := rd.ReadArrayLen()
+ if err != nil {
return err
}
+
+ rawTips := make(map[string]string, tipsLen)
+ if cmdInfo.ReadOnly {
+ rawTips[routing.ReadOnlyCMD] = ""
+ }
+ for f := 0; f < tipsLen; f++ {
+ tip, err := rd.ReadString()
+ if err != nil {
+ return err
+ }
+
+ k, v, ok := strings.Cut(tip, ":")
+ if !ok {
+ // Handle tips that don't have a colon (like "nondeterministic_output")
+ rawTips[tip] = ""
+ } else {
+ // Handle normal key:value tips
+ rawTips[k] = v
+ }
+ }
+ cmdInfo.CommandPolicy = parseCommandPolicies(rawTips, cmdInfo.FirstKeyPos)
+
if err := rd.DiscardNext(); err != nil {
return err
}
@@ -3710,13 +4576,47 @@ func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *CommandsInfoCmd) Clone() Cmder {
+ var val map[string]*CommandInfo
+ if cmd.val != nil {
+ val = make(map[string]*CommandInfo, len(cmd.val))
+ for k, v := range cmd.val {
+ if v != nil {
+ newInfo := &CommandInfo{
+ Name: v.Name,
+ Arity: v.Arity,
+ FirstKeyPos: v.FirstKeyPos,
+ LastKeyPos: v.LastKeyPos,
+ StepCount: v.StepCount,
+ ReadOnly: v.ReadOnly,
+ CommandPolicy: v.CommandPolicy, // CommandPolicy can be shared as it's immutable
+ }
+ if v.Flags != nil {
+ newInfo.Flags = make([]string, len(v.Flags))
+ copy(newInfo.Flags, v.Flags)
+ }
+ if v.ACLFlags != nil {
+ newInfo.ACLFlags = make([]string, len(v.ACLFlags))
+ copy(newInfo.ACLFlags, v.ACLFlags)
+ }
+ val[k] = newInfo
+ }
+ }
+ }
+ return &CommandsInfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type cmdsInfoCache struct {
fn func(ctx context.Context) (map[string]*CommandInfo, error)
- once internal.Once
- cmds map[string]*CommandInfo
+ once internal.Once
+ refreshLock sync.Mutex
+ cmds map[string]*CommandInfo
}
func newCmdsInfoCache(fn func(ctx context.Context) (map[string]*CommandInfo, error)) *cmdsInfoCache {
@@ -3726,6 +4626,9 @@ func newCmdsInfoCache(fn func(ctx context.Context) (map[string]*CommandInfo, err
}
func (c *cmdsInfoCache) Get(ctx context.Context) (map[string]*CommandInfo, error) {
+ c.refreshLock.Lock()
+ defer c.refreshLock.Unlock()
+
err := c.once.Do(func() error {
cmds, err := c.fn(ctx)
if err != nil {
@@ -3745,6 +4648,44 @@ func (c *cmdsInfoCache) Get(ctx context.Context) (map[string]*CommandInfo, error
return c.cmds, err
}
+func (c *cmdsInfoCache) Refresh() {
+ c.refreshLock.Lock()
+ defer c.refreshLock.Unlock()
+
+ c.once = internal.Once{}
+}
+
+// ------------------------------------------------------------------------------
+const requestPolicy = "request_policy"
+const responsePolicy = "response_policy"
+
+func parseCommandPolicies(commandInfoTips map[string]string, firstKeyPos int8) *routing.CommandPolicy {
+ req := routing.ReqDefault
+ resp := routing.RespDefaultKeyless
+ if firstKeyPos > 0 {
+ resp = routing.RespDefaultHashSlot
+ }
+
+ tips := make(map[string]string, len(commandInfoTips))
+ for k, v := range commandInfoTips {
+ if k == requestPolicy {
+ if p, err := routing.ParseRequestPolicy(v); err == nil {
+ req = p
+ }
+ continue
+ }
+ if k == responsePolicy {
+ if p, err := routing.ParseResponsePolicy(v); err == nil {
+ resp = p
+ }
+ continue
+ }
+ tips[k] = v
+ }
+
+ return &routing.CommandPolicy{Request: req, Response: resp, Tips: tips}
+}
+
//------------------------------------------------------------------------------
type SlowLog struct {
@@ -3769,8 +4710,9 @@ var _ Cmder = (*SlowLogCmd)(nil)
func NewSlowLogCmd(ctx context.Context, args ...interface{}) *SlowLogCmd {
return &SlowLogCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeSlowLog,
},
}
}
@@ -3855,6 +4797,30 @@ func (cmd *SlowLogCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *SlowLogCmd) Clone() Cmder {
+ var val []SlowLog
+ if cmd.val != nil {
+ val = make([]SlowLog, len(cmd.val))
+ for i, log := range cmd.val {
+ val[i] = SlowLog{
+ ID: log.ID,
+ Time: log.Time,
+ Duration: log.Duration,
+ ClientAddr: log.ClientAddr,
+ ClientName: log.ClientName,
+ }
+ if log.Args != nil {
+ val[i].Args = make([]string, len(log.Args))
+ copy(val[i].Args, log.Args)
+ }
+ }
+ }
+ return &SlowLogCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//-----------------------------------------------------------------------
type Latency struct {
@@ -3932,6 +4898,255 @@ func (cmd *LatencyCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *LatencyCmd) Clone() Cmder {
+ var val []Latency
+ if cmd.val != nil {
+ val = make([]Latency, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &LatencyCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
+//-----------------------------------------------------------------------
+
+// HotKeysSlotRange represents a slot or slot range in the response.
+// Single element slice = individual slot, two element slice = slot range [start, end].
+type HotKeysSlotRange []int64
+
+// HotKeysKeyEntry represents a hot key entry with its metric value.
+type HotKeysKeyEntry struct {
+ Key string
+ Value interface{} // Can be int64 or string
+}
+
+// HotKeysResult represents the response data from HOTKEYS GET command.
+// Field names match the Redis response format.
+type HotKeysResult struct {
+ TrackingActive bool
+ SampleRatio uint8
+ SelectedSlots []HotKeysSlotRange
+ SampledCommandsSelectedSlots time.Duration // Present when sample-ratio > 1 and selected-slots is not empty
+ AllCommandsSelectedSlots time.Duration // Present when selected-slots is not empty
+ AllCommandsAllSlots time.Duration
+ NetBytesSampledCommandsSelectedSlots int64 // Present when sample-ratio > 1 and selected-slots is not empty
+ NetBytesAllCommandsSelectedSlots int64 // Present when selected-slots is not empty
+ NetBytesAllCommandsAllSlots int64
+ CollectionStartTime time.Time
+ CollectionDuration time.Duration
+ UsedCPUSys time.Duration
+ UsedCPUUser time.Duration
+ TotalNetBytes int64
+ ByCPUTime []HotKeysKeyEntry
+ ByNetBytes []HotKeysKeyEntry
+}
+
+type HotKeysCmd struct {
+ baseCmd
+
+ val *HotKeysResult
+}
+
+var _ Cmder = (*HotKeysCmd)(nil)
+
+func NewHotKeysCmd(ctx context.Context, args ...interface{}) *HotKeysCmd {
+ return &HotKeysCmd{
+ baseCmd: baseCmd{
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeHotKeys,
+ },
+ }
+}
+
+func (cmd *HotKeysCmd) SetVal(val *HotKeysResult) {
+ cmd.val = val
+}
+
+func (cmd *HotKeysCmd) Val() *HotKeysResult {
+ return cmd.val
+}
+
+func (cmd *HotKeysCmd) Result() (*HotKeysResult, error) {
+ return cmd.val, cmd.err
+}
+
+func (cmd *HotKeysCmd) String() string {
+ return cmdString(cmd, cmd.val)
+}
+
+func (cmd *HotKeysCmd) readReply(rd *proto.Reader) error {
+ // HOTKEYS GET response is wrapped in an array for aggregation support
+ arrayLen, err := rd.ReadArrayLen()
+ if err != nil {
+ return err
+ }
+
+ if arrayLen == 0 {
+ // Empty array means no tracking was started or after reset
+ cmd.val = nil
+ return nil
+ }
+
+ // Read the first (and typically only) element which is a map
+ n, err := rd.ReadMapLen()
+ if err != nil {
+ return err
+ }
+
+ result := &HotKeysResult{}
+ data := make(map[string]interface{}, n)
+
+ for i := 0; i < n; i++ {
+ k, err := rd.ReadString()
+ if err != nil {
+ return err
+ }
+ v, err := rd.ReadReply()
+ if err != nil {
+ if err == Nil {
+ data[k] = Nil
+ continue
+ }
+ if err, ok := err.(proto.RedisError); ok {
+ data[k] = err
+ continue
+ }
+ return err
+ }
+ data[k] = v
+ }
+
+ if v, ok := data["tracking-active"].(int64); ok {
+ result.TrackingActive = v == 1
+ }
+ if v, ok := data["sample-ratio"].(int64); ok {
+ result.SampleRatio = uint8(v)
+ }
+ if v, ok := data["selected-slots"].([]interface{}); ok {
+ result.SelectedSlots = make([]HotKeysSlotRange, 0, len(v))
+ for _, slot := range v {
+ switch s := slot.(type) {
+ case int64:
+ // Single slot
+ result.SelectedSlots = append(result.SelectedSlots, HotKeysSlotRange{s})
+ case []interface{}:
+ // Slot range
+ slotRange := make(HotKeysSlotRange, 0, len(s))
+ for _, sr := range s {
+ if val, ok := sr.(int64); ok {
+ slotRange = append(slotRange, val)
+ }
+ }
+ result.SelectedSlots = append(result.SelectedSlots, slotRange)
+ }
+ }
+ }
+ if v, ok := data["sampled-commands-selected-slots-us"].(int64); ok {
+ result.SampledCommandsSelectedSlots = time.Duration(v) * time.Microsecond
+ }
+ if v, ok := data["all-commands-selected-slots-us"].(int64); ok {
+ result.AllCommandsSelectedSlots = time.Duration(v) * time.Microsecond
+ }
+ if v, ok := data["all-commands-all-slots-us"].(int64); ok {
+ result.AllCommandsAllSlots = time.Duration(v) * time.Microsecond
+ }
+ if v, ok := data["net-bytes-sampled-commands-selected-slots"].(int64); ok {
+ result.NetBytesSampledCommandsSelectedSlots = v
+ }
+ if v, ok := data["net-bytes-all-commands-selected-slots"].(int64); ok {
+ result.NetBytesAllCommandsSelectedSlots = v
+ }
+ if v, ok := data["net-bytes-all-commands-all-slots"].(int64); ok {
+ result.NetBytesAllCommandsAllSlots = v
+ }
+ if v, ok := data["collection-start-time-unix-ms"].(int64); ok {
+ result.CollectionStartTime = time.UnixMilli(v)
+ }
+ if v, ok := data["collection-duration-ms"].(int64); ok {
+ result.CollectionDuration = time.Duration(v) * time.Millisecond
+ }
+ if v, ok := data["used-cpu-sys-ms"].(int64); ok {
+ result.UsedCPUSys = time.Duration(v) * time.Millisecond
+ }
+ if v, ok := data["used-cpu-user-ms"].(int64); ok {
+ result.UsedCPUUser = time.Duration(v) * time.Millisecond
+ }
+ if v, ok := data["total-net-bytes"].(int64); ok {
+ result.TotalNetBytes = v
+ }
+
+ if v, ok := data["by-cpu-time-us"].([]interface{}); ok {
+ result.ByCPUTime = parseHotKeysKeyEntries(v)
+ }
+
+ if v, ok := data["by-net-bytes"].([]interface{}); ok {
+ result.ByNetBytes = parseHotKeysKeyEntries(v)
+ }
+
+ cmd.val = result
+ return nil
+}
+
+// parseHotKeysKeyEntries parses the key-value pairs from HOTKEYS GET response.
+func parseHotKeysKeyEntries(v []interface{}) []HotKeysKeyEntry {
+ entries := make([]HotKeysKeyEntry, 0, len(v)/2)
+ for i := 0; i < len(v); i += 2 {
+ if i+1 < len(v) {
+ key, keyOk := v[i].(string)
+ if keyOk {
+ entries = append(entries, HotKeysKeyEntry{
+ Key: key,
+ Value: v[i+1], // Can be int64 or string
+ })
+ }
+ }
+ }
+ return entries
+}
+
+func (cmd *HotKeysCmd) Clone() Cmder {
+ var val *HotKeysResult
+ if cmd.val != nil {
+ val = &HotKeysResult{
+ TrackingActive: cmd.val.TrackingActive,
+ SampleRatio: cmd.val.SampleRatio,
+ SampledCommandsSelectedSlots: cmd.val.SampledCommandsSelectedSlots,
+ AllCommandsSelectedSlots: cmd.val.AllCommandsSelectedSlots,
+ AllCommandsAllSlots: cmd.val.AllCommandsAllSlots,
+ NetBytesSampledCommandsSelectedSlots: cmd.val.NetBytesSampledCommandsSelectedSlots,
+ NetBytesAllCommandsSelectedSlots: cmd.val.NetBytesAllCommandsSelectedSlots,
+ NetBytesAllCommandsAllSlots: cmd.val.NetBytesAllCommandsAllSlots,
+ CollectionStartTime: cmd.val.CollectionStartTime,
+ CollectionDuration: cmd.val.CollectionDuration,
+ UsedCPUSys: cmd.val.UsedCPUSys,
+ UsedCPUUser: cmd.val.UsedCPUUser,
+ TotalNetBytes: cmd.val.TotalNetBytes,
+ }
+ if cmd.val.SelectedSlots != nil {
+ val.SelectedSlots = make([]HotKeysSlotRange, len(cmd.val.SelectedSlots))
+ for i, sr := range cmd.val.SelectedSlots {
+ val.SelectedSlots[i] = make(HotKeysSlotRange, len(sr))
+ copy(val.SelectedSlots[i], sr)
+ }
+ }
+ if cmd.val.ByCPUTime != nil {
+ val.ByCPUTime = make([]HotKeysKeyEntry, len(cmd.val.ByCPUTime))
+ copy(val.ByCPUTime, cmd.val.ByCPUTime)
+ }
+ if cmd.val.ByNetBytes != nil {
+ val.ByNetBytes = make([]HotKeysKeyEntry, len(cmd.val.ByNetBytes))
+ copy(val.ByNetBytes, cmd.val.ByNetBytes)
+ }
+ }
+ return &HotKeysCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//-----------------------------------------------------------------------
type MapStringInterfaceCmd struct {
@@ -3945,8 +5160,9 @@ var _ Cmder = (*MapStringInterfaceCmd)(nil)
func NewMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceCmd {
return &MapStringInterfaceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeMapStringInterface,
},
}
}
@@ -3996,6 +5212,20 @@ func (cmd *MapStringInterfaceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *MapStringInterfaceCmd) Clone() Cmder {
+ var val map[string]interface{}
+ if cmd.val != nil {
+ val = make(map[string]interface{}, len(cmd.val))
+ for k, v := range cmd.val {
+ val[k] = v
+ }
+ }
+ return &MapStringInterfaceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//-----------------------------------------------------------------------
type MapStringStringSliceCmd struct {
@@ -4009,8 +5239,9 @@ var _ Cmder = (*MapStringStringSliceCmd)(nil)
func NewMapStringStringSliceCmd(ctx context.Context, args ...interface{}) *MapStringStringSliceCmd {
return &MapStringStringSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeMapStringStringSlice,
},
}
}
@@ -4060,6 +5291,25 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *MapStringStringSliceCmd) Clone() Cmder {
+ var val []map[string]string
+ if cmd.val != nil {
+ val = make([]map[string]string, len(cmd.val))
+ for i, m := range cmd.val {
+ if m != nil {
+ val[i] = make(map[string]string, len(m))
+ for k, v := range m {
+ val[i][k] = v
+ }
+ }
+ }
+ }
+ return &MapStringStringSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// -----------------------------------------------------------------------
// MapMapStringInterfaceCmd represents a command that returns a map of strings to interface{}.
@@ -4071,8 +5321,9 @@ type MapMapStringInterfaceCmd struct {
func NewMapMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapMapStringInterfaceCmd {
return &MapMapStringInterfaceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeMapMapStringInterface,
},
}
}
@@ -4138,6 +5389,20 @@ func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *MapMapStringInterfaceCmd) Clone() Cmder {
+ var val map[string]interface{}
+ if cmd.val != nil {
+ val = make(map[string]interface{}, len(cmd.val))
+ for k, v := range cmd.val {
+ val[k] = v
+ }
+ }
+ return &MapMapStringInterfaceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//-----------------------------------------------------------------------
type MapStringInterfaceSliceCmd struct {
@@ -4151,8 +5416,9 @@ var _ Cmder = (*MapStringInterfaceSliceCmd)(nil)
func NewMapStringInterfaceSliceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceSliceCmd {
return &MapStringInterfaceSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeMapStringInterfaceSlice,
},
}
}
@@ -4203,6 +5469,25 @@ func (cmd *MapStringInterfaceSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *MapStringInterfaceSliceCmd) Clone() Cmder {
+ var val []map[string]interface{}
+ if cmd.val != nil {
+ val = make([]map[string]interface{}, len(cmd.val))
+ for i, m := range cmd.val {
+ if m != nil {
+ val[i] = make(map[string]interface{}, len(m))
+ for k, v := range m {
+ val[i][k] = v
+ }
+ }
+ }
+ }
+ return &MapStringInterfaceSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type KeyValuesCmd struct {
@@ -4217,8 +5502,9 @@ var _ Cmder = (*KeyValuesCmd)(nil)
func NewKeyValuesCmd(ctx context.Context, args ...interface{}) *KeyValuesCmd {
return &KeyValuesCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeKeyValues,
},
}
}
@@ -4265,6 +5551,19 @@ func (cmd *KeyValuesCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *KeyValuesCmd) Clone() Cmder {
+ var val []string
+ if cmd.val != nil {
+ val = make([]string, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &KeyValuesCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ key: cmd.key,
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
type ZSliceWithKeyCmd struct {
@@ -4279,8 +5578,9 @@ var _ Cmder = (*ZSliceWithKeyCmd)(nil)
func NewZSliceWithKeyCmd(ctx context.Context, args ...interface{}) *ZSliceWithKeyCmd {
return &ZSliceWithKeyCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeZSliceWithKey,
},
}
}
@@ -4348,6 +5648,19 @@ func (cmd *ZSliceWithKeyCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *ZSliceWithKeyCmd) Clone() Cmder {
+ var val []Z
+ if cmd.val != nil {
+ val = make([]Z, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &ZSliceWithKeyCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ key: cmd.key,
+ val: val,
+ }
+}
+
type Function struct {
Name string
Description string
@@ -4372,8 +5685,9 @@ var _ Cmder = (*FunctionListCmd)(nil)
func NewFunctionListCmd(ctx context.Context, args ...interface{}) *FunctionListCmd {
return &FunctionListCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeFunctionList,
},
}
}
@@ -4500,6 +5814,37 @@ func (cmd *FunctionListCmd) readFunctions(rd *proto.Reader) ([]Function, error)
return functions, nil
}
+func (cmd *FunctionListCmd) Clone() Cmder {
+ var val []Library
+ if cmd.val != nil {
+ val = make([]Library, len(cmd.val))
+ for i, lib := range cmd.val {
+ val[i] = Library{
+ Name: lib.Name,
+ Engine: lib.Engine,
+ Code: lib.Code,
+ }
+ if lib.Functions != nil {
+ val[i].Functions = make([]Function, len(lib.Functions))
+ for j, fn := range lib.Functions {
+ val[i].Functions[j] = Function{
+ Name: fn.Name,
+ Description: fn.Description,
+ }
+ if fn.Flags != nil {
+ val[i].Functions[j].Flags = make([]string, len(fn.Flags))
+ copy(val[i].Functions[j].Flags, fn.Flags)
+ }
+ }
+ }
+ }
+ }
+ return &FunctionListCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// FunctionStats contains information about the scripts currently executing on the server, and the available engines
// - Engines:
// Statistics about the engine like number of functions and number of libraries
@@ -4553,8 +5898,9 @@ var _ Cmder = (*FunctionStatsCmd)(nil)
func NewFunctionStatsCmd(ctx context.Context, args ...interface{}) *FunctionStatsCmd {
return &FunctionStatsCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeFunctionStats,
},
}
}
@@ -4725,6 +6071,34 @@ func (cmd *FunctionStatsCmd) readRunningScripts(rd *proto.Reader) ([]RunningScri
return runningScripts, len(runningScripts) > 0, nil
}
+func (cmd *FunctionStatsCmd) Clone() Cmder {
+ val := FunctionStats{
+ isRunning: cmd.val.isRunning,
+ rs: cmd.val.rs, // RunningScript is a simple struct, can be copied directly
+ }
+ if cmd.val.Engines != nil {
+ val.Engines = make([]Engine, len(cmd.val.Engines))
+ copy(val.Engines, cmd.val.Engines)
+ }
+ if cmd.val.allrs != nil {
+ val.allrs = make([]RunningScript, len(cmd.val.allrs))
+ for i, rs := range cmd.val.allrs {
+ val.allrs[i] = RunningScript{
+ Name: rs.Name,
+ Duration: rs.Duration,
+ }
+ if rs.Command != nil {
+ val.allrs[i].Command = make([]string, len(rs.Command))
+ copy(val.allrs[i].Command, rs.Command)
+ }
+ }
+ }
+ return &FunctionStatsCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
// LCSQuery is a parameter used for the LCS command
@@ -4788,8 +6162,9 @@ func NewLCSCmd(ctx context.Context, q *LCSQuery) *LCSCmd {
}
}
cmd.baseCmd = baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeLCS,
}
return cmd
@@ -4901,6 +6276,25 @@ func (cmd *LCSCmd) readPosition(rd *proto.Reader) (pos LCSPosition, err error) {
return pos, nil
}
+func (cmd *LCSCmd) Clone() Cmder {
+ var val *LCSMatch
+ if cmd.val != nil {
+ val = &LCSMatch{
+ MatchString: cmd.val.MatchString,
+ Len: cmd.val.Len,
+ }
+ if cmd.val.Matches != nil {
+ val.Matches = make([]LCSMatchedPosition, len(cmd.val.Matches))
+ copy(val.Matches, cmd.val.Matches)
+ }
+ }
+ return &LCSCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ readType: cmd.readType,
+ val: val,
+ }
+}
+
// ------------------------------------------------------------------------
type KeyFlags struct {
@@ -4919,8 +6313,9 @@ var _ Cmder = (*KeyFlagsCmd)(nil)
func NewKeyFlagsCmd(ctx context.Context, args ...interface{}) *KeyFlagsCmd {
return &KeyFlagsCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeKeyFlags,
},
}
}
@@ -4979,6 +6374,26 @@ func (cmd *KeyFlagsCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *KeyFlagsCmd) Clone() Cmder {
+ var val []KeyFlags
+ if cmd.val != nil {
+ val = make([]KeyFlags, len(cmd.val))
+ for i, kf := range cmd.val {
+ val[i] = KeyFlags{
+ Key: kf.Key,
+ }
+ if kf.Flags != nil {
+ val[i].Flags = make([]string, len(kf.Flags))
+ copy(val[i].Flags, kf.Flags)
+ }
+ }
+ }
+ return &KeyFlagsCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// ---------------------------------------------------------------------------------------------------
type ClusterLink struct {
@@ -5001,8 +6416,9 @@ var _ Cmder = (*ClusterLinksCmd)(nil)
func NewClusterLinksCmd(ctx context.Context, args ...interface{}) *ClusterLinksCmd {
return &ClusterLinksCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeClusterLinks,
},
}
}
@@ -5068,6 +6484,18 @@ func (cmd *ClusterLinksCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *ClusterLinksCmd) Clone() Cmder {
+ var val []ClusterLink
+ if cmd.val != nil {
+ val = make([]ClusterLink, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &ClusterLinksCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// ------------------------------------------------------------------------------------------------------------------
type SlotRange struct {
@@ -5103,8 +6531,9 @@ var _ Cmder = (*ClusterShardsCmd)(nil)
func NewClusterShardsCmd(ctx context.Context, args ...interface{}) *ClusterShardsCmd {
return &ClusterShardsCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeClusterShards,
},
}
}
@@ -5218,6 +6647,28 @@ func (cmd *ClusterShardsCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *ClusterShardsCmd) Clone() Cmder {
+ var val []ClusterShard
+ if cmd.val != nil {
+ val = make([]ClusterShard, len(cmd.val))
+ for i, shard := range cmd.val {
+ val[i] = ClusterShard{}
+ if shard.Slots != nil {
+ val[i].Slots = make([]SlotRange, len(shard.Slots))
+ copy(val[i].Slots, shard.Slots)
+ }
+ if shard.Nodes != nil {
+ val[i].Nodes = make([]Node, len(shard.Nodes))
+ copy(val[i].Nodes, shard.Nodes)
+ }
+ }
+ }
+ return &ClusterShardsCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// -----------------------------------------
type RankScore struct {
@@ -5236,8 +6687,9 @@ var _ Cmder = (*RankWithScoreCmd)(nil)
func NewRankWithScoreCmd(ctx context.Context, args ...interface{}) *RankWithScoreCmd {
return &RankWithScoreCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeRankWithScore,
},
}
}
@@ -5278,6 +6730,13 @@ func (cmd *RankWithScoreCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *RankWithScoreCmd) Clone() Cmder {
+ return &RankWithScoreCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val, // RankScore is a simple struct, can be copied directly
+ }
+}
+
// --------------------------------------------------------------------------------------------------
// ClientFlags is redis-server client flags, copy from redis/src/server.h (redis 7.0)
@@ -5387,8 +6846,9 @@ var _ Cmder = (*ClientInfoCmd)(nil)
func NewClientInfoCmd(ctx context.Context, args ...interface{}) *ClientInfoCmd {
return &ClientInfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeClientInfo,
},
}
}
@@ -5565,6 +7025,50 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) {
return info, nil
}
+func (cmd *ClientInfoCmd) Clone() Cmder {
+ var val *ClientInfo
+ if cmd.val != nil {
+ val = &ClientInfo{
+ ID: cmd.val.ID,
+ Addr: cmd.val.Addr,
+ LAddr: cmd.val.LAddr,
+ FD: cmd.val.FD,
+ Name: cmd.val.Name,
+ Age: cmd.val.Age,
+ Idle: cmd.val.Idle,
+ Flags: cmd.val.Flags,
+ DB: cmd.val.DB,
+ Sub: cmd.val.Sub,
+ PSub: cmd.val.PSub,
+ SSub: cmd.val.SSub,
+ Multi: cmd.val.Multi,
+ Watch: cmd.val.Watch,
+ QueryBuf: cmd.val.QueryBuf,
+ QueryBufFree: cmd.val.QueryBufFree,
+ ArgvMem: cmd.val.ArgvMem,
+ MultiMem: cmd.val.MultiMem,
+ BufferSize: cmd.val.BufferSize,
+ BufferPeak: cmd.val.BufferPeak,
+ OutputBufferLength: cmd.val.OutputBufferLength,
+ OutputListLength: cmd.val.OutputListLength,
+ OutputMemory: cmd.val.OutputMemory,
+ TotalMemory: cmd.val.TotalMemory,
+ IoThread: cmd.val.IoThread,
+ Events: cmd.val.Events,
+ LastCmd: cmd.val.LastCmd,
+ User: cmd.val.User,
+ Redir: cmd.val.Redir,
+ Resp: cmd.val.Resp,
+ LibName: cmd.val.LibName,
+ LibVer: cmd.val.LibVer,
+ }
+ }
+ return &ClientInfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// -------------------------------------------
type ACLLogEntry struct {
@@ -5591,8 +7095,9 @@ var _ Cmder = (*ACLLogCmd)(nil)
func NewACLLogCmd(ctx context.Context, args ...interface{}) *ACLLogCmd {
return &ACLLogCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeACLLog,
},
}
}
@@ -5674,6 +7179,69 @@ func (cmd *ACLLogCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *ACLLogCmd) Clone() Cmder {
+ var val []*ACLLogEntry
+ if cmd.val != nil {
+ val = make([]*ACLLogEntry, len(cmd.val))
+ for i, entry := range cmd.val {
+ if entry != nil {
+ val[i] = &ACLLogEntry{
+ Count: entry.Count,
+ Reason: entry.Reason,
+ Context: entry.Context,
+ Object: entry.Object,
+ Username: entry.Username,
+ AgeSeconds: entry.AgeSeconds,
+ EntryID: entry.EntryID,
+ TimestampCreated: entry.TimestampCreated,
+ TimestampLastUpdated: entry.TimestampLastUpdated,
+ }
+ // Clone ClientInfo if present
+ if entry.ClientInfo != nil {
+ val[i].ClientInfo = &ClientInfo{
+ ID: entry.ClientInfo.ID,
+ Addr: entry.ClientInfo.Addr,
+ LAddr: entry.ClientInfo.LAddr,
+ FD: entry.ClientInfo.FD,
+ Name: entry.ClientInfo.Name,
+ Age: entry.ClientInfo.Age,
+ Idle: entry.ClientInfo.Idle,
+ Flags: entry.ClientInfo.Flags,
+ DB: entry.ClientInfo.DB,
+ Sub: entry.ClientInfo.Sub,
+ PSub: entry.ClientInfo.PSub,
+ SSub: entry.ClientInfo.SSub,
+ Multi: entry.ClientInfo.Multi,
+ Watch: entry.ClientInfo.Watch,
+ QueryBuf: entry.ClientInfo.QueryBuf,
+ QueryBufFree: entry.ClientInfo.QueryBufFree,
+ ArgvMem: entry.ClientInfo.ArgvMem,
+ MultiMem: entry.ClientInfo.MultiMem,
+ BufferSize: entry.ClientInfo.BufferSize,
+ BufferPeak: entry.ClientInfo.BufferPeak,
+ OutputBufferLength: entry.ClientInfo.OutputBufferLength,
+ OutputListLength: entry.ClientInfo.OutputListLength,
+ OutputMemory: entry.ClientInfo.OutputMemory,
+ TotalMemory: entry.ClientInfo.TotalMemory,
+ IoThread: entry.ClientInfo.IoThread,
+ Events: entry.ClientInfo.Events,
+ LastCmd: entry.ClientInfo.LastCmd,
+ User: entry.ClientInfo.User,
+ Redir: entry.ClientInfo.Redir,
+ Resp: entry.ClientInfo.Resp,
+ LibName: entry.ClientInfo.LibName,
+ LibVer: entry.ClientInfo.LibVer,
+ }
+ }
+ }
+ }
+ }
+ return &ACLLogCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// LibraryInfo holds the library info.
type LibraryInfo struct {
LibName *string
@@ -5702,8 +7270,9 @@ var _ Cmder = (*InfoCmd)(nil)
func NewInfoCmd(ctx context.Context, args ...interface{}) *InfoCmd {
return &InfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeInfo,
},
}
}
@@ -5769,6 +7338,25 @@ func (cmd *InfoCmd) Item(section, key string) string {
}
}
+func (cmd *InfoCmd) Clone() Cmder {
+ var val map[string]map[string]string
+ if cmd.val != nil {
+ val = make(map[string]map[string]string, len(cmd.val))
+ for section, sectionMap := range cmd.val {
+ if sectionMap != nil {
+ val[section] = make(map[string]string, len(sectionMap))
+ for k, v := range sectionMap {
+ val[section][k] = v
+ }
+ }
+ }
+ }
+ return &InfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
type MonitorStatus int
const (
@@ -5787,8 +7375,9 @@ type MonitorCmd struct {
func newMonitorCmd(ctx context.Context, ch chan string) *MonitorCmd {
return &MonitorCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: []interface{}{"monitor"},
+ ctx: ctx,
+ args: []interface{}{"monitor"},
+ cmdType: CmdTypeMonitor,
},
ch: ch,
status: monitorStatusIdle,
@@ -5907,5 +7496,532 @@ func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error {
}
cmd.val[i].Score = score
}
+
return nil
}
+
+func (cmd *VectorScoreSliceCmd) Clone() Cmder {
+ return &VectorScoreSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ }
+}
+
+func (cmd *MonitorCmd) Clone() Cmder {
+ // MonitorCmd cannot be safely cloned due to channels and goroutines
+ // Return a new MonitorCmd with the same channel
+ return newMonitorCmd(cmd.ctx, cmd.ch)
+}
+
+// ExtractCommandValue extracts the value from a command result using the fast enum-based approach
+func ExtractCommandValue(cmd interface{}) (interface{}, error) {
+ // First try to get the command type using the interface
+ if cmdTypeGetter, ok := cmd.(CmdTypeGetter); ok {
+ cmdType := cmdTypeGetter.GetCmdType()
+
+ // Use fast type-based extraction
+ switch cmdType {
+ case CmdTypeGeneric:
+ if genericCmd, ok := cmd.(interface {
+ Val() interface{}
+ Err() error
+ }); ok {
+ return genericCmd.Val(), genericCmd.Err()
+ }
+ case CmdTypeString:
+ if stringCmd, ok := cmd.(interface {
+ Val() string
+ Err() error
+ }); ok {
+ return stringCmd.Val(), stringCmd.Err()
+ }
+ case CmdTypeInt:
+ if intCmd, ok := cmd.(interface {
+ Val() int64
+ Err() error
+ }); ok {
+ return intCmd.Val(), intCmd.Err()
+ }
+ case CmdTypeBool:
+ if boolCmd, ok := cmd.(interface {
+ Val() bool
+ Err() error
+ }); ok {
+ return boolCmd.Val(), boolCmd.Err()
+ }
+ case CmdTypeFloat:
+ if floatCmd, ok := cmd.(interface {
+ Val() float64
+ Err() error
+ }); ok {
+ return floatCmd.Val(), floatCmd.Err()
+ }
+ case CmdTypeStatus:
+ if statusCmd, ok := cmd.(interface {
+ Val() string
+ Err() error
+ }); ok {
+ return statusCmd.Val(), statusCmd.Err()
+ }
+ case CmdTypeDuration:
+ if durationCmd, ok := cmd.(interface {
+ Val() time.Duration
+ Err() error
+ }); ok {
+ return durationCmd.Val(), durationCmd.Err()
+ }
+ case CmdTypeTime:
+ if timeCmd, ok := cmd.(interface {
+ Val() time.Time
+ Err() error
+ }); ok {
+ return timeCmd.Val(), timeCmd.Err()
+ }
+ case CmdTypeStringStructMap:
+ if structMapCmd, ok := cmd.(interface {
+ Val() map[string]struct{}
+ Err() error
+ }); ok {
+ return structMapCmd.Val(), structMapCmd.Err()
+ }
+ case CmdTypeXMessageSlice:
+ if xMessageSliceCmd, ok := cmd.(interface {
+ Val() []XMessage
+ Err() error
+ }); ok {
+ return xMessageSliceCmd.Val(), xMessageSliceCmd.Err()
+ }
+ case CmdTypeXStreamSlice:
+ if xStreamSliceCmd, ok := cmd.(interface {
+ Val() []XStream
+ Err() error
+ }); ok {
+ return xStreamSliceCmd.Val(), xStreamSliceCmd.Err()
+ }
+ case CmdTypeXPending:
+ if xPendingCmd, ok := cmd.(interface {
+ Val() *XPending
+ Err() error
+ }); ok {
+ return xPendingCmd.Val(), xPendingCmd.Err()
+ }
+ case CmdTypeXPendingExt:
+ if xPendingExtCmd, ok := cmd.(interface {
+ Val() []XPendingExt
+ Err() error
+ }); ok {
+ return xPendingExtCmd.Val(), xPendingExtCmd.Err()
+ }
+ case CmdTypeXAutoClaim:
+ if xAutoClaimCmd, ok := cmd.(interface {
+ Val() ([]XMessage, string)
+ Err() error
+ }); ok {
+ messages, start := xAutoClaimCmd.Val()
+ return CmdTypeXAutoClaimValue{messages: messages, start: start}, xAutoClaimCmd.Err()
+ }
+ case CmdTypeXAutoClaimJustID:
+ if xAutoClaimJustIDCmd, ok := cmd.(interface {
+ Val() ([]string, string)
+ Err() error
+ }); ok {
+ ids, start := xAutoClaimJustIDCmd.Val()
+ return CmdTypeXAutoClaimJustIDValue{ids: ids, start: start}, xAutoClaimJustIDCmd.Err()
+ }
+ case CmdTypeXInfoConsumers:
+ if xInfoConsumersCmd, ok := cmd.(interface {
+ Val() []XInfoConsumer
+ Err() error
+ }); ok {
+ return xInfoConsumersCmd.Val(), xInfoConsumersCmd.Err()
+ }
+ case CmdTypeXInfoGroups:
+ if xInfoGroupsCmd, ok := cmd.(interface {
+ Val() []XInfoGroup
+ Err() error
+ }); ok {
+ return xInfoGroupsCmd.Val(), xInfoGroupsCmd.Err()
+ }
+ case CmdTypeXInfoStream:
+ if xInfoStreamCmd, ok := cmd.(interface {
+ Val() *XInfoStream
+ Err() error
+ }); ok {
+ return xInfoStreamCmd.Val(), xInfoStreamCmd.Err()
+ }
+ case CmdTypeXInfoStreamFull:
+ if xInfoStreamFullCmd, ok := cmd.(interface {
+ Val() *XInfoStreamFull
+ Err() error
+ }); ok {
+ return xInfoStreamFullCmd.Val(), xInfoStreamFullCmd.Err()
+ }
+ case CmdTypeZSlice:
+ if zSliceCmd, ok := cmd.(interface {
+ Val() []Z
+ Err() error
+ }); ok {
+ return zSliceCmd.Val(), zSliceCmd.Err()
+ }
+ case CmdTypeZWithKey:
+ if zWithKeyCmd, ok := cmd.(interface {
+ Val() *ZWithKey
+ Err() error
+ }); ok {
+ return zWithKeyCmd.Val(), zWithKeyCmd.Err()
+ }
+ case CmdTypeScan:
+ if scanCmd, ok := cmd.(interface {
+ Val() ([]string, uint64)
+ Err() error
+ }); ok {
+ keys, cursor := scanCmd.Val()
+ return CmdTypeScanValue{keys: keys, cursor: cursor}, scanCmd.Err()
+ }
+ case CmdTypeClusterSlots:
+ if clusterSlotsCmd, ok := cmd.(interface {
+ Val() []ClusterSlot
+ Err() error
+ }); ok {
+ return clusterSlotsCmd.Val(), clusterSlotsCmd.Err()
+ }
+ case CmdTypeGeoLocation:
+ if geoLocationCmd, ok := cmd.(interface {
+ Val() []GeoLocation
+ Err() error
+ }); ok {
+ return geoLocationCmd.Val(), geoLocationCmd.Err()
+ }
+ case CmdTypeGeoSearchLocation:
+ if geoSearchLocationCmd, ok := cmd.(interface {
+ Val() []GeoLocation
+ Err() error
+ }); ok {
+ return geoSearchLocationCmd.Val(), geoSearchLocationCmd.Err()
+ }
+ case CmdTypeGeoPos:
+ if geoPosCmd, ok := cmd.(interface {
+ Val() []*GeoPos
+ Err() error
+ }); ok {
+ return geoPosCmd.Val(), geoPosCmd.Err()
+ }
+ case CmdTypeCommandsInfo:
+ if commandsInfoCmd, ok := cmd.(interface {
+ Val() map[string]*CommandInfo
+ Err() error
+ }); ok {
+ return commandsInfoCmd.Val(), commandsInfoCmd.Err()
+ }
+ case CmdTypeSlowLog:
+ if slowLogCmd, ok := cmd.(interface {
+ Val() []SlowLog
+ Err() error
+ }); ok {
+ return slowLogCmd.Val(), slowLogCmd.Err()
+ }
+ case CmdTypeHotKeys:
+ if hotKeysCmd, ok := cmd.(interface {
+ Val() *HotKeysResult
+ Err() error
+ }); ok {
+ return hotKeysCmd.Val(), hotKeysCmd.Err()
+ }
+ case CmdTypeKeyValues:
+ if keyValuesCmd, ok := cmd.(interface {
+ Val() (string, []string)
+ Err() error
+ }); ok {
+ key, values := keyValuesCmd.Val()
+ return CmdTypeKeyValuesValue{key: key, values: values}, keyValuesCmd.Err()
+ }
+ case CmdTypeZSliceWithKey:
+ if zSliceWithKeyCmd, ok := cmd.(interface {
+ Val() (string, []Z)
+ Err() error
+ }); ok {
+ key, zSlice := zSliceWithKeyCmd.Val()
+ return CmdTypeZSliceWithKeyValue{key: key, zSlice: zSlice}, zSliceWithKeyCmd.Err()
+ }
+ case CmdTypeFunctionList:
+ if functionListCmd, ok := cmd.(interface {
+ Val() []Library
+ Err() error
+ }); ok {
+ return functionListCmd.Val(), functionListCmd.Err()
+ }
+ case CmdTypeFunctionStats:
+ if functionStatsCmd, ok := cmd.(interface {
+ Val() FunctionStats
+ Err() error
+ }); ok {
+ return functionStatsCmd.Val(), functionStatsCmd.Err()
+ }
+ case CmdTypeLCS:
+ if lcsCmd, ok := cmd.(interface {
+ Val() *LCSMatch
+ Err() error
+ }); ok {
+ return lcsCmd.Val(), lcsCmd.Err()
+ }
+ case CmdTypeKeyFlags:
+ if keyFlagsCmd, ok := cmd.(interface {
+ Val() []KeyFlags
+ Err() error
+ }); ok {
+ return keyFlagsCmd.Val(), keyFlagsCmd.Err()
+ }
+ case CmdTypeClusterLinks:
+ if clusterLinksCmd, ok := cmd.(interface {
+ Val() []ClusterLink
+ Err() error
+ }); ok {
+ return clusterLinksCmd.Val(), clusterLinksCmd.Err()
+ }
+ case CmdTypeClusterShards:
+ if clusterShardsCmd, ok := cmd.(interface {
+ Val() []ClusterShard
+ Err() error
+ }); ok {
+ return clusterShardsCmd.Val(), clusterShardsCmd.Err()
+ }
+ case CmdTypeRankWithScore:
+ if rankWithScoreCmd, ok := cmd.(interface {
+ Val() RankScore
+ Err() error
+ }); ok {
+ return rankWithScoreCmd.Val(), rankWithScoreCmd.Err()
+ }
+ case CmdTypeClientInfo:
+ if clientInfoCmd, ok := cmd.(interface {
+ Val() *ClientInfo
+ Err() error
+ }); ok {
+ return clientInfoCmd.Val(), clientInfoCmd.Err()
+ }
+ case CmdTypeACLLog:
+ if aclLogCmd, ok := cmd.(interface {
+ Val() []*ACLLogEntry
+ Err() error
+ }); ok {
+ return aclLogCmd.Val(), aclLogCmd.Err()
+ }
+ case CmdTypeInfo:
+ if infoCmd, ok := cmd.(interface {
+ Val() string
+ Err() error
+ }); ok {
+ return infoCmd.Val(), infoCmd.Err()
+ }
+ case CmdTypeMonitor:
+ if monitorCmd, ok := cmd.(interface {
+ Val() string
+ Err() error
+ }); ok {
+ return monitorCmd.Val(), monitorCmd.Err()
+ }
+ case CmdTypeJSON:
+ if jsonCmd, ok := cmd.(interface {
+ Val() string
+ Err() error
+ }); ok {
+ return jsonCmd.Val(), jsonCmd.Err()
+ }
+ case CmdTypeJSONSlice:
+ if jsonSliceCmd, ok := cmd.(interface {
+ Val() []interface{}
+ Err() error
+ }); ok {
+ return jsonSliceCmd.Val(), jsonSliceCmd.Err()
+ }
+ case CmdTypeIntPointerSlice:
+ if intPointerSliceCmd, ok := cmd.(interface {
+ Val() []*int64
+ Err() error
+ }); ok {
+ return intPointerSliceCmd.Val(), intPointerSliceCmd.Err()
+ }
+ case CmdTypeScanDump:
+ if scanDumpCmd, ok := cmd.(interface {
+ Val() ScanDump
+ Err() error
+ }); ok {
+ return scanDumpCmd.Val(), scanDumpCmd.Err()
+ }
+ case CmdTypeBFInfo:
+ if bfInfoCmd, ok := cmd.(interface {
+ Val() BFInfo
+ Err() error
+ }); ok {
+ return bfInfoCmd.Val(), bfInfoCmd.Err()
+ }
+ case CmdTypeCFInfo:
+ if cfInfoCmd, ok := cmd.(interface {
+ Val() CFInfo
+ Err() error
+ }); ok {
+ return cfInfoCmd.Val(), cfInfoCmd.Err()
+ }
+ case CmdTypeCMSInfo:
+ if cmsInfoCmd, ok := cmd.(interface {
+ Val() CMSInfo
+ Err() error
+ }); ok {
+ return cmsInfoCmd.Val(), cmsInfoCmd.Err()
+ }
+ case CmdTypeTopKInfo:
+ if topKInfoCmd, ok := cmd.(interface {
+ Val() TopKInfo
+ Err() error
+ }); ok {
+ return topKInfoCmd.Val(), topKInfoCmd.Err()
+ }
+ case CmdTypeTDigestInfo:
+ if tDigestInfoCmd, ok := cmd.(interface {
+ Val() TDigestInfo
+ Err() error
+ }); ok {
+ return tDigestInfoCmd.Val(), tDigestInfoCmd.Err()
+ }
+ case CmdTypeFTSearch:
+ if ftSearchCmd, ok := cmd.(interface {
+ Val() FTSearchResult
+ Err() error
+ }); ok {
+ return ftSearchCmd.Val(), ftSearchCmd.Err()
+ }
+ case CmdTypeFTInfo:
+ if ftInfoCmd, ok := cmd.(interface {
+ Val() FTInfoResult
+ Err() error
+ }); ok {
+ return ftInfoCmd.Val(), ftInfoCmd.Err()
+ }
+ case CmdTypeFTSpellCheck:
+ if ftSpellCheckCmd, ok := cmd.(interface {
+ Val() []SpellCheckResult
+ Err() error
+ }); ok {
+ return ftSpellCheckCmd.Val(), ftSpellCheckCmd.Err()
+ }
+ case CmdTypeFTSynDump:
+ if ftSynDumpCmd, ok := cmd.(interface {
+ Val() []FTSynDumpResult
+ Err() error
+ }); ok {
+ return ftSynDumpCmd.Val(), ftSynDumpCmd.Err()
+ }
+ case CmdTypeAggregate:
+ if aggregateCmd, ok := cmd.(interface {
+ Val() *FTAggregateResult
+ Err() error
+ }); ok {
+ return aggregateCmd.Val(), aggregateCmd.Err()
+ }
+ case CmdTypeTSTimestampValue:
+ if tsTimestampValueCmd, ok := cmd.(interface {
+ Val() TSTimestampValue
+ Err() error
+ }); ok {
+ return tsTimestampValueCmd.Val(), tsTimestampValueCmd.Err()
+ }
+ case CmdTypeTSTimestampValueSlice:
+ if tsTimestampValueSliceCmd, ok := cmd.(interface {
+ Val() []TSTimestampValue
+ Err() error
+ }); ok {
+ return tsTimestampValueSliceCmd.Val(), tsTimestampValueSliceCmd.Err()
+ }
+ case CmdTypeStringSlice:
+ if stringSliceCmd, ok := cmd.(interface {
+ Val() []string
+ Err() error
+ }); ok {
+ return stringSliceCmd.Val(), stringSliceCmd.Err()
+ }
+ case CmdTypeIntSlice:
+ if intSliceCmd, ok := cmd.(interface {
+ Val() []int64
+ Err() error
+ }); ok {
+ return intSliceCmd.Val(), intSliceCmd.Err()
+ }
+ case CmdTypeBoolSlice:
+ if boolSliceCmd, ok := cmd.(interface {
+ Val() []bool
+ Err() error
+ }); ok {
+ return boolSliceCmd.Val(), boolSliceCmd.Err()
+ }
+ case CmdTypeFloatSlice:
+ if floatSliceCmd, ok := cmd.(interface {
+ Val() []float64
+ Err() error
+ }); ok {
+ return floatSliceCmd.Val(), floatSliceCmd.Err()
+ }
+ case CmdTypeSlice:
+ if sliceCmd, ok := cmd.(interface {
+ Val() []interface{}
+ Err() error
+ }); ok {
+ return sliceCmd.Val(), sliceCmd.Err()
+ }
+ case CmdTypeKeyValueSlice:
+ if keyValueSliceCmd, ok := cmd.(interface {
+ Val() []KeyValue
+ Err() error
+ }); ok {
+ return keyValueSliceCmd.Val(), keyValueSliceCmd.Err()
+ }
+ case CmdTypeMapStringString:
+ if mapCmd, ok := cmd.(interface {
+ Val() map[string]string
+ Err() error
+ }); ok {
+ return mapCmd.Val(), mapCmd.Err()
+ }
+ case CmdTypeMapStringInt:
+ if mapCmd, ok := cmd.(interface {
+ Val() map[string]int64
+ Err() error
+ }); ok {
+ return mapCmd.Val(), mapCmd.Err()
+ }
+ case CmdTypeMapStringInterfaceSlice:
+ if mapCmd, ok := cmd.(interface {
+ Val() []map[string]interface{}
+ Err() error
+ }); ok {
+ return mapCmd.Val(), mapCmd.Err()
+ }
+ case CmdTypeMapStringInterface:
+ if mapCmd, ok := cmd.(interface {
+ Val() map[string]interface{}
+ Err() error
+ }); ok {
+ return mapCmd.Val(), mapCmd.Err()
+ }
+ case CmdTypeMapStringStringSlice:
+ if mapCmd, ok := cmd.(interface {
+ Val() []map[string]string
+ Err() error
+ }); ok {
+ return mapCmd.Val(), mapCmd.Err()
+ }
+ case CmdTypeMapMapStringInterface:
+ if mapCmd, ok := cmd.(interface {
+ Val() map[string]interface{}
+ Err() error
+ }); ok {
+ return mapCmd.Val(), mapCmd.Err()
+ }
+ default:
+ // For unknown command types, return nil
+ return nil, nil
+ }
+ }
+
+ // If we can't get the command type, return nil
+ return nil, nil
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go b/backend/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go
new file mode 100644
index 00000000..da8c6d31
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/command_policy_resolver.go
@@ -0,0 +1,209 @@
+package redis
+
+import (
+ "context"
+ "strings"
+
+ "github.com/redis/go-redis/v9/internal/routing"
+)
+
+type (
+ module = string
+ commandName = string
+)
+
+var defaultPolicies = map[module]map[commandName]*routing.CommandPolicy{
+ "ft": {
+ "create": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "search": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "aggregate": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "dictadd": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "dictdump": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "dictdel": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "suglen": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultHashSlot,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "cursor": {
+ Request: routing.ReqSpecial,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "sugadd": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultHashSlot,
+ },
+ "sugget": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultHashSlot,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "sugdel": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultHashSlot,
+ },
+ "spellcheck": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "explain": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "explaincli": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "aliasadd": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "aliasupdate": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "aliasdel": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "info": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "tagvals": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "syndump": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "synupdate": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "profile": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ Tips: map[string]string{
+ routing.ReadOnlyCMD: "",
+ },
+ },
+ "alter": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "dropindex": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ "drop": {
+ Request: routing.ReqDefault,
+ Response: routing.RespDefaultKeyless,
+ },
+ },
+}
+
+type CommandInfoResolveFunc func(ctx context.Context, cmd Cmder) *routing.CommandPolicy
+
+type commandInfoResolver struct {
+ resolveFunc CommandInfoResolveFunc
+ fallBackResolver *commandInfoResolver
+}
+
+func NewCommandInfoResolver(resolveFunc CommandInfoResolveFunc) *commandInfoResolver {
+ return &commandInfoResolver{
+ resolveFunc: resolveFunc,
+ }
+}
+
+func NewDefaultCommandPolicyResolver() *commandInfoResolver {
+ return NewCommandInfoResolver(func(ctx context.Context, cmd Cmder) *routing.CommandPolicy {
+ module := "core"
+ command := cmd.Name()
+ cmdParts := strings.Split(command, ".")
+ if len(cmdParts) == 2 {
+ module = cmdParts[0]
+ command = cmdParts[1]
+ }
+
+ if policy, ok := defaultPolicies[module][command]; ok {
+ return policy
+ }
+
+ return nil
+ })
+}
+
+func (r *commandInfoResolver) GetCommandPolicy(ctx context.Context, cmd Cmder) *routing.CommandPolicy {
+ if r.resolveFunc == nil {
+ return nil
+ }
+
+ policy := r.resolveFunc(ctx, cmd)
+ if policy != nil {
+ return policy
+ }
+
+ if r.fallBackResolver != nil {
+ return r.fallBackResolver.GetCommandPolicy(ctx, cmd)
+ }
+
+ return nil
+}
+
+func (r *commandInfoResolver) SetFallbackResolver(fallbackResolver *commandInfoResolver) {
+ r.fallBackResolver = fallbackResolver
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/commands.go b/backend/vendor/github.com/redis/go-redis/v9/commands.go
index daee5505..219fe464 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/commands.go
@@ -55,6 +55,11 @@ func appendArgs(dst, src []interface{}) []interface{} {
return appendArg(dst, src[0])
}
+ if cap(dst) < len(dst)+len(src) {
+ newDst := make([]interface{}, len(dst), len(dst)+len(src))
+ copy(newDst, dst)
+ dst = newDst
+ }
dst = append(dst, src...)
return dst
}
@@ -443,6 +448,9 @@ func (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd {
return cmd
}
+// Quit closes the connection.
+//
+// Deprecated: Just close the connection instead as of Redis 7.2.0.
func (c cmdable) Quit(_ context.Context) *StatusCmd {
panic("not implemented")
}
@@ -665,6 +673,9 @@ func (c cmdable) ShutdownNoSave(ctx context.Context) *StatusCmd {
return c.shutdown(ctx, "nosave")
}
+// SlaveOf sets a Redis server as a replica of another, or promotes it to being a master.
+//
+// Deprecated: Use ReplicaOf instead as of Redis 5.0.0.
func (c cmdable) SlaveOf(ctx context.Context, host, port string) *StatusCmd {
cmd := NewStatusCmd(ctx, "slaveof", host, port)
_ = c(ctx, cmd)
diff --git a/backend/vendor/github.com/redis/go-redis/v9/docker-compose.yml b/backend/vendor/github.com/redis/go-redis/v9/docker-compose.yml
index 5ffedb0a..8299fd9d 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/docker-compose.yml
+++ b/backend/vendor/github.com/redis/go-redis/v9/docker-compose.yml
@@ -1,12 +1,16 @@
---
+x-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.6.0}
+
services:
redis:
- image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
+ image: *default-image
platform: linux/amd64
container_name: redis-standalone
environment:
- TLS_ENABLED=yes
+ - TLS_CLIENT_CNS=testcertuser
+ - TLS_AUTH_CLIENTS_USER=CN
- REDIS_CLUSTER=no
- PORT=6379
- TLS_PORT=6666
@@ -21,9 +25,10 @@ services:
- sentinel
- all-stack
- all
+ - e2e
osscluster:
- image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
+ image: *default-image
platform: linux/amd64
container_name: redis-osscluster
environment:
@@ -39,14 +44,77 @@ services:
- all-stack
- all
+ cae-resp-proxy:
+ image: redislabs/client-resp-proxy:latest
+ container_name: cae-resp-proxy
+ environment:
+ - TARGET_HOST=redis
+ - TARGET_PORT=6379
+ - LISTEN_PORT=17000,17001,17002,17003 # 4 proxy nodes: initially show 3, swap in 4th during SMIGRATED
+ - LISTEN_HOST=0.0.0.0
+ - API_PORT=3000
+ - DEFAULT_INTERCEPTORS=cluster,hitless
+ ports:
+ - "17000:17000" # Proxy node 1 (host:container)
+ - "17001:17001" # Proxy node 2 (host:container)
+ - "17002:17002" # Proxy node 3 (host:container)
+ - "17003:17003" # Proxy node 4 (host:container) - hidden initially, swapped in during SMIGRATED
+ - "18100:3000" # HTTP API port (host:container)
+ depends_on:
+ - redis
+ profiles:
+ - e2e
+ - all
+
+ proxy-fault-injector:
+ build:
+ context: .
+ dockerfile: maintnotifications/e2e/cmd/proxy-fi-server/Dockerfile
+ container_name: proxy-fault-injector
+ ports:
+ - "15000:5000" # Fault injector API port (host:container)
+ depends_on:
+ - cae-resp-proxy
+ environment:
+ - PROXY_API_URL=http://cae-resp-proxy:3000
+ profiles:
+ - e2e
+ - all
+
+ osscluster-tls:
+ image: *default-image
+ platform: linux/amd64
+ container_name: redis-osscluster-tls
+ environment:
+ - NODES=6
+ - PORT=6430
+ - TLS_PORT=5430
+ - TLS_ENABLED=yes
+ - TLS_CLIENT_CNS=testcertuser
+ - TLS_AUTH_CLIENTS_USER=CN
+ - REDIS_CLUSTER=yes
+ - REPLICAS=1
+ command: "--tls-auth-clients optional --cluster-announce-ip 127.0.0.1"
+ ports:
+ - "6430-6435:6430-6435" # Regular ports
+ - "5430-5435:5430-5435" # TLS ports (set via TLS_PORT env var)
+ - "16430-16435:16430-16435" # Cluster bus ports (PORT + 10000)
+ volumes:
+ - "./dockers/osscluster-tls:/redis/work"
+ profiles:
+ - cluster-tls
+ - all
+
sentinel-cluster:
- image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
+ image: *default-image
platform: linux/amd64
container_name: redis-sentinel-cluster
network_mode: "host"
environment:
- NODES=3
- TLS_ENABLED=yes
+ - TLS_CLIENT_CNS=testcertuser
+ - TLS_AUTH_CLIENTS_USER=CN
- REDIS_CLUSTER=no
- PORT=9121
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
@@ -60,7 +128,7 @@ services:
- all
sentinel:
- image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
+ image: *default-image
platform: linux/amd64
container_name: redis-sentinel
depends_on:
@@ -84,12 +152,14 @@ services:
- all
ring-cluster:
- image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
+ image: *default-image
platform: linux/amd64
container_name: redis-ring-cluster
environment:
- NODES=3
- TLS_ENABLED=yes
+ - TLS_CLIENT_CNS=testcertuser
+ - TLS_AUTH_CLIENTS_USER=CN
- REDIS_CLUSTER=no
- PORT=6390
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/error.go b/backend/vendor/github.com/redis/go-redis/v9/error.go
index 12b5604d..d2462a49 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/error.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/error.go
@@ -124,6 +124,9 @@ func shouldRetry(err error, retryTimeout bool) bool {
if proto.IsTryAgainError(err) {
return true
}
+ if proto.IsNoReplicasError(err) {
+ return true
+ }
// Fallback to string checking for backward compatibility with plain errors
s := err.Error()
@@ -145,6 +148,9 @@ func shouldRetry(err error, retryTimeout bool) bool {
if strings.HasPrefix(s, "MASTERDOWN ") {
return true
}
+ if strings.HasPrefix(s, "NOREPLICAS ") {
+ return true
+ }
return false
}
@@ -342,6 +348,14 @@ func IsOOMError(err error) bool {
return proto.IsOOMError(err)
}
+// IsNoReplicasError checks if an error is a Redis NOREPLICAS error, even if wrapped.
+// NOREPLICAS errors occur when not enough replicas acknowledge a write operation.
+// This typically happens with WAIT/WAITAOF commands or CLUSTER SETSLOT with synchronous
+// replication when the required number of replicas cannot confirm the write within the timeout.
+func IsNoReplicasError(err error) bool {
+ return proto.IsNoReplicasError(err)
+}
+
//------------------------------------------------------------------------------
type timeoutError interface {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/geo_commands.go b/backend/vendor/github.com/redis/go-redis/v9/geo_commands.go
index f047b98a..0f274289 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/geo_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/geo_commands.go
@@ -33,7 +33,10 @@ func (c cmdable) GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLoca
return cmd
}
-// GeoRadius is a read-only GEORADIUS_RO command.
+// GeoRadius queries a geospatial index for members within a distance from a coordinate.
+// This is a read-only variant that does not support Store or StoreDist options.
+//
+// Deprecated: Use GeoSearch with BYRADIUS argument instead as of Redis 6.2.0.
func (c cmdable) GeoRadius(
ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery,
) *GeoLocationCmd {
@@ -60,7 +63,10 @@ func (c cmdable) GeoRadiusStore(
return cmd
}
-// GeoRadiusByMember is a read-only GEORADIUSBYMEMBER_RO command.
+// GeoRadiusByMember queries a geospatial index for members within a distance from a member.
+// This is a read-only variant that does not support Store or StoreDist options.
+//
+// Deprecated: Use GeoSearch with BYRADIUS and FROMMEMBER arguments instead as of Redis 6.2.0.
func (c cmdable) GeoRadiusByMember(
ctx context.Context, key, member string, query *GeoRadiusQuery,
) *GeoLocationCmd {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/hotkeys_commands.go b/backend/vendor/github.com/redis/go-redis/v9/hotkeys_commands.go
new file mode 100644
index 00000000..024db3ff
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/hotkeys_commands.go
@@ -0,0 +1,122 @@
+package redis
+
+import (
+ "context"
+ "errors"
+ "strings"
+)
+
+// HOTKEYS commands are only available on standalone *Client instances.
+// They are NOT available on ClusterClient, Ring, or UniversalClient because
+// HOTKEYS is a stateful command requiring session affinity - all operations
+// (START, GET, STOP, RESET) must be sent to the same Redis node.
+//
+// If you are using UniversalClient and need HOTKEYS functionality, you must
+// type assert to *Client first:
+//
+// if client, ok := universalClient.(*redis.Client); ok {
+// result, err := client.HotKeysStart(ctx, args)
+// // ...
+// }
+
+// HotKeysMetric represents the metrics that can be tracked by the HOTKEYS command.
+type HotKeysMetric string
+
+const (
+ // HotKeysMetricCPU tracks CPU time spent on the key (in microseconds).
+ HotKeysMetricCPU HotKeysMetric = "CPU"
+ // HotKeysMetricNET tracks network bytes used by the key (ingress + egress + replication).
+ HotKeysMetricNET HotKeysMetric = "NET"
+)
+
+// HotKeysStartArgs contains the arguments for the HOTKEYS START command.
+// This command is only available on standalone clients due to its stateful nature
+// requiring session affinity. It must NOT be used on cluster or pooled clients.
+type HotKeysStartArgs struct {
+ // Metrics to track. At least one must be specified.
+ Metrics []HotKeysMetric
+ // Count is the number of top keys to report.
+ // Default: 10, Min: 10, Max: 64
+ Count uint8
+ // Duration is the auto-stop tracking after this many seconds.
+ // Default: 0 (no auto-stop)
+ Duration int64
+ // Sample is the sample ratio - track keys with probability 1/sample.
+ // Default: 1 (track every key), Min: 1
+ Sample int64
+ // Slots specifies specific hash slots to track (0-16383).
+ // All specified slots must be hosted by the receiving node.
+ // If not specified, all slots are tracked.
+ Slots []uint16
+}
+
+// ErrHotKeysNoMetrics is returned when HotKeysStart is called without any metrics specified.
+var ErrHotKeysNoMetrics = errors.New("redis: at least one metric must be specified for HOTKEYS START")
+
+// HotKeysStart starts collecting hotkeys data.
+// At least one metric must be specified in args.Metrics.
+// This command is only available on standalone clients.
+func (c *Client) HotKeysStart(ctx context.Context, args *HotKeysStartArgs) *StatusCmd {
+ cmdArgs := make([]interface{}, 0, 16)
+ cmdArgs = append(cmdArgs, "hotkeys", "start")
+
+ // Validate that at least one metric is specified
+ if len(args.Metrics) == 0 {
+ cmd := NewStatusCmd(ctx, cmdArgs...)
+ cmd.SetErr(ErrHotKeysNoMetrics)
+ return cmd
+ }
+
+ cmdArgs = append(cmdArgs, "metrics", len(args.Metrics))
+ for _, metric := range args.Metrics {
+ cmdArgs = append(cmdArgs, strings.ToLower(string(metric)))
+ }
+
+ if args.Count > 0 {
+ cmdArgs = append(cmdArgs, "count", args.Count)
+ }
+
+ if args.Duration > 0 {
+ cmdArgs = append(cmdArgs, "duration", args.Duration)
+ }
+
+ if args.Sample > 0 {
+ cmdArgs = append(cmdArgs, "sample", args.Sample)
+ }
+
+ if len(args.Slots) > 0 {
+ cmdArgs = append(cmdArgs, "slots", len(args.Slots))
+ for _, slot := range args.Slots {
+ cmdArgs = append(cmdArgs, slot)
+ }
+ }
+
+ cmd := NewStatusCmd(ctx, cmdArgs...)
+ _ = c.Process(ctx, cmd)
+ return cmd
+}
+
+// HotKeysStop stops the ongoing hotkeys collection session.
+// This command is only available on standalone clients.
+func (c *Client) HotKeysStop(ctx context.Context) *StatusCmd {
+ cmd := NewStatusCmd(ctx, "hotkeys", "stop")
+ _ = c.Process(ctx, cmd)
+ return cmd
+}
+
+// HotKeysReset discards the last hotkeys collection session results.
+// Returns an error if tracking is currently active.
+// This command is only available on standalone clients.
+func (c *Client) HotKeysReset(ctx context.Context) *StatusCmd {
+ cmd := NewStatusCmd(ctx, "hotkeys", "reset")
+ _ = c.Process(ctx, cmd)
+ return cmd
+}
+
+// HotKeysGet retrieves the results of the ongoing or last hotkeys collection session.
+// This command is only available on standalone clients.
+func (c *Client) HotKeysGet(ctx context.Context) *HotKeysCmd {
+ cmd := NewHotKeysCmd(ctx, "hotkeys", "get")
+ _ = c.Process(ctx, cmd)
+ return cmd
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/interfaces/interfaces.go b/backend/vendor/github.com/redis/go-redis/v9/internal/interfaces/interfaces.go
index 17e2a185..8f856971 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/interfaces/interfaces.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/interfaces/interfaces.go
@@ -40,6 +40,11 @@ type OptionsInterface interface {
// GetAddr returns the connection address.
GetAddr() string
+ // GetNodeAddress returns the address of the Redis node as reported by the server.
+ // For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation.
+ // For standalone clients, this defaults to Addr.
+ GetNodeAddress() string
+
// IsTLSEnabled returns true if TLS is enabled.
IsTLSEnabled() bool
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/maintnotifications/logs/log_messages.go b/backend/vendor/github.com/redis/go-redis/v9/internal/maintnotifications/logs/log_messages.go
index 34cb1692..93e5bded 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/maintnotifications/logs/log_messages.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/maintnotifications/logs/log_messages.go
@@ -121,6 +121,9 @@ const (
UnrelaxedTimeoutMessage = "clearing relaxed timeout"
ManagerNotInitializedMessage = "manager not initialized"
FailedToMarkForHandoffMessage = "failed to mark connection for handoff"
+ InvalidSeqIDInSMigratingNotificationMessage = "invalid SeqID in SMIGRATING notification"
+ InvalidSeqIDInSMigratedNotificationMessage = "invalid SeqID in SMIGRATED notification"
+ TriggeringClusterStateReloadMessage = "triggering cluster state reload"
// ========================================
// used in pool/conn
@@ -288,19 +291,29 @@ func OperationNotTracked(connID uint64, seqID int64) string {
// Connection pool functions
func RemovingConnectionFromPool(connID uint64, reason error) string {
- message := fmt.Sprintf("conn[%d] %s due to: %v", connID, RemovingConnectionFromPoolMessage, reason)
- return appendJSONIfDebug(message, map[string]interface{}{
+ metadata := map[string]interface{}{
"connID": connID,
- "reason": reason.Error(),
- })
+ "reason": "unknown", // this will be overwritten if reason is not nil
+ }
+ if reason != nil {
+ metadata["reason"] = reason.Error()
+ }
+
+ message := fmt.Sprintf("conn[%d] %s due to: %v", connID, RemovingConnectionFromPoolMessage, reason)
+ return appendJSONIfDebug(message, metadata)
}
func NoPoolProvidedCannotRemove(connID uint64, reason error) string {
- message := fmt.Sprintf("conn[%d] %s due to: %v", connID, NoPoolProvidedMessageCannotRemoveMessage, reason)
- return appendJSONIfDebug(message, map[string]interface{}{
+ metadata := map[string]interface{}{
"connID": connID,
- "reason": reason.Error(),
- })
+ "reason": "unknown", // this will be overwritten if reason is not nil
+ }
+ if reason != nil {
+ metadata["reason"] = reason.Error()
+ }
+
+ message := fmt.Sprintf("conn[%d] %s due to: %v", connID, NoPoolProvidedMessageCannotRemoveMessage, reason)
+ return appendJSONIfDebug(message, metadata)
}
// Circuit breaker functions
@@ -623,3 +636,28 @@ func ExtractDataFromLogMessage(logMessage string) map[string]interface{} {
// If JSON parsing fails, return empty map
return result
}
+
+// Cluster notification functions
+func InvalidSeqIDInSMigratingNotification(seqID interface{}) string {
+ message := fmt.Sprintf("%s: %v", InvalidSeqIDInSMigratingNotificationMessage, seqID)
+ return appendJSONIfDebug(message, map[string]interface{}{
+ "seqID": fmt.Sprintf("%v", seqID),
+ })
+}
+
+func InvalidSeqIDInSMigratedNotification(seqID interface{}) string {
+ message := fmt.Sprintf("%s: %v", InvalidSeqIDInSMigratedNotificationMessage, seqID)
+ return appendJSONIfDebug(message, map[string]interface{}{
+ "seqID": fmt.Sprintf("%v", seqID),
+ })
+}
+
+// TriggeringClusterStateReload logs when cluster state reload is triggered (deduplicated, once per seqID)
+func TriggeringClusterStateReload(seqID int64, hostPort string, slotRanges []string) string {
+ message := fmt.Sprintf("%s seqID=%d host:port=%s slots=%v", TriggeringClusterStateReloadMessage, seqID, hostPort, slotRanges)
+ return appendJSONIfDebug(message, map[string]interface{}{
+ "seqID": seqID,
+ "hostPort": hostPort,
+ "slotRanges": slotRanges,
+ })
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go b/backend/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go
new file mode 100644
index 00000000..a4840825
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go
@@ -0,0 +1,279 @@
+package otel
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "sync"
+ "time"
+
+ "github.com/redis/go-redis/v9/internal/pool"
+)
+
+// generateUniqueID generates a short unique identifier for pool names.
+func generateUniqueID() string {
+ b := make([]byte, 4)
+ if _, err := rand.Read(b); err != nil {
+ return ""
+ }
+ return hex.EncodeToString(b)
+}
+
+// Cmder is a minimal interface for command information needed for metrics.
+// This avoids circular dependencies with the main redis package.
+type Cmder interface {
+ Name() string
+ FullName() string
+ Args() []interface{}
+ Err() error
+}
+
+// Recorder is the interface for recording metrics.
+type Recorder interface {
+ // RecordOperationDuration records the total operation duration (including all retries)
+ // dbIndex is the Redis database index (0-15)
+ RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int)
+
+ // RecordPipelineOperationDuration records the total pipeline/transaction duration.
+ // operationName should be "PIPELINE" for regular pipelines or "MULTI" for transactions.
+ // cmdCount is the number of commands in the pipeline.
+ // err is the error from the pipeline execution (can be nil).
+ // dbIndex is the Redis database index (0-15)
+ RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int)
+
+ // RecordConnectionCreateTime records the time it took to create a new connection
+ RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn)
+
+ // RecordConnectionRelaxedTimeout records when connection timeout is relaxed/unrelaxed
+ // delta: +1 for relaxed, -1 for unrelaxed
+ // poolName: name of the connection pool (e.g., "main", "pubsub")
+ // notificationType: the notification type that triggered the timeout relaxation (e.g., "MOVING")
+ RecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string)
+
+ // RecordConnectionHandoff records when a connection is handed off to another node
+ // poolName: name of the connection pool (e.g., "main", "pubsub")
+ RecordConnectionHandoff(ctx context.Context, cn *pool.Conn, poolName string)
+
+ // RecordError records client errors (ASK, MOVED, handshake failures, etc.)
+ // errorType: type of error (e.g., "ASK", "MOVED", "HANDSHAKE_FAILED")
+ // statusCode: Redis response status code if available (e.g., "MOVED", "ASK")
+ // isInternal: whether this is an internal error
+ // retryAttempts: number of retry attempts made
+ RecordError(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int)
+
+ // RecordMaintenanceNotification records when a maintenance notification is received
+ // notificationType: the type of notification (e.g., "MOVING", "MIGRATING", etc.)
+ RecordMaintenanceNotification(ctx context.Context, cn *pool.Conn, notificationType string)
+
+ // RecordConnectionWaitTime records the time spent waiting for a connection from the pool
+ RecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn *pool.Conn)
+
+ // RecordConnectionClosed records when a connection is closed
+ // reason: reason for closing (e.g., "idle", "max_lifetime", "error", "pool_closed")
+ // err: the error that caused the close (nil for non-error closures)
+ RecordConnectionClosed(ctx context.Context, cn *pool.Conn, reason string, err error)
+
+ // RecordPubSubMessage records a Pub/Sub message
+ // direction: "sent" or "received"
+ // channel: channel name (may be hidden for cardinality reduction)
+ // sharded: true for sharded pub/sub (SPUBLISH/SSUBSCRIBE)
+ RecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool)
+
+ // RecordStreamLag records the lag for stream consumer group processing
+ // lag: time difference between message creation and consumption
+ // streamName: name of the stream (may be hidden for cardinality reduction)
+ // consumerGroup: name of the consumer group
+ // consumerName: name of the consumer
+ RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string)
+}
+
+type PubSubPooler interface {
+ Stats() *pool.PubSubStats
+}
+
+type PoolRegistrar interface {
+ // RegisterPool is called when a new client is created with its connection pools.
+ // poolName: identifier for the pool (e.g., "main_abc123")
+ // pool: the connection pool
+ RegisterPool(poolName string, pool pool.Pooler)
+ // UnregisterPool is called when a client is closed to remove its pool from the registry.
+ // pool: the connection pool to unregister
+ UnregisterPool(pool pool.Pooler)
+ // RegisterPubSubPool is called when a new client is created with a PubSub pool.
+ // poolName: identifier for the pool (e.g., "main_abc123_pubsub")
+ // pool: the PubSub connection pool
+ RegisterPubSubPool(poolName string, pool PubSubPooler)
+ // UnregisterPubSubPool is called when a PubSub client is closed to remove its pool.
+ // pool: the PubSub connection pool to unregister
+ UnregisterPubSubPool(pool PubSubPooler)
+}
+
+var (
+ // recorderMu protects globalRecorder and operation duration callbacks
+ recorderMu sync.RWMutex
+
+ // Global recorder instance (initialized by extra/redisotel-native)
+ globalRecorder Recorder = noopRecorder{}
+
+ // Callbacks for operation duration metrics
+ operationDurationCallback func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int)
+ pipelineOperationDurationCallback func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int)
+)
+
+// GetOperationDurationCallback returns the callback for operation duration.
+func GetOperationDurationCallback() func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) {
+ recorderMu.RLock()
+ cb := operationDurationCallback
+ recorderMu.RUnlock()
+ return cb
+}
+
+// GetPipelineOperationDurationCallback returns the callback for pipeline operation duration.
+func GetPipelineOperationDurationCallback() func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) {
+ recorderMu.RLock()
+ cb := pipelineOperationDurationCallback
+ recorderMu.RUnlock()
+ return cb
+}
+
+// getRecorder returns the current global recorder under a read lock.
+func getRecorder() Recorder {
+ recorderMu.RLock()
+ r := globalRecorder
+ recorderMu.RUnlock()
+ return r
+}
+
+// SetGlobalRecorder sets the global recorder (called by Init() in extra/redisotel-native)
+func SetGlobalRecorder(r Recorder) {
+ recorderMu.Lock()
+ if r == nil {
+ globalRecorder = noopRecorder{}
+ operationDurationCallback = nil
+ pipelineOperationDurationCallback = nil
+ recorderMu.Unlock()
+ // Unregister all pool metric callbacks atomically
+ pool.SetAllMetricCallbacks(nil)
+ return
+ }
+ globalRecorder = r
+
+ // Register operation duration callbacks
+ // These capture r directly since we want them to use the specific recorder
+ // that was set at this point in time
+ operationDurationCallback = func(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) {
+ getRecorder().RecordOperationDuration(ctx, duration, cmd, attempts, err, cn, dbIndex)
+ }
+ pipelineOperationDurationCallback = func(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) {
+ getRecorder().RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, cn, dbIndex)
+ }
+ recorderMu.Unlock()
+
+ // Register all pool metric callbacks atomically
+ // These use getRecorder() to safely access the current recorder
+ pool.SetAllMetricCallbacks(&pool.MetricCallbacks{
+ ConnectionCreateTime: func(ctx context.Context, duration time.Duration, cn *pool.Conn) {
+ getRecorder().RecordConnectionCreateTime(ctx, duration, cn)
+ },
+ ConnectionRelaxedTimeout: func(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string) {
+ getRecorder().RecordConnectionRelaxedTimeout(ctx, delta, cn, poolName, notificationType)
+ },
+ ConnectionHandoff: func(ctx context.Context, cn *pool.Conn, poolName string) {
+ getRecorder().RecordConnectionHandoff(ctx, cn, poolName)
+ },
+ Error: func(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int) {
+ getRecorder().RecordError(ctx, errorType, cn, statusCode, isInternal, retryAttempts)
+ },
+ MaintenanceNotification: func(ctx context.Context, cn *pool.Conn, notificationType string) {
+ getRecorder().RecordMaintenanceNotification(ctx, cn, notificationType)
+ },
+ ConnectionWaitTime: func(ctx context.Context, duration time.Duration, cn *pool.Conn) {
+ getRecorder().RecordConnectionWaitTime(ctx, duration, cn)
+ },
+ ConnectionClosed: func(ctx context.Context, cn *pool.Conn, reason string, err error) {
+ getRecorder().RecordConnectionClosed(ctx, cn, reason, err)
+ },
+ })
+}
+
+// RecordOperationDuration records the total operation duration.
+// dbIndex is the Redis database index (0-15).
+func RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) {
+ getRecorder().RecordOperationDuration(ctx, duration, cmd, attempts, err, cn, dbIndex)
+}
+
+// RecordPipelineOperationDuration records the total pipeline/transaction duration.
+// This is called from redis.go after pipeline/transaction execution completes.
+// operationName should be "PIPELINE" for regular pipelines or "MULTI" for transactions.
+// err is the error from the pipeline execution (can be nil).
+// dbIndex is the Redis database index (0-15).
+func RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) {
+ getRecorder().RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, cn, dbIndex)
+}
+
+// RecordConnectionCreateTime records the time it took to create a new connection.
+func RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn) {
+ getRecorder().RecordConnectionCreateTime(ctx, duration, cn)
+}
+
+// RecordPubSubMessage records a Pub/Sub message sent or received.
+func RecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool) {
+ getRecorder().RecordPubSubMessage(ctx, cn, direction, channel, sharded)
+}
+
+// RecordStreamLag records the lag between message creation and consumption in a stream.
+func RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string) {
+ getRecorder().RecordStreamLag(ctx, lag, cn, streamName, consumerGroup, consumerName)
+}
+
+type noopRecorder struct{}
+
+func (noopRecorder) RecordOperationDuration(context.Context, time.Duration, Cmder, int, error, *pool.Conn, int) {
+}
+func (noopRecorder) RecordPipelineOperationDuration(context.Context, time.Duration, string, int, int, error, *pool.Conn, int) {
+}
+func (noopRecorder) RecordConnectionCreateTime(context.Context, time.Duration, *pool.Conn) {}
+func (noopRecorder) RecordConnectionRelaxedTimeout(context.Context, int, *pool.Conn, string, string) {
+}
+func (noopRecorder) RecordConnectionHandoff(context.Context, *pool.Conn, string) {}
+func (noopRecorder) RecordError(context.Context, string, *pool.Conn, string, bool, int) {}
+func (noopRecorder) RecordMaintenanceNotification(context.Context, *pool.Conn, string) {}
+
+func (noopRecorder) RecordConnectionWaitTime(context.Context, time.Duration, *pool.Conn) {}
+func (noopRecorder) RecordConnectionClosed(context.Context, *pool.Conn, string, error) {}
+
+func (noopRecorder) RecordPubSubMessage(context.Context, *pool.Conn, string, string, bool) {}
+
+func (noopRecorder) RecordStreamLag(context.Context, time.Duration, *pool.Conn, string, string, string) {
+}
+
+// RegisterPools registers connection pools with the global recorder.
+func RegisterPools(connPool pool.Pooler, pubSubPool PubSubPooler, addr string) {
+ // Check if the global recorder implements PoolRegistrar
+ if registrar, ok := globalRecorder.(PoolRegistrar); ok {
+ // Generate a unique ID for this client's pools
+ uniqueID := generateUniqueID()
+
+ if connPool != nil {
+ poolName := addr + "_" + uniqueID
+ registrar.RegisterPool(poolName, connPool)
+ }
+ if pubSubPool != nil {
+ poolName := addr + "_" + uniqueID + "_pubsub"
+ registrar.RegisterPubSubPool(poolName, pubSubPool)
+ }
+ }
+}
+
+// UnregisterPools removes connection pools from the global recorder
+func UnregisterPools(connPool pool.Pooler, pubSubPool PubSubPooler) {
+ // Check if the global recorder implements PoolRegistrar
+ if registrar, ok := globalRecorder.(PoolRegistrar); ok {
+ if connPool != nil {
+ registrar.UnregisterPool(connPool)
+ }
+ if pubSubPool != nil {
+ registrar.UnregisterPubSubPool(pubSubPool)
+ }
+ }
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go
index 95d83bfd..f0af63c6 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go
@@ -69,8 +69,9 @@ type Conn struct {
// Connection identifier for unique tracking
id uint64
- usedAt atomic.Int64
- lastPutAt atomic.Int64
+ usedAt atomic.Int64
+ lastPutAt atomic.Int64
+ dialStartNs atomic.Int64 // Time when dial started (for connection create time metric)
// Lock-free netConn access using atomic.Value
// Contains *atomicNetConn wrapper, accessed atomically for better performance
@@ -104,6 +105,7 @@ type Conn struct {
closed atomic.Bool
createdAt time.Time
expiresAt time.Time
+ poolName string // Name of the pool this connection belongs to (for metrics)
// maintenanceNotifications upgrade support: relaxed timeouts during migrations/failovers
@@ -184,6 +186,24 @@ func (cn *Conn) SetLastPutAtNs(ns int64) {
cn.lastPutAt.Store(ns)
}
+// GetDialStartNs returns the time when the dial started (in nanoseconds since epoch).
+// This is used to calculate the full connection creation time (TCP + handshake).
+func (cn *Conn) GetDialStartNs() int64 {
+ return cn.dialStartNs.Load()
+}
+
+// PoolName returns the name of the pool this connection belongs to.
+// This is used for metrics to identify which pool a connection is from.
+func (cn *Conn) PoolName() string {
+ return cn.poolName
+}
+
+// SetPoolName sets the name of the pool this connection belongs to.
+// This should be called when the connection is added to a pool.
+func (cn *Conn) SetPoolName(name string) {
+ cn.poolName = name
+}
+
// Backward-compatible wrapper methods for state machine
// These maintain the existing API while using the new state machine internally
@@ -418,6 +438,8 @@ func (cn *Conn) IsPubSub() bool {
// SetRelaxedTimeout sets relaxed timeouts for this connection during maintenanceNotifications upgrades.
// These timeouts will be used for all subsequent commands until the deadline expires.
// Uses atomic operations for lock-free access.
+// Note: Metrics should be recorded by the caller (notification handler) which has context about
+// the notification type and pool name.
func (cn *Conn) SetRelaxedTimeout(readTimeout, writeTimeout time.Duration) {
cn.relaxedCounter.Add(1)
cn.relaxedReadTimeoutNs.Store(int64(readTimeout))
@@ -452,6 +474,11 @@ func (cn *Conn) clearRelaxedTimeout() {
cn.relaxedWriteTimeoutNs.Store(0)
cn.relaxedDeadlineNs.Store(0)
cn.relaxedCounter.Store(0)
+
+ // Note: Metrics for timeout unrelaxing are not recorded here because we don't have
+ // context about which notification type or pool triggered the relaxation.
+ // In practice, relaxed timeouts expire automatically via deadline, so explicit
+ // unrelaxing metrics are less critical than the initial relaxation metrics.
}
// HasRelaxedTimeout returns true if relaxed timeouts are currently active on this connection.
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go
index 2050a742..afdc631c 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go
@@ -13,11 +13,12 @@ import (
// States are designed to be lightweight and fast to check.
//
// State Transitions:
-// CREATED → INITIALIZING → IDLE ⇄ IN_USE
-// ↓
-// UNUSABLE (handoff/reauth)
-// ↓
-// IDLE/CLOSED
+//
+// CREATED → INITIALIZING → IDLE ⇄ IN_USE
+// ↓
+// UNUSABLE (handoff/reauth)
+// ↓
+// IDLE/CLOSED
type ConnState uint32
const (
@@ -120,7 +121,7 @@ type ConnStateMachine struct {
// FIFO queue for waiters - only locked during waiter add/remove/notify
mu sync.Mutex
- waiters *list.List // List of *waiter
+ waiters *list.List // List of *waiter
waiterCount atomic.Int32 // Fast lock-free check for waiters (avoids mutex in hot path)
}
@@ -340,4 +341,3 @@ func (sm *ConnStateMachine) notifyWaiters() {
}
}
}
-
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go
index d757d1f4..aaca530c 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go
@@ -10,7 +10,7 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/proto"
- "github.com/redis/go-redis/v9/internal/util"
+ "github.com/redis/go-redis/v9/internal/rand"
)
var (
@@ -32,6 +32,45 @@ var (
// errConnNotPooled is returned when trying to return a non-pooled connection to the pool.
errConnNotPooled = errors.New("connection not pooled")
+ // metricCallbackMu protects all global metric callback functions for thread-safe access.
+ metricCallbackMu sync.RWMutex
+
+ // Global metric callbacks for connection state changes
+ metricConnectionStateChangeCallback func(ctx context.Context, cn *Conn, fromState, toState string)
+
+ // Global metric callback for connection creation time
+ metricConnectionCreateTimeCallback func(ctx context.Context, duration time.Duration, cn *Conn)
+
+ // Global metric callback for connection relaxed timeout changes
+ // Parameters: ctx, delta (+1/-1), cn, poolName, notificationType
+ metricConnectionRelaxedTimeoutCallback func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string)
+
+ // Global metric callback for connection handoff
+ // Parameters: ctx, cn, poolName
+ metricConnectionHandoffCallback func(ctx context.Context, cn *Conn, poolName string)
+
+ // Global metric callback for error tracking
+ // Parameters: ctx, errorType, cn, statusCode, isInternal, retryAttempts
+ metricErrorCallback func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int)
+
+ // Global metric callback for maintenance notifications
+ // Parameters: ctx, cn, notificationType
+ metricMaintenanceNotificationCallback func(ctx context.Context, cn *Conn, notificationType string)
+
+ // Global metric callback for connection wait time
+ // Parameters: ctx, duration, cn
+ metricConnectionWaitTimeCallback func(ctx context.Context, duration time.Duration, cn *Conn)
+
+ // Global metric callback for connection timeouts
+ // Parameters: ctx, cn, timeoutType
+ metricConnectionTimeoutCallback func(ctx context.Context, cn *Conn, timeoutType string)
+
+ // Global metric callback for connection closed
+ // Parameters: ctx, cn, reason, err
+ metricConnectionClosedCallback func(ctx context.Context, cn *Conn, reason string, err error)
+
+ // errPanicInDial is returned when a panic occurs in the dial function.
+ errPanicInQueuedNewConn = errors.New("panic in queuedNewConn")
// popAttempts is the maximum number of attempts to find a usable connection
// when popping from the idle connection pool. This handles cases where connections
@@ -51,6 +90,139 @@ var (
noExpiration = maxTime
)
+// MetricCallbacks holds all metric callback functions.
+// Use SetAllMetricCallbacks to register all callbacks atomically.
+type MetricCallbacks struct {
+ // ConnectionCreateTime is called when a new connection is created
+ ConnectionCreateTime func(ctx context.Context, duration time.Duration, cn *Conn)
+
+ // ConnectionRelaxedTimeout is called when connection timeout is relaxed/unrelaxed
+ // delta: +1 for relaxed, -1 for unrelaxed
+ ConnectionRelaxedTimeout func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string)
+
+ // ConnectionHandoff is called when a connection is handed off to another node
+ ConnectionHandoff func(ctx context.Context, cn *Conn, poolName string)
+
+ // Error is called when an error occurs
+ Error func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int)
+
+ // MaintenanceNotification is called when a maintenance notification is received
+ MaintenanceNotification func(ctx context.Context, cn *Conn, notificationType string)
+
+ // ConnectionWaitTime is called to record time spent waiting for a connection
+ ConnectionWaitTime func(ctx context.Context, duration time.Duration, cn *Conn)
+
+ // ConnectionClosed is called when a connection is closed
+ ConnectionClosed func(ctx context.Context, cn *Conn, reason string, err error)
+}
+
+// SetAllMetricCallbacks sets all metric callbacks atomically.
+// Pass nil to clear all callbacks (disable metrics).
+// This ensures all callbacks are set together under a single lock,
+// preventing inconsistent state during registration.
+//
+// Note on thread safety: After returning, there is a small window where
+// concurrent getMetric* calls may return the old callback value. This is
+// acceptable for metrics - at most one event may go to the old recorder
+// or be missed during the transition. The callbacks themselves are immutable
+// function pointers, so calling an "old" callback is safe.
+func SetAllMetricCallbacks(callbacks *MetricCallbacks) {
+ metricCallbackMu.Lock()
+ defer metricCallbackMu.Unlock()
+
+ if callbacks == nil {
+ metricConnectionCreateTimeCallback = nil
+ metricConnectionRelaxedTimeoutCallback = nil
+ metricConnectionHandoffCallback = nil
+ metricErrorCallback = nil
+ metricMaintenanceNotificationCallback = nil
+ metricConnectionWaitTimeCallback = nil
+ metricConnectionClosedCallback = nil
+ return
+ }
+
+ metricConnectionCreateTimeCallback = callbacks.ConnectionCreateTime
+ metricConnectionRelaxedTimeoutCallback = callbacks.ConnectionRelaxedTimeout
+ metricConnectionHandoffCallback = callbacks.ConnectionHandoff
+ metricErrorCallback = callbacks.Error
+ metricMaintenanceNotificationCallback = callbacks.MaintenanceNotification
+ metricConnectionWaitTimeCallback = callbacks.ConnectionWaitTime
+ metricConnectionClosedCallback = callbacks.ConnectionClosed
+}
+
+// getMetricConnectionStateChangeCallback returns the metric callback for connection state changes.
+func getMetricConnectionStateChangeCallback() func(ctx context.Context, cn *Conn, fromState, toState string) {
+ metricCallbackMu.RLock()
+ cb := metricConnectionStateChangeCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
+// GetMetricConnectionCreateTimeCallback returns the metric callback for connection creation time.
+func GetMetricConnectionCreateTimeCallback() func(ctx context.Context, duration time.Duration, cn *Conn) {
+ metricCallbackMu.RLock()
+ cb := metricConnectionCreateTimeCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
+// GetMetricConnectionRelaxedTimeoutCallback returns the metric callback for connection relaxed timeout changes.
+// This is used by maintnotifications to record relaxed timeout metrics.
+func GetMetricConnectionRelaxedTimeoutCallback() func(ctx context.Context, delta int, cn *Conn, poolName, notificationType string) {
+ metricCallbackMu.RLock()
+ cb := metricConnectionRelaxedTimeoutCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
+// GetMetricConnectionHandoffCallback returns the metric callback for connection handoffs.
+// This is used by maintnotifications to record handoff metrics.
+func GetMetricConnectionHandoffCallback() func(ctx context.Context, cn *Conn, poolName string) {
+ metricCallbackMu.RLock()
+ cb := metricConnectionHandoffCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
+// GetMetricErrorCallback returns the metric callback for error tracking.
+// This is used by cluster and client code to record error metrics.
+func GetMetricErrorCallback() func(ctx context.Context, errorType string, cn *Conn, statusCode string, isInternal bool, retryAttempts int) {
+ metricCallbackMu.RLock()
+ cb := metricErrorCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
+// GetMetricMaintenanceNotificationCallback returns the metric callback for maintenance notifications.
+// This is used by maintnotifications to record notification metrics.
+func GetMetricMaintenanceNotificationCallback() func(ctx context.Context, cn *Conn, notificationType string) {
+ metricCallbackMu.RLock()
+ cb := metricMaintenanceNotificationCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
+func getMetricConnectionWaitTimeCallback() func(ctx context.Context, duration time.Duration, cn *Conn) {
+ metricCallbackMu.RLock()
+ cb := metricConnectionWaitTimeCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
+func getMetricConnectionTimeoutCallback() func(ctx context.Context, cn *Conn, timeoutType string) {
+ metricCallbackMu.RLock()
+ cb := metricConnectionTimeoutCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
+func getMetricConnectionClosedCallback() func(ctx context.Context, cn *Conn, reason string, err error) {
+ metricCallbackMu.RLock()
+ cb := metricConnectionClosedCallback
+ metricCallbackMu.RUnlock()
+ return cb
+}
+
// Stats contains pool state information and accumulated stats.
type Stats struct {
Hits uint32 // number of times free connection was found in the pool
@@ -60,9 +232,10 @@ type Stats struct {
Unusable uint32 // number of times a connection was found to be unusable
WaitDurationNs int64 // total time spent for waiting a connection in nanoseconds
- TotalConns uint32 // number of total connections in the pool
- IdleConns uint32 // number of idle connections in the pool
- StaleConns uint32 // number of stale connections removed from the pool
+ TotalConns uint32 // number of total connections in the pool
+ IdleConns uint32 // number of idle connections in the pool
+ StaleConns uint32 // number of stale connections removed from the pool
+ PendingRequests uint32 // number of pending requests waiting for a connection
PubSubStats PubSubStats
}
@@ -110,6 +283,7 @@ type Options struct {
MaxActiveConns int32
ConnMaxIdleTime time.Duration
ConnMaxLifetime time.Duration
+ ConnMaxLifetimeJitter time.Duration
PushNotificationsEnabled bool
// DialerRetries is the maximum number of retry attempts when dialing fails.
@@ -119,6 +293,10 @@ type Options struct {
// DialerRetryTimeout is the backoff duration between retry attempts.
// Default: 100ms
DialerRetryTimeout time.Duration
+
+ // Name is a unique identifier for this pool, used in metrics.
+ // Format: addr_uniqueID (e.g., "localhost:6379_a1b2c3d4")
+ Name string
}
type lastDialErrorWrap struct {
@@ -240,9 +418,9 @@ func (p *ConnPool) checkMinIdleConns() {
for p.poolSize.Load() < p.cfg.PoolSize && p.idleConnsLen.Load() < p.cfg.MinIdleConns {
// Try to acquire a semaphore token
if !p.semaphore.TryAcquire() {
- // Semaphore is full, can't create more connections
- p.idleCheckInProgress.Store(false)
- return
+ // Semaphore is full, can't create more connections right now
+ // Break out of inner loop to check if we need to retry
+ break
}
p.poolSize.Add(1)
@@ -321,6 +499,12 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
return nil, ErrPoolExhausted
}
+ // Protect against nil context due to race condition in queuedNewConn
+ // where the context can be set to nil after timeout/cancellation
+ if ctx == nil {
+ ctx = context.Background()
+ }
+
dialCtx, cancel := context.WithTimeout(ctx, p.cfg.DialTimeout)
defer cancel()
cn, err := p.dialConn(dialCtx, pooled)
@@ -359,6 +543,11 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
}
}
+ // Notify metrics: new connection created and idle
+ if cb := getMetricConnectionStateChangeCallback(); cb != nil {
+ cb(ctx, cn, "", "idle")
+ }
+
return cn, nil
}
@@ -371,6 +560,14 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
return nil, p.getLastDialError()
}
+ // Record dial start time for connection creation metric
+ // This will be used after handshake completes in redis.go _getConn()
+ // Only call time.Now() if callback is registered to avoid overhead
+ var dialStartNs int64
+ if GetMetricConnectionCreateTimeCallback() != nil {
+ dialStartNs = time.Now().UnixNano()
+ }
+
// Retry dialing with backoff
// the context timeout is already handled by the context passed in
// so we may never reach the max retries, higher values don't hurt
@@ -404,14 +601,15 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
continue
}
- // Success - create connection
cn := NewConnWithBufferSize(netConn, p.cfg.ReadBufferSize, p.cfg.WriteBufferSize)
cn.pooled = pooled
- if p.cfg.ConnMaxLifetime > 0 {
- cn.expiresAt = time.Now().Add(p.cfg.ConnMaxLifetime)
- } else {
- cn.expiresAt = noExpiration
+ // Store dial start time only if we recorded it
+ if dialStartNs > 0 {
+ cn.dialStartNs.Store(dialStartNs)
}
+ cn.expiresAt = p.calcConnExpiresAt()
+ // Set pool name for metrics
+ cn.SetPoolName(p.cfg.Name)
return cn, nil
}
@@ -425,6 +623,25 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
return nil, lastErr
}
+// calcConnExpiresAt calculates the expiration time for a connection.
+// It applies random jitter to prevent all connections from expiring simultaneously,
+// avoiding the "thundering herd" problem where all connections expire at once.
+// Returns noExpiration if ConnMaxLifetime is not set.
+func (p *ConnPool) calcConnExpiresAt() time.Time {
+ if p.cfg.ConnMaxLifetime <= 0 {
+ return noExpiration
+ }
+
+ if p.cfg.ConnMaxLifetimeJitter <= 0 {
+ return time.Now().Add(p.cfg.ConnMaxLifetime)
+ }
+
+ jitter := p.cfg.ConnMaxLifetimeJitter
+ jitterRange := jitter.Nanoseconds() * 2
+ jitterNs := rand.Int63n(jitterRange) - jitter.Nanoseconds()
+ return time.Now().Add(p.cfg.ConnMaxLifetime + time.Duration(jitterNs))
+}
+
func (p *ConnPool) tryDial() {
for {
if p.closed() {
@@ -466,17 +683,44 @@ func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
}
// getConn returns a connection from the pool.
-func (p *ConnPool) getConn(ctx context.Context) (*Conn, error) {
- var cn *Conn
- var err error
-
+func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {
if p.closed() {
return nil, ErrClosed
}
- if err := p.waitTurn(ctx); err != nil {
+ // Track pending requests in pool stats
+ // NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.
+ atomic.AddUint32(&p.stats.PendingRequests, 1)
+ defer func() {
+ if err != nil {
+ // Failed to get connection, decrement pending requests
+ atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1
+ }
+ }()
+
+ // Track wait time - only call time.Now() if callback is registered
+ var waitStart time.Time
+ waitTimeCallback := getMetricConnectionWaitTimeCallback()
+ if waitTimeCallback != nil {
+ waitStart = time.Now()
+ }
+ if err = p.waitTurn(ctx); err != nil {
+ // Record timeout if applicable
+ if err == ErrPoolTimeout {
+ if cb := getMetricConnectionTimeoutCallback(); cb != nil {
+ cb(ctx, nil, "pool")
+ }
+ // Record general error metric for pool timeout
+ if cb := GetMetricErrorCallback(); cb != nil {
+ cb(ctx, "POOL_TIMEOUT", nil, "POOL_TIMEOUT", true, 0)
+ }
+ }
return nil, err
}
+ var waitDuration time.Duration
+ if waitTimeCallback != nil {
+ waitDuration = time.Since(waitStart)
+ }
// Use cached time for health checks (max 50ms staleness is acceptable)
nowNs := getCachedTimeNs()
@@ -507,10 +751,10 @@ func (p *ConnPool) getConn(ctx context.Context) (*Conn, error) {
// Process connection using the hooks system
// Combine error and rejection checks to reduce branches
if hookManager != nil {
- acceptConn, err := hookManager.ProcessOnGet(ctx, cn, false)
- if err != nil || !acceptConn {
- if err != nil {
- internal.Logger.Printf(ctx, "redis: connection pool: failed to process idle connection by hook: %v", err)
+ acceptConn, hookErr := hookManager.ProcessOnGet(ctx, cn, false)
+ if hookErr != nil || !acceptConn {
+ if hookErr != nil {
+ internal.Logger.Printf(ctx, "redis: connection pool: failed to process idle connection by hook: %v", hookErr)
_ = p.CloseConn(cn)
} else {
internal.Logger.Printf(ctx, "redis: connection pool: conn[%d] rejected by hook, returning to pool", cn.GetID())
@@ -524,19 +768,37 @@ func (p *ConnPool) getConn(ctx context.Context) (*Conn, error) {
}
atomic.AddUint32(&p.stats.Hits, 1)
+
+ // Notify metrics: connection moved from idle to used
+ if cb := getMetricConnectionStateChangeCallback(); cb != nil {
+ cb(ctx, cn, "idle", "used")
+ }
+
+ // Record wait time (use cached callback from above)
+ if waitTimeCallback != nil {
+ waitTimeCallback(ctx, waitDuration, cn)
+ }
+
+ // Decrement pending requests (connection acquired successfully)
+ // NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.
+ atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1
+
return cn, nil
}
atomic.AddUint32(&p.stats.Misses, 1)
- newcn, err := p.queuedNewConn(ctx)
+ var newcn *Conn
+ newcn, err = p.queuedNewConn(ctx)
if err != nil {
return nil, err
}
// Process connection using the hooks system
+ // This includes the handshake (HELLO/AUTH) via initConn hook
if hookManager != nil {
- acceptConn, err := hookManager.ProcessOnGet(ctx, newcn, true)
+ var acceptConn bool
+ acceptConn, err = hookManager.ProcessOnGet(ctx, newcn, true)
// both errors and accept=false mean a hook rejected the connection
// this should not happen with a new connection, but we handle it gracefully
if err != nil || !acceptConn {
@@ -546,6 +808,21 @@ func (p *ConnPool) getConn(ctx context.Context) (*Conn, error) {
return nil, err
}
}
+
+ // Notify metrics: new connection is created and used
+ if cb := getMetricConnectionStateChangeCallback(); cb != nil {
+ cb(ctx, newcn, "", "used")
+ }
+
+ // Record wait time (use cached callback from above)
+ if waitTimeCallback != nil {
+ waitTimeCallback(ctx, waitDuration, newcn)
+ }
+
+ // Decrement pending requests (connection acquired successfully)
+ // NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.
+ atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1
+
return newcn, nil
}
@@ -574,12 +851,15 @@ func (p *ConnPool) queuedNewConn(ctx context.Context) (*Conn, error) {
}
}()
+ p.dialsQueue.discardDoneAtFront()
p.dialsQueue.enqueue(w)
go func(w *wantConn) {
var freeTurnCalled bool
defer func() {
if err := recover(); err != nil {
+ w.tryDeliver(nil, errPanicInQueuedNewConn)
+ p.dialsQueue.discardDoneAtFront()
if !freeTurnCalled {
p.freeTurn()
}
@@ -594,12 +874,14 @@ func (p *ConnPool) queuedNewConn(ctx context.Context) (*Conn, error) {
cn, cnErr := p.newConn(dialCtx, true)
if cnErr != nil {
w.tryDeliver(nil, cnErr) // deliver error to caller, notify connection creation failed
+ p.dialsQueue.discardDoneAtFront()
p.freeTurn()
freeTurnCalled = true
return
}
delivered := w.tryDeliver(cn, cnErr)
+ p.dialsQueue.discardDoneAtFront()
if !delivered && p.putIdleConn(dialCtx, cn) {
p.freeTurn()
freeTurnCalled = true
@@ -695,7 +977,7 @@ func (p *ConnPool) popIdle() (*Conn, error) {
var cn *Conn
attempts := 0
- maxAttempts := util.Min(popAttempts, n)
+ maxAttempts := min(popAttempts, n)
for attempts < maxAttempts {
if len(p.idleConns) == 0 {
return nil, nil
@@ -756,6 +1038,15 @@ func (p *ConnPool) putConnWithoutTurn(ctx context.Context, cn *Conn) {
// putConn is the internal implementation of Put that optionally frees a turn.
func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
+ // Guard against nil connection
+ if cn == nil {
+ internal.Logger.Printf(ctx, "putConn called with nil connection")
+ if freeTurn {
+ p.freeTurn()
+ }
+ return
+ }
+
// Process connection using the hooks system
shouldPool := true
shouldRemove := false
@@ -806,7 +1097,14 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
if !transitionedToIdle {
// Fast path failed - hook might have changed state (e.g., to UNUSABLE for handoff)
// Keep the state set by the hook and pool the connection anyway
- currentState := cn.GetStateMachine().GetState()
+ sm := cn.GetStateMachine()
+ if sm == nil {
+ // State machine is nil - connection is in an invalid state, remove it
+ internal.Logger.Printf(ctx, "conn[%d] has nil state machine, removing it", cn.GetID())
+ p.removeConnInternal(ctx, cn, errConnNotPooled, freeTurn)
+ return
+ }
+ currentState := sm.GetState()
switch currentState {
case StateUnusable:
// expected state, don't log it
@@ -840,9 +1138,19 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
p.connsMu.Unlock()
p.idleConnsLen.Add(1)
}
+
+ // Notify metrics: connection moved from used to idle
+ if cb := getMetricConnectionStateChangeCallback(); cb != nil {
+ cb(ctx, cn, "used", "idle")
+ }
} else {
shouldCloseConn = true
p.removeConnWithLock(cn)
+
+ // Notify metrics: connection removed (used -> nothing)
+ if cb := getMetricConnectionStateChangeCallback(); cb != nil {
+ cb(ctx, cn, "used", "")
+ }
}
if freeTurn {
@@ -883,6 +1191,20 @@ func (p *ConnPool) removeConnInternal(ctx context.Context, cn *Conn, reason erro
p.freeTurn()
}
+ // Notify metrics: connection removed (assume from used state)
+ if cb := getMetricConnectionStateChangeCallback(); cb != nil {
+ cb(ctx, cn, "used", "")
+ }
+
+ // Record connection closed
+ if cb := getMetricConnectionClosedCallback(); cb != nil {
+ reasonStr := "unknown"
+ if reason != nil {
+ reasonStr = reason.Error()
+ }
+ cb(ctx, cn, reasonStr, reason)
+ }
+
_ = p.closeConn(cn)
// Check if we need to create new idle connections to maintain MinIdleConns
@@ -949,12 +1271,13 @@ func (p *ConnPool) Size() int {
func (p *ConnPool) Stats() *Stats {
return &Stats{
- Hits: atomic.LoadUint32(&p.stats.Hits),
- Misses: atomic.LoadUint32(&p.stats.Misses),
- Timeouts: atomic.LoadUint32(&p.stats.Timeouts),
- WaitCount: atomic.LoadUint32(&p.stats.WaitCount),
- Unusable: atomic.LoadUint32(&p.stats.Unusable),
- WaitDurationNs: p.waitDurationNs.Load(),
+ Hits: atomic.LoadUint32(&p.stats.Hits),
+ Misses: atomic.LoadUint32(&p.stats.Misses),
+ Timeouts: atomic.LoadUint32(&p.stats.Timeouts),
+ WaitCount: atomic.LoadUint32(&p.stats.WaitCount),
+ Unusable: atomic.LoadUint32(&p.stats.Unusable),
+ WaitDurationNs: p.waitDurationNs.Load(),
+ PendingRequests: atomic.LoadUint32(&p.stats.PendingRequests),
TotalConns: uint32(p.Len()),
IdleConns: uint32(p.IdleLen()),
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go
index 5b29659e..e566d42b 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go
@@ -44,9 +44,10 @@ func (p *PubSubPool) NewConn(ctx context.Context, network string, addr string, c
}
cn := NewConnWithBufferSize(netConn, p.opt.ReadBufferSize, p.opt.WriteBufferSize)
cn.pubsub = true
+ // Set pool name for metrics
+ cn.SetPoolName(p.opt.Name)
atomic.AddUint32(&p.stats.Created, 1)
return cn, nil
-
}
func (p *PubSubPool) TrackConn(cn *Conn) {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/want_conn.go b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/want_conn.go
index 6f9e4bfa..78f86813 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/pool/want_conn.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/pool/want_conn.go
@@ -6,7 +6,7 @@ import (
)
type wantConn struct {
- mu sync.Mutex // protects ctx, done and sending of the result
+ mu sync.RWMutex // protects ctx, done and sending of the result
ctx context.Context // context for dial, cleared after delivered or canceled
cancelCtx context.CancelFunc
done bool // true after delivered or canceled
@@ -15,8 +15,8 @@ type wantConn struct {
// getCtxForDial returns context for dial or nil if connection was delivered or canceled.
func (w *wantConn) getCtxForDial() context.Context {
- w.mu.Lock()
- defer w.mu.Unlock()
+ w.mu.RLock()
+ defer w.mu.RUnlock()
return w.ctx
}
@@ -57,6 +57,12 @@ func (w *wantConn) cancel() *Conn {
return cn
}
+func (w *wantConn) isOngoing() bool {
+ w.mu.RLock()
+ defer w.mu.RUnlock()
+ return !w.done
+}
+
type wantConnResult struct {
cn *Conn
err error
@@ -91,3 +97,19 @@ func (q *wantConnQueue) dequeue() (*wantConn, bool) {
q.items = q.items[1:]
return item, true
}
+
+func (q *wantConnQueue) discardDoneAtFront() int {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+ count := 0
+ for len(q.items) > 0 {
+ if q.items[0].isOngoing() {
+ break
+ }
+
+ q.items = q.items[1:]
+ count++
+ }
+
+ return count
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go b/backend/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go
index f553e2f9..a28240f5 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go
@@ -212,6 +212,25 @@ func NewOOMError(msg string) *OOMError {
return &OOMError{msg: msg}
}
+// NoReplicasError is returned when not enough replicas acknowledge a write.
+// This error occurs when using WAIT/WAITAOF commands or CLUSTER SETSLOT with
+// synchronous replication, and the required number of replicas cannot confirm
+// the write within the timeout period.
+type NoReplicasError struct {
+ msg string
+}
+
+func (e *NoReplicasError) Error() string {
+ return e.msg
+}
+
+func (e *NoReplicasError) RedisError() {}
+
+// NewNoReplicasError creates a new NoReplicasError with the given message.
+func NewNoReplicasError(msg string) *NoReplicasError {
+ return &NoReplicasError{msg: msg}
+}
+
// parseTypedRedisError parses a Redis error message and returns a typed error if applicable.
// This function maintains backward compatibility by keeping the same error messages.
func parseTypedRedisError(msg string) error {
@@ -235,6 +254,8 @@ func parseTypedRedisError(msg string) error {
return NewTryAgainError(msg)
case strings.HasPrefix(msg, "MASTERDOWN "):
return NewMasterDownError(msg)
+ case strings.HasPrefix(msg, "NOREPLICAS "):
+ return NewNoReplicasError(msg)
case msg == "ERR max number of clients reached":
return NewMaxClientsError(msg)
case strings.HasPrefix(msg, "NOAUTH "), strings.HasPrefix(msg, "WRONGPASS "), strings.Contains(msg, "unauthenticated"):
@@ -486,3 +507,21 @@ func IsOOMError(err error) bool {
// Fallback to string checking for backward compatibility
return strings.HasPrefix(err.Error(), "OOM ")
}
+
+// IsNoReplicasError checks if an error is a NoReplicasError, even if wrapped.
+func IsNoReplicasError(err error) bool {
+ if err == nil {
+ return false
+ }
+ var noReplicasErr *NoReplicasError
+ if errors.As(err, &noReplicasErr) {
+ return true
+ }
+ // Check if wrapped error is a RedisError with NOREPLICAS prefix
+ var redisErr RedisError
+ if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "NOREPLICAS ") {
+ return true
+ }
+ // Fallback to string checking for backward compatibility
+ return strings.HasPrefix(err.Error(), "NOREPLICAS ")
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/routing/aggregator.go b/backend/vendor/github.com/redis/go-redis/v9/internal/routing/aggregator.go
new file mode 100644
index 00000000..0d6321ec
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/routing/aggregator.go
@@ -0,0 +1,1000 @@
+package routing
+
+import (
+ "errors"
+ "fmt"
+ "math"
+ "sync"
+
+ "sync/atomic"
+
+ "github.com/redis/go-redis/v9/internal/util"
+ uberAtomic "go.uber.org/atomic"
+)
+
+var (
+ ErrMaxAggregation = errors.New("redis: no valid results to aggregate for max operation")
+ ErrMinAggregation = errors.New("redis: no valid results to aggregate for min operation")
+ ErrAndAggregation = errors.New("redis: no valid results to aggregate for logical AND operation")
+ ErrOrAggregation = errors.New("redis: no valid results to aggregate for logical OR operation")
+)
+
+// ResponseAggregator defines the interface for aggregating responses from multiple shards.
+type ResponseAggregator interface {
+ // Add processes a single shard response.
+ Add(result interface{}, err error) error
+
+ // AddWithKey processes a single shard response for a specific key (used by keyed aggregators).
+ AddWithKey(key string, result interface{}, err error) error
+
+ BatchAdd(map[string]AggregatorResErr) error
+
+ BatchSlice([]AggregatorResErr) error
+
+ // Result returns the final aggregated result and any error.
+ Result() (interface{}, error)
+}
+
+type AggregatorResErr struct {
+ Result interface{}
+ Err error
+}
+
+// NewResponseAggregator creates an aggregator based on the response policy.
+func NewResponseAggregator(policy ResponsePolicy, cmdName string) ResponseAggregator {
+ switch policy {
+ case RespDefaultKeyless:
+ return &DefaultKeylessAggregator{results: make([]interface{}, 0)}
+ case RespDefaultHashSlot:
+ return &DefaultKeyedAggregator{results: make(map[string]interface{})}
+ case RespAllSucceeded:
+ return &AllSucceededAggregator{}
+ case RespOneSucceeded:
+ return &OneSucceededAggregator{}
+ case RespAggSum:
+ return &AggSumAggregator{
+ // res:
+ }
+ case RespAggMin:
+ return &AggMinAggregator{
+ res: util.NewAtomicMin(),
+ }
+ case RespAggMax:
+ return &AggMaxAggregator{
+ res: util.NewAtomicMax(),
+ }
+ case RespAggLogicalAnd:
+ andAgg := &AggLogicalAndAggregator{}
+ andAgg.res.Store(true)
+
+ return andAgg
+ case RespAggLogicalOr:
+ return &AggLogicalOrAggregator{}
+ case RespSpecial:
+ return NewSpecialAggregator(cmdName)
+ default:
+ return &AllSucceededAggregator{}
+ }
+}
+
+func NewDefaultAggregator(isKeyed bool) ResponseAggregator {
+ if isKeyed {
+ return &DefaultKeyedAggregator{
+ results: make(map[string]interface{}),
+ }
+ }
+ return &DefaultKeylessAggregator{}
+}
+
+// AllSucceededAggregator returns one non-error reply if every shard succeeded,
+// propagates the first error otherwise.
+type AllSucceededAggregator struct {
+ err atomic.Value
+ res atomic.Value
+}
+
+func (a *AllSucceededAggregator) Add(result interface{}, err error) error {
+ if err != nil {
+ a.err.CompareAndSwap(nil, err)
+ return nil
+ }
+
+ if result != nil {
+ a.res.CompareAndSwap(nil, result)
+ }
+
+ return nil
+}
+
+func (a *AllSucceededAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ for _, res := range results {
+ err := a.Add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *AllSucceededAggregator) BatchSlice(results []AggregatorResErr) error {
+ for _, res := range results {
+ err := a.Add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *AllSucceededAggregator) Result() (interface{}, error) {
+ var err error
+ res, e := a.res.Load(), a.err.Load()
+ if e != nil {
+ err = e.(error)
+ }
+
+ return res, err
+}
+
+func (a *AllSucceededAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+// OneSucceededAggregator returns the first non-error reply,
+// if all shards errored, returns any one of those errors.
+type OneSucceededAggregator struct {
+ err atomic.Value
+ res atomic.Value
+}
+
+func (a *OneSucceededAggregator) Add(result interface{}, err error) error {
+ if err != nil {
+ a.err.CompareAndSwap(nil, err)
+ return nil
+ }
+
+ if result != nil {
+ a.res.CompareAndSwap(nil, result)
+ }
+
+ return nil
+}
+
+func (a *OneSucceededAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ for _, res := range results {
+ err := a.Add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err == nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *OneSucceededAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+func (a *OneSucceededAggregator) BatchSlice(results []AggregatorResErr) error {
+ for _, res := range results {
+ err := a.Add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err == nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *OneSucceededAggregator) Result() (interface{}, error) {
+ res, e := a.res.Load(), a.err.Load()
+ if res == nil {
+ return nil, e.(error)
+ }
+
+ return res, nil
+}
+
+// AggSumAggregator sums numeric replies from all shards.
+type AggSumAggregator struct {
+ err atomic.Value
+ res uberAtomic.Float64
+}
+
+func (a *AggSumAggregator) Add(result interface{}, err error) error {
+ if err != nil {
+ a.err.CompareAndSwap(nil, err)
+ }
+
+ if result != nil {
+ val, err := toFloat64(result)
+ if err != nil {
+ a.err.CompareAndSwap(nil, err)
+ return err
+ }
+ a.res.Add(val)
+ }
+
+ return nil
+}
+
+func (a *AggSumAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ var sum int64
+
+ for _, res := range results {
+ if res.Err != nil {
+ return a.Add(res.Result, res.Err)
+ }
+
+ intRes, err := toInt64(res.Result)
+ if err != nil {
+ return a.Add(nil, err)
+ }
+
+ sum += intRes
+ }
+
+ return a.Add(sum, nil)
+}
+
+func (a *AggSumAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+func (a *AggSumAggregator) BatchSlice(results []AggregatorResErr) error {
+ var sum int64
+
+ for _, res := range results {
+ if res.Err != nil {
+ return a.Add(res.Result, res.Err)
+ }
+
+ intRes, err := toInt64(res.Result)
+ if err != nil {
+ return a.Add(nil, err)
+ }
+
+ sum += intRes
+ }
+
+ return a.Add(sum, nil)
+}
+
+func (a *AggSumAggregator) Result() (interface{}, error) {
+ res, err := a.res.Load(), a.err.Load()
+ if err != nil {
+ return nil, err.(error)
+ }
+
+ return res, nil
+}
+
+// AggMinAggregator returns the minimum numeric value from all shards.
+type AggMinAggregator struct {
+ err atomic.Value
+ res *util.AtomicMin
+}
+
+func (a *AggMinAggregator) Add(result interface{}, err error) error {
+ if err != nil {
+ a.err.CompareAndSwap(nil, err)
+ return nil
+ }
+
+ floatVal, e := toFloat64(result)
+ if e != nil {
+ a.err.CompareAndSwap(nil, err)
+ return nil
+ }
+
+ a.res.Value(floatVal)
+
+ return nil
+}
+
+func (a *AggMinAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ min := int64(math.MaxInt64)
+
+ for _, res := range results {
+ if res.Err != nil {
+ _ = a.Add(nil, res.Err)
+ return nil
+ }
+
+ resInt, err := toInt64(res.Result)
+ if err != nil {
+ _ = a.Add(nil, res.Err)
+ return nil
+ }
+
+ if resInt < min {
+ min = resInt
+ }
+
+ }
+
+ return a.Add(min, nil)
+}
+
+func (a *AggMinAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+func (a *AggMinAggregator) BatchSlice(results []AggregatorResErr) error {
+ min := float64(math.MaxFloat64)
+
+ for _, res := range results {
+ if res.Err != nil {
+ _ = a.Add(nil, res.Err)
+ return nil
+ }
+
+ floatVal, err := toFloat64(res.Result)
+ if err != nil {
+ _ = a.Add(nil, res.Err)
+ return nil
+ }
+
+ if floatVal < min {
+ min = floatVal
+ }
+
+ }
+
+ return a.Add(min, nil)
+}
+
+func (a *AggMinAggregator) Result() (interface{}, error) {
+ err := a.err.Load()
+ if err != nil {
+ return nil, err.(error)
+ }
+
+ val, hasVal := a.res.Min()
+ if !hasVal {
+ return nil, ErrMinAggregation
+ }
+ return val, nil
+}
+
+// AggMaxAggregator returns the maximum numeric value from all shards.
+type AggMaxAggregator struct {
+ err atomic.Value
+ res *util.AtomicMax
+}
+
+func (a *AggMaxAggregator) Add(result interface{}, err error) error {
+ if err != nil {
+ a.err.CompareAndSwap(nil, err)
+ return nil
+ }
+
+ floatVal, e := toFloat64(result)
+ if e != nil {
+ a.err.CompareAndSwap(nil, err)
+ return nil
+ }
+
+ a.res.Value(floatVal)
+
+ return nil
+}
+
+func (a *AggMaxAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ max := int64(math.MinInt64)
+
+ for _, res := range results {
+ if res.Err != nil {
+ _ = a.Add(nil, res.Err)
+ return nil
+ }
+
+ resInt, err := toInt64(res.Result)
+ if err != nil {
+ _ = a.Add(nil, res.Err)
+ return nil
+ }
+
+ if resInt > max {
+ max = resInt
+ }
+
+ }
+
+ return a.Add(max, nil)
+}
+
+func (a *AggMaxAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+func (a *AggMaxAggregator) BatchSlice(results []AggregatorResErr) error {
+ max := int64(math.MinInt64)
+
+ for _, res := range results {
+ if res.Err != nil {
+ _ = a.Add(nil, res.Err)
+ return nil
+ }
+
+ resInt, err := toInt64(res.Result)
+ if err != nil {
+ _ = a.Add(nil, res.Err)
+ return nil
+ }
+
+ if resInt > max {
+ max = resInt
+ }
+
+ }
+
+ return a.Add(max, nil)
+}
+
+func (a *AggMaxAggregator) Result() (interface{}, error) {
+ err := a.err.Load()
+ if err != nil {
+ return nil, err.(error)
+ }
+
+ val, hasVal := a.res.Max()
+ if !hasVal {
+ return nil, ErrMaxAggregation
+ }
+ return val, nil
+}
+
+// AggLogicalAndAggregator performs logical AND on boolean values.
+type AggLogicalAndAggregator struct {
+ err atomic.Value
+ res atomic.Bool
+ hasResult atomic.Bool
+}
+
+func (a *AggLogicalAndAggregator) Add(result interface{}, err error) error {
+ if err != nil {
+ a.err.CompareAndSwap(nil, err)
+ return nil
+ }
+
+ val, e := toBool(result)
+ if e != nil {
+ a.err.CompareAndSwap(nil, e)
+ return e
+ }
+
+ // Atomic AND operation: if val is false, result is always false
+ if !val {
+ a.res.Store(false)
+ }
+
+ a.hasResult.Store(true)
+
+ return nil
+}
+
+func (a *AggLogicalAndAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ result := true
+
+ for _, res := range results {
+ if res.Err != nil {
+ return a.Add(nil, res.Err)
+ }
+
+ boolRes, err := toBool(res.Result)
+ if err != nil {
+ return a.Add(nil, err)
+ }
+
+ result = result && boolRes
+ }
+
+ return a.Add(result, nil)
+}
+
+func (a *AggLogicalAndAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+func (a *AggLogicalAndAggregator) BatchSlice(results []AggregatorResErr) error {
+ result := true
+
+ for _, res := range results {
+ if res.Err != nil {
+ return a.Add(nil, res.Err)
+ }
+
+ boolRes, err := toBool(res.Result)
+ if err != nil {
+ return a.Add(nil, err)
+ }
+
+ result = result && boolRes
+ }
+
+ return a.Add(result, nil)
+}
+
+func (a *AggLogicalAndAggregator) Result() (interface{}, error) {
+ err := a.err.Load()
+ if err != nil {
+ return nil, err.(error)
+ }
+
+ if !a.hasResult.Load() {
+ return nil, ErrAndAggregation
+ }
+ return a.res.Load(), nil
+}
+
+// AggLogicalOrAggregator performs logical OR on boolean values.
+type AggLogicalOrAggregator struct {
+ err atomic.Value
+ res atomic.Bool
+ hasResult atomic.Bool
+}
+
+func (a *AggLogicalOrAggregator) Add(result interface{}, err error) error {
+ if err != nil {
+ a.err.CompareAndSwap(nil, err)
+ return nil
+ }
+
+ val, e := toBool(result)
+ if e != nil {
+ a.err.CompareAndSwap(nil, e)
+ return e
+ }
+
+ // Atomic OR operation: if val is true, result is always true
+ if val {
+ a.res.Store(true)
+ }
+
+ a.hasResult.Store(true)
+
+ return nil
+}
+
+func (a *AggLogicalOrAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ result := false
+
+ for _, res := range results {
+ if res.Err != nil {
+ return a.Add(nil, res.Err)
+ }
+
+ boolRes, err := toBool(res.Result)
+ if err != nil {
+ return a.Add(nil, err)
+ }
+
+ result = result || boolRes
+ }
+
+ return a.Add(result, nil)
+}
+
+func (a *AggLogicalOrAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+func (a *AggLogicalOrAggregator) BatchSlice(results []AggregatorResErr) error {
+ result := false
+
+ for _, res := range results {
+ if res.Err != nil {
+ return a.Add(nil, res.Err)
+ }
+
+ boolRes, err := toBool(res.Result)
+ if err != nil {
+ return a.Add(nil, err)
+ }
+
+ result = result || boolRes
+ }
+
+ return a.Add(result, nil)
+}
+
+func (a *AggLogicalOrAggregator) Result() (interface{}, error) {
+ err := a.err.Load()
+ if err != nil {
+ return nil, err.(error)
+ }
+
+ if !a.hasResult.Load() {
+ return nil, ErrOrAggregation
+ }
+ return a.res.Load(), nil
+}
+
+func toInt64(val interface{}) (int64, error) {
+ if val == nil {
+ return 0, nil
+ }
+ switch v := val.(type) {
+ case int64:
+ return v, nil
+ case int:
+ return int64(v), nil
+ case int32:
+ return int64(v), nil
+ case float64:
+ if v != math.Trunc(v) {
+ return 0, fmt.Errorf("cannot convert float %f to int64", v)
+ }
+ return int64(v), nil
+ default:
+ return 0, fmt.Errorf("cannot convert %T to int64", val)
+ }
+}
+
+func toFloat64(val interface{}) (float64, error) {
+ if val == nil {
+ return 0, nil
+ }
+
+ switch v := val.(type) {
+ case float64:
+ return v, nil
+ case int:
+ return float64(v), nil
+ case int32:
+ return float64(v), nil
+ case int64:
+ return float64(v), nil
+ case float32:
+ return float64(v), nil
+ default:
+ return 0, fmt.Errorf("cannot convert %T to float64", val)
+ }
+}
+
+func toBool(val interface{}) (bool, error) {
+ if val == nil {
+ return false, nil
+ }
+ switch v := val.(type) {
+ case bool:
+ return v, nil
+ case int64:
+ return v != 0, nil
+ case int:
+ return v != 0, nil
+ default:
+ return false, fmt.Errorf("cannot convert %T to bool", val)
+ }
+}
+
+// DefaultKeylessAggregator collects all results in an array, order doesn't matter.
+type DefaultKeylessAggregator struct {
+ mu sync.Mutex
+ results []interface{}
+ firstErr error
+}
+
+func (a *DefaultKeylessAggregator) add(result interface{}, err error) error {
+ if err != nil && a.firstErr == nil {
+ a.firstErr = err
+ return nil
+ }
+ if err == nil {
+ a.results = append(a.results, result)
+ }
+ return nil
+}
+
+func (a *DefaultKeylessAggregator) Add(result interface{}, err error) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ return a.add(result, err)
+}
+
+func (a *DefaultKeylessAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for _, res := range results {
+ err := a.add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *DefaultKeylessAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+func (a *DefaultKeylessAggregator) BatchSlice(results []AggregatorResErr) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for _, res := range results {
+ err := a.add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *DefaultKeylessAggregator) Result() (interface{}, error) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ if a.firstErr != nil {
+ return nil, a.firstErr
+ }
+ return a.results, nil
+}
+
+// DefaultKeyedAggregator reassembles replies in the exact key order of the original request.
+type DefaultKeyedAggregator struct {
+ mu sync.Mutex
+ results map[string]interface{}
+ keyOrder []string
+ firstErr error
+}
+
+func NewDefaultKeyedAggregator(keyOrder []string) *DefaultKeyedAggregator {
+ return &DefaultKeyedAggregator{
+ results: make(map[string]interface{}),
+ keyOrder: keyOrder,
+ }
+}
+
+func (a *DefaultKeyedAggregator) add(result interface{}, err error) error {
+ if err != nil && a.firstErr == nil {
+ a.firstErr = err
+ return nil
+ }
+ // For non-keyed Add, just collect the result without ordering
+ if err == nil {
+ a.results["__default__"] = result
+ }
+ return nil
+}
+
+func (a *DefaultKeyedAggregator) Add(result interface{}, err error) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ return a.add(result, err)
+}
+
+func (a *DefaultKeyedAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for _, res := range results {
+ err := a.add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *DefaultKeyedAggregator) addWithKey(key string, result interface{}, err error) error {
+ if err != nil && a.firstErr == nil {
+ a.firstErr = err
+ return nil
+ }
+ if err == nil {
+ a.results[key] = result
+ }
+ return nil
+}
+
+func (a *DefaultKeyedAggregator) AddWithKey(key string, result interface{}, err error) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ return a.addWithKey(key, result, err)
+}
+
+func (a *DefaultKeyedAggregator) BatchAddWithKeyOrder(results map[string]AggregatorResErr, keyOrder []string) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ a.keyOrder = keyOrder
+ for key, res := range results {
+ err := a.addWithKey(key, res.Result, res.Err)
+ if err != nil {
+ return nil
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *DefaultKeyedAggregator) SetKeyOrder(keyOrder []string) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ a.keyOrder = keyOrder
+}
+
+func (a *DefaultKeyedAggregator) BatchSlice(results []AggregatorResErr) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for _, res := range results {
+ err := a.add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *DefaultKeyedAggregator) Result() (interface{}, error) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ if a.firstErr != nil {
+ return nil, a.firstErr
+ }
+
+ // If no explicit key order is set, return results in any order
+ if len(a.keyOrder) == 0 {
+ orderedResults := make([]interface{}, 0, len(a.results))
+ for _, result := range a.results {
+ orderedResults = append(orderedResults, result)
+ }
+ return orderedResults, nil
+ }
+
+ // Return results in the exact key order
+ orderedResults := make([]interface{}, len(a.keyOrder))
+ for i, key := range a.keyOrder {
+ if result, exists := a.results[key]; exists {
+ orderedResults[i] = result
+ }
+ }
+ return orderedResults, nil
+}
+
+// SpecialAggregator provides a registry for command-specific aggregation logic.
+type SpecialAggregator struct {
+ mu sync.Mutex
+ aggregatorFunc func([]interface{}, []error) (interface{}, error)
+ results []interface{}
+ errors []error
+}
+
+func (a *SpecialAggregator) add(result interface{}, err error) error {
+ a.results = append(a.results, result)
+ a.errors = append(a.errors, err)
+ return nil
+}
+
+func (a *SpecialAggregator) Add(result interface{}, err error) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ return a.add(result, err)
+}
+
+func (a *SpecialAggregator) BatchAdd(results map[string]AggregatorResErr) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for _, res := range results {
+ err := a.add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *SpecialAggregator) AddWithKey(key string, result interface{}, err error) error {
+ return a.Add(result, err)
+}
+
+func (a *SpecialAggregator) BatchSlice(results []AggregatorResErr) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for _, res := range results {
+ err := a.add(res.Result, res.Err)
+ if err != nil {
+ return err
+ }
+
+ if res.Err != nil {
+ return nil
+ }
+ }
+
+ return nil
+}
+
+func (a *SpecialAggregator) Result() (interface{}, error) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ if a.aggregatorFunc != nil {
+ return a.aggregatorFunc(a.results, a.errors)
+ }
+ // Default behavior: return first non-error result or first error
+ for i, err := range a.errors {
+ if err == nil {
+ return a.results[i], nil
+ }
+ }
+ if len(a.errors) > 0 {
+ return nil, a.errors[0]
+ }
+ return nil, nil
+}
+
+// SpecialAggregatorRegistry holds custom aggregation functions for specific commands.
+var SpecialAggregatorRegistry = make(map[string]func([]interface{}, []error) (interface{}, error))
+
+// RegisterSpecialAggregator registers a custom aggregation function for a command.
+func RegisterSpecialAggregator(cmdName string, fn func([]interface{}, []error) (interface{}, error)) {
+ SpecialAggregatorRegistry[cmdName] = fn
+}
+
+// NewSpecialAggregator creates a special aggregator with command-specific logic if available.
+func NewSpecialAggregator(cmdName string) *SpecialAggregator {
+ agg := &SpecialAggregator{}
+ if fn, exists := SpecialAggregatorRegistry[cmdName]; exists {
+ agg.aggregatorFunc = fn
+ }
+ return agg
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/routing/policy.go b/backend/vendor/github.com/redis/go-redis/v9/internal/routing/policy.go
new file mode 100644
index 00000000..7f784b50
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/routing/policy.go
@@ -0,0 +1,144 @@
+package routing
+
+import (
+ "fmt"
+ "strings"
+)
+
+type RequestPolicy uint8
+
+const (
+ ReqDefault RequestPolicy = iota
+
+ ReqAllNodes
+
+ ReqAllShards
+
+ ReqMultiShard
+
+ ReqSpecial
+)
+
+const (
+ ReadOnlyCMD string = "readonly"
+)
+
+func (p RequestPolicy) String() string {
+ switch p {
+ case ReqDefault:
+ return "default"
+ case ReqAllNodes:
+ return "all_nodes"
+ case ReqAllShards:
+ return "all_shards"
+ case ReqMultiShard:
+ return "multi_shard"
+ case ReqSpecial:
+ return "special"
+ default:
+ return fmt.Sprintf("unknown_request_policy(%d)", p)
+ }
+}
+
+func ParseRequestPolicy(raw string) (RequestPolicy, error) {
+ switch strings.ToLower(raw) {
+ case "", "default", "none":
+ return ReqDefault, nil
+ case "all_nodes":
+ return ReqAllNodes, nil
+ case "all_shards":
+ return ReqAllShards, nil
+ case "multi_shard":
+ return ReqMultiShard, nil
+ case "special":
+ return ReqSpecial, nil
+ default:
+ return ReqDefault, fmt.Errorf("routing: unknown request_policy %q", raw)
+ }
+}
+
+type ResponsePolicy uint8
+
+const (
+ RespDefaultKeyless ResponsePolicy = iota
+ RespDefaultHashSlot
+ RespAllSucceeded
+ RespOneSucceeded
+ RespAggSum
+ RespAggMin
+ RespAggMax
+ RespAggLogicalAnd
+ RespAggLogicalOr
+ RespSpecial
+)
+
+func (p ResponsePolicy) String() string {
+ switch p {
+ case RespDefaultKeyless:
+ return "default(keyless)"
+ case RespDefaultHashSlot:
+ return "default(hashslot)"
+ case RespAllSucceeded:
+ return "all_succeeded"
+ case RespOneSucceeded:
+ return "one_succeeded"
+ case RespAggSum:
+ return "agg_sum"
+ case RespAggMin:
+ return "agg_min"
+ case RespAggMax:
+ return "agg_max"
+ case RespAggLogicalAnd:
+ return "agg_logical_and"
+ case RespAggLogicalOr:
+ return "agg_logical_or"
+ case RespSpecial:
+ return "special"
+ default:
+ return "all_succeeded"
+ }
+}
+
+func ParseResponsePolicy(raw string) (ResponsePolicy, error) {
+ switch strings.ToLower(raw) {
+ case "default(keyless)":
+ return RespDefaultKeyless, nil
+ case "default(hashslot)":
+ return RespDefaultHashSlot, nil
+ case "all_succeeded":
+ return RespAllSucceeded, nil
+ case "one_succeeded":
+ return RespOneSucceeded, nil
+ case "agg_sum":
+ return RespAggSum, nil
+ case "agg_min":
+ return RespAggMin, nil
+ case "agg_max":
+ return RespAggMax, nil
+ case "agg_logical_and":
+ return RespAggLogicalAnd, nil
+ case "agg_logical_or":
+ return RespAggLogicalOr, nil
+ case "special":
+ return RespSpecial, nil
+ default:
+ return RespDefaultKeyless, fmt.Errorf("routing: unknown response_policy %q", raw)
+ }
+}
+
+type CommandPolicy struct {
+ Request RequestPolicy
+ Response ResponsePolicy
+ // Tips that are not request_policy or response_policy
+ // e.g nondeterministic_output, nondeterministic_output_order.
+ Tips map[string]string
+}
+
+func (p *CommandPolicy) CanBeUsedInPipeline() bool {
+ return p.Request != ReqAllNodes && p.Request != ReqAllShards && p.Request != ReqMultiShard
+}
+
+func (p *CommandPolicy) IsReadOnly() bool {
+ _, readOnly := p.Tips[ReadOnlyCMD]
+ return readOnly
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/routing/shard_picker.go b/backend/vendor/github.com/redis/go-redis/v9/internal/routing/shard_picker.go
new file mode 100644
index 00000000..8e6228dd
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/routing/shard_picker.go
@@ -0,0 +1,57 @@
+package routing
+
+import (
+ "math/rand"
+ "sync/atomic"
+)
+
+// ShardPicker chooses “one arbitrary shard” when the request_policy is
+// ReqDefault and the command has no keys.
+type ShardPicker interface {
+ Next(total int) int // returns an index in [0,total)
+}
+
+// StaticShardPicker always returns the same shard index.
+type StaticShardPicker struct {
+ index int
+}
+
+func NewStaticShardPicker(index int) *StaticShardPicker {
+ return &StaticShardPicker{index: index}
+}
+
+func (p *StaticShardPicker) Next(total int) int {
+ if total == 0 || p.index >= total {
+ return 0
+ }
+ return p.index
+}
+
+/*───────────────────────────────
+ Round-robin (default)
+────────────────────────────────*/
+
+type RoundRobinPicker struct {
+ cnt atomic.Uint32
+}
+
+func (p *RoundRobinPicker) Next(total int) int {
+ if total == 0 {
+ return 0
+ }
+ i := p.cnt.Add(1)
+ return int(i-1) % total
+}
+
+/*───────────────────────────────
+ Random
+────────────────────────────────*/
+
+type RandomPicker struct{}
+
+func (RandomPicker) Next(total int) int {
+ if total == 0 {
+ return 0
+ }
+ return rand.Intn(total)
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/semaphore.go b/backend/vendor/github.com/redis/go-redis/v9/internal/semaphore.go
index a1dfca5f..a7f40466 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/semaphore.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/semaphore.go
@@ -190,4 +190,4 @@ func (s *FIFOSemaphore) Close() {
// Len returns the current number of acquired tokens.
func (s *FIFOSemaphore) Len() int32 {
return s.max - int32(len(s.tokens))
-}
\ No newline at end of file
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/util/atomic_max.go b/backend/vendor/github.com/redis/go-redis/v9/internal/util/atomic_max.go
new file mode 100644
index 00000000..6c621ba8
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/util/atomic_max.go
@@ -0,0 +1,97 @@
+/*
+© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/)
+ISC License
+
+Modified by htemelski-redis
+Removed the treshold, adapted it to work with float64
+*/
+
+package util
+
+import (
+ "math"
+
+ "go.uber.org/atomic"
+)
+
+// AtomicMax is a thread-safe max container
+// - hasValue indicator true if a value was equal to or greater than threshold
+// - optional threshold for minimum accepted max value
+// - if threshold is not used, initialization-free
+// - —
+// - wait-free CompareAndSwap mechanic
+type AtomicMax struct {
+
+ // value is current max
+ value atomic.Float64
+ // whether [AtomicMax.Value] has been invoked
+ // with value equal or greater to threshold
+ hasValue atomic.Bool
+}
+
+// NewAtomicMax returns a thread-safe max container
+// - if threshold is not used, AtomicMax is initialization-free
+func NewAtomicMax() (atomicMax *AtomicMax) {
+ m := AtomicMax{}
+ m.value.Store((-math.MaxFloat64))
+ return &m
+}
+
+// Value updates the container with a possible max value
+// - isNewMax is true if:
+// - — value is equal to or greater than any threshold and
+// - — invocation recorded the first 0 or
+// - — a new max
+// - upon return, Max and Max1 are guaranteed to reflect the invocation
+// - the return order of concurrent Value invocations is not guaranteed
+// - Thread-safe
+func (m *AtomicMax) Value(value float64) (isNewMax bool) {
+ // -math.MaxFloat64 as max case
+ var hasValue0 = m.hasValue.Load()
+ if value == (-math.MaxFloat64) {
+ if !hasValue0 {
+ isNewMax = m.hasValue.CompareAndSwap(false, true)
+ }
+ return // -math.MaxFloat64 as max: isNewMax true for first 0 writer
+ }
+
+ // check against present value
+ var current = m.value.Load()
+ if isNewMax = value > current; !isNewMax {
+ return // not a new max return: isNewMax false
+ }
+
+ // store the new max
+ for {
+
+ // try to write value to *max
+ if isNewMax = m.value.CompareAndSwap(current, value); isNewMax {
+ if !hasValue0 {
+ // may be rarely written multiple times
+ // still faster than CompareAndSwap
+ m.hasValue.Store(true)
+ }
+ return // new max written return: isNewMax true
+ }
+ if current = m.value.Load(); current >= value {
+ return // no longer a need to write return: isNewMax false
+ }
+ }
+}
+
+// Max returns current max and value-present flag
+// - hasValue true indicates that value reflects a Value invocation
+// - hasValue false: value is zero-value
+// - Thread-safe
+func (m *AtomicMax) Max() (value float64, hasValue bool) {
+ if hasValue = m.hasValue.Load(); !hasValue {
+ return
+ }
+ value = m.value.Load()
+ return
+}
+
+// Max1 returns current maximum whether zero-value or set by Value
+// - threshold is ignored
+// - Thread-safe
+func (m *AtomicMax) Max1() (value float64) { return m.value.Load() }
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/util/atomic_min.go b/backend/vendor/github.com/redis/go-redis/v9/internal/util/atomic_min.go
new file mode 100644
index 00000000..e33d29cc
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/util/atomic_min.go
@@ -0,0 +1,96 @@
+package util
+
+/*
+© 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/)
+ISC License
+
+Modified by htemelski-redis
+Adapted from the modified atomic_max, but with inverted logic
+*/
+
+import (
+ "math"
+
+ "go.uber.org/atomic"
+)
+
+// AtomicMin is a thread-safe Min container
+// - hasValue indicator true if a value was equal to or greater than threshold
+// - optional threshold for minimum accepted Min value
+// - —
+// - wait-free CompareAndSwap mechanic
+type AtomicMin struct {
+
+ // value is current Min
+ value atomic.Float64
+ // whether [AtomicMin.Value] has been invoked
+ // with value equal or greater to threshold
+ hasValue atomic.Bool
+}
+
+// NewAtomicMin returns a thread-safe Min container
+// - if threshold is not used, AtomicMin is initialization-free
+func NewAtomicMin() (atomicMin *AtomicMin) {
+ m := AtomicMin{}
+ m.value.Store(math.MaxFloat64)
+ return &m
+}
+
+// Value updates the container with a possible Min value
+// - isNewMin is true if:
+// - — value is equal to or greater than any threshold and
+// - — invocation recorded the first 0 or
+// - — a new Min
+// - upon return, Min and Min1 are guaranteed to reflect the invocation
+// - the return order of concurrent Value invocations is not guaranteed
+// - Thread-safe
+func (m *AtomicMin) Value(value float64) (isNewMin bool) {
+ // math.MaxFloat64 as Min case
+ var hasValue0 = m.hasValue.Load()
+ if value == math.MaxFloat64 {
+ if !hasValue0 {
+ isNewMin = m.hasValue.CompareAndSwap(false, true)
+ }
+ return // math.MaxFloat64 as Min: isNewMin true for first 0 writer
+ }
+
+ // check against present value
+ var current = m.value.Load()
+ if isNewMin = value < current; !isNewMin {
+ return // not a new Min return: isNewMin false
+ }
+
+ // store the new Min
+ for {
+
+ // try to write value to *Min
+ if isNewMin = m.value.CompareAndSwap(current, value); isNewMin {
+ if !hasValue0 {
+ // may be rarely written multiple times
+ // still faster than CompareAndSwap
+ m.hasValue.Store(true)
+ }
+ return // new Min written return: isNewMin true
+ }
+ if current = m.value.Load(); current <= value {
+ return // no longer a need to write return: isNewMin false
+ }
+ }
+}
+
+// Min returns current min and value-present flag
+// - hasValue true indicates that value reflects a Value invocation
+// - hasValue false: value is zero-value
+// - Thread-safe
+func (m *AtomicMin) Min() (value float64, hasValue bool) {
+ if hasValue = m.hasValue.Load(); !hasValue {
+ return
+ }
+ value = m.value.Load()
+ return
+}
+
+// Min1 returns current Minimum whether zero-value or set by Value
+// - threshold is ignored
+// - Thread-safe
+func (m *AtomicMin) Min1() (value float64) { return m.value.Load() }
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/util/math.go b/backend/vendor/github.com/redis/go-redis/v9/internal/util/math.go
deleted file mode 100644
index e707c47a..00000000
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/util/math.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package util
-
-// Max returns the maximum of two integers
-func Max(a, b int) int {
- if a > b {
- return a
- }
- return b
-}
-
-// Min returns the minimum of two integers
-func Min(a, b int) int {
- if a < b {
- return a
- }
- return b
-}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go b/backend/vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go
index cbcd2cc0..f4c3c3f3 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/internal/util/unsafe.go
@@ -8,15 +8,10 @@ import (
// BytesToString converts byte slice to string.
func BytesToString(b []byte) string {
- return *(*string)(unsafe.Pointer(&b))
+ return unsafe.String(unsafe.SliceData(b), len(b))
}
// StringToBytes converts string to byte slice.
func StringToBytes(s string) []byte {
- return *(*[]byte)(unsafe.Pointer(
- &struct {
- string
- Cap int
- }{s, len(s)},
- ))
+ return unsafe.Slice(unsafe.StringData(s), len(s))
}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/json.go b/backend/vendor/github.com/redis/go-redis/v9/json.go
index 2b9fa527..781cc468 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/json.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/json.go
@@ -68,8 +68,9 @@ var _ Cmder = (*JSONCmd)(nil)
func newJSONCmd(ctx context.Context, args ...interface{}) *JSONCmd {
return &JSONCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeJSON,
},
}
}
@@ -165,6 +166,14 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *JSONCmd) Clone() Cmder {
+ return &JSONCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val,
+ expanded: cmd.expanded, // interface{} can be shared as it should be immutable after parsing
+ }
+}
+
// -------------------------------------------
type JSONSliceCmd struct {
@@ -175,8 +184,9 @@ type JSONSliceCmd struct {
func NewJSONSliceCmd(ctx context.Context, args ...interface{}) *JSONSliceCmd {
return &JSONSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeJSONSlice,
},
}
}
@@ -233,6 +243,18 @@ func (cmd *JSONSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *JSONSliceCmd) Clone() Cmder {
+ var val []interface{}
+ if cmd.val != nil {
+ val = make([]interface{}, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &JSONSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
/*******************************************************************************
*
* IntPointerSliceCmd
@@ -249,8 +271,9 @@ type IntPointerSliceCmd struct {
func NewIntPointerSliceCmd(ctx context.Context, args ...interface{}) *IntPointerSliceCmd {
return &IntPointerSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeIntPointerSlice,
},
}
}
@@ -290,6 +313,18 @@ func (cmd *IntPointerSliceCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *IntPointerSliceCmd) Clone() Cmder {
+ var val []*int64
+ if cmd.val != nil {
+ val = make([]*int64, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &IntPointerSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
//------------------------------------------------------------------------------
// JSONArrAppend adds the provided JSON values to the end of the array at the given path.
diff --git a/backend/vendor/github.com/redis/go-redis/v9/list_commands.go b/backend/vendor/github.com/redis/go-redis/v9/list_commands.go
index 24a0de08..9d9e16c6 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/list_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/list_commands.go
@@ -77,6 +77,10 @@ func (c cmdable) BRPop(ctx context.Context, timeout time.Duration, keys ...strin
return cmd
}
+// BRPopLPush pops an element from a list, pushes it to another list and returns it.
+// Blocks until an element is available or timeout is reached.
+//
+// Deprecated: Use BLMove with RIGHT and LEFT arguments instead as of Redis 6.2.0.
func (c cmdable) BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd {
cmd := NewStringCmd(
ctx,
@@ -247,6 +251,10 @@ func (c cmdable) RPopCount(ctx context.Context, key string, count int) *StringSl
return cmd
}
+// RPopLPush atomically returns and removes the last element of the source list,
+// and pushes the element as the first element of the destination list.
+//
+// Deprecated: Use LMove with RIGHT and LEFT arguments instead as of Redis 6.2.0.
func (c cmdable) RPopLPush(ctx context.Context, source, destination string) *StringCmd {
cmd := NewStringCmd(ctx, "rpoplpush", source, destination)
_ = c(ctx, cmd)
diff --git a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/FEATURES.md b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/FEATURES.md
index caa4f705..03bbd391 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/FEATURES.md
+++ b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/FEATURES.md
@@ -156,18 +156,16 @@ Capped by: min(MaxActiveConns + 1, 5 × PoolSize)
### Client Support
#### Currently Supported
-- **Standalone Client** (`redis.NewClient`)
+- **Standalone Client** (`redis.NewClient`) - Full support for MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER notifications
+- **Cluster Client** (`redis.NewClusterClient`) - Support for SMIGRATING and SMIGRATED notifications for hitless slot migrations
-#### Planned Support
-- **Cluster Client** (not yet supported)
-
#### Will Not Support
- **Failover Client** (no planned support)
- **Ring Client** (no planned support)
## Migration Guide
-### Enabling Maintenance Notifications
+### Enabling Maintenance Notifications (Standalone Client)
**Before:**
```go
@@ -188,6 +186,26 @@ client := redis.NewClient(&redis.Options{
})
```
+### Enabling Hitless Upgrades (Cluster Client)
+
+For Redis Cluster with hitless slot migration support:
+
+```go
+client := redis.NewClusterClient(&redis.ClusterOptions{
+ Addrs: []string{"localhost:7000", "localhost:7001", "localhost:7002"},
+ Protocol: 3, // RESP3 required for push notifications
+ MaintNotificationsConfig: &maintnotifications.Config{
+ Mode: maintnotifications.ModeAuto,
+ RelaxedTimeout: 10 * time.Second, // Extended timeout during slot migrations
+ },
+})
+```
+
+The cluster client automatically handles:
+- **SMIGRATING**: Relaxes timeouts when slots are being migrated
+- **SMIGRATED**: Triggers lazy cluster state reload when migration completes
+- **SeqID Deduplication**: Same notification from multiple nodes triggers only one reload
+
### Adding Monitoring
```go
@@ -206,13 +224,12 @@ if manager != nil {
## Known Limitations
-1. **Standalone Only**: Currently only supported in standalone Redis clients
-2. **RESP3 Required**: Push notifications require RESP3 protocol
-3. **Server Support**: Requires Redis Enterprise or compatible Redis with maintenance notifications
-4. **Single Connection Commands**: Some commands (MULTI/EXEC, WATCH) may need special handling
-5. **No Failover/Ring Client Support**: Failover and Ring clients are not supported and there are no plans to add support
+1. **RESP3 Required**: Push notifications require RESP3 protocol
+2. **Server Support**: Requires Redis Enterprise or compatible Redis with maintenance notifications
+3. **Single Connection Commands**: Some commands (MULTI/EXEC, WATCH) may need special handling
+4. **No Failover/Ring Client Support**: Failover and Ring clients are not supported and there are no plans to add support
## Future Enhancements
-- Cluster client support
-- Enhanced metrics and observability
\ No newline at end of file
+- Enhanced metrics and observability
+- TTL-based cleanup for SeqID deduplication map
\ No newline at end of file
diff --git a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/README.md b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/README.md
index 2ac6b9cb..2f354ef6 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/README.md
+++ b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/README.md
@@ -2,8 +2,14 @@
Seamless Redis connection handoffs during cluster maintenance operations without dropping connections.
-## ⚠️ **Important Note**
-**Maintenance notifications are currently supported only in standalone Redis clients.** Cluster clients (ClusterClient, FailoverClient, etc.) do not yet support this functionality.
+## Cluster Support
+
+**Cluster notifications are now supported for ClusterClient!**
+
+- **SMIGRATING**: `["SMIGRATING", SeqID, slot/range, ...]` - Relaxes timeouts when slots are being migrated
+- **SMIGRATED**: `["SMIGRATED", SeqID, src host:port, dst host:port, slot/range, ...]` - Reloads cluster state when slot migration completes
+
+**Note:** Other maintenance notifications (MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER) are supported only in standalone Redis clients. Cluster clients support SMIGRATING and SMIGRATED for cluster-specific slot migration handling.
## Quick Start
diff --git a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go
index cbf4f6b2..db666f3a 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go
@@ -9,7 +9,6 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
- "github.com/redis/go-redis/v9/internal/util"
)
// Mode represents the maintenance notifications mode
@@ -261,10 +260,10 @@ func (c *Config) ApplyDefaultsWithPoolConfig(poolSize int, maxActiveConns int) *
// Default: max(20x workers, PoolSize), capped by maxActiveConns or 5x pool size
workerBasedSize := result.MaxWorkers * 20
poolBasedSize := poolSize
- result.HandoffQueueSize = util.Max(workerBasedSize, poolBasedSize)
+ result.HandoffQueueSize = max(workerBasedSize, poolBasedSize)
if c.HandoffQueueSize > 0 {
// When explicitly set: enforce minimum of 200
- result.HandoffQueueSize = util.Max(200, c.HandoffQueueSize)
+ result.HandoffQueueSize = max(200, c.HandoffQueueSize)
}
// Cap queue size: use maxActiveConns+1 if set, otherwise 5x pool size
@@ -278,7 +277,7 @@ func (c *Config) ApplyDefaultsWithPoolConfig(poolSize int, maxActiveConns int) *
} else {
queueCap = poolSize * 5
}
- result.HandoffQueueSize = util.Min(result.HandoffQueueSize, queueCap)
+ result.HandoffQueueSize = min(result.HandoffQueueSize, queueCap)
// Ensure minimum queue size of 2 (fallback for very small pools)
if result.HandoffQueueSize < 2 {
@@ -353,10 +352,10 @@ func (c *Config) applyWorkerDefaults(poolSize int) {
// When not set: min(poolSize/2, max(10, poolSize/3)) - balanced scaling approach
originalMaxWorkers := c.MaxWorkers
- c.MaxWorkers = util.Min(poolSize/2, util.Max(10, poolSize/3))
+ c.MaxWorkers = min(poolSize/2, max(10, poolSize/3))
if originalMaxWorkers != 0 {
// When explicitly set: max(poolSize/2, set_value) - ensure at least poolSize/2 workers
- c.MaxWorkers = util.Max(poolSize/2, originalMaxWorkers)
+ c.MaxWorkers = max(poolSize/2, originalMaxWorkers)
}
// Ensure minimum of 1 worker (fallback for very small pools)
diff --git a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/handoff_worker.go b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/handoff_worker.go
index 5b60e39b..d66542ff 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/handoff_worker.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/handoff_worker.go
@@ -13,6 +13,9 @@ import (
"github.com/redis/go-redis/v9/internal/pool"
)
+// PoolNameMain is the name used for the main connection pool in metrics.
+const PoolNameMain = "main"
+
// handoffWorkerManager manages background workers and queue for connection handoffs
type handoffWorkerManager struct {
// Event-driven handoff support
@@ -434,6 +437,11 @@ func (hwm *handoffWorkerManager) performHandoffInternal(
deadline := time.Now().Add(hwm.config.PostHandoffRelaxedDuration)
conn.SetRelaxedTimeoutWithDeadline(relaxedTimeout, relaxedTimeout, deadline)
+ // Record relaxed timeout metric (post-handoff)
+ if relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil {
+ relaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, "HANDOFF")
+ }
+
if internal.LogLevel.InfoOrAbove() {
internal.Logger.Printf(context.Background(), logs.ApplyingRelaxedTimeoutDueToPostHandoff(connID, relaxedTimeout, deadline.Format("15:04:05.000")))
}
@@ -462,6 +470,11 @@ func (hwm *handoffWorkerManager) performHandoffInternal(
internal.Logger.Printf(ctx, logs.HandoffSucceeded(connID, newEndpoint))
// successfully completed the handoff, no retry needed and no error
+ // Notify metrics: connection handoff succeeded
+ if handoffCallback := pool.GetMetricConnectionHandoffCallback(); handoffCallback != nil {
+ handoffCallback(ctx, conn, PoolNameMain)
+ }
+
return false, nil
}
@@ -501,9 +514,9 @@ func (hwm *handoffWorkerManager) closeConnFromRequest(ctx context.Context, reque
internal.Logger.Printf(ctx, logs.RemovingConnectionFromPool(conn.GetID(), err))
}
} else {
- err := conn.Close() // Close the connection if no pool provided
- if err != nil {
- internal.Logger.Printf(ctx, "redis: failed to close connection: %v", err)
+ errClose := conn.Close() // Close the connection if no pool provided
+ if errClose != nil {
+ internal.Logger.Printf(ctx, "redis: failed to close connection: %v", errClose)
}
if internal.LogLevel.WarnOrAbove() {
internal.Logger.Printf(ctx, logs.NoPoolProvidedCannotRemove(conn.GetID(), err))
diff --git a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/manager.go b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/manager.go
index 775c163e..3f9478e1 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/manager.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/manager.go
@@ -18,11 +18,13 @@ import (
// Push notification type constants for maintenance
const (
- NotificationMoving = "MOVING"
- NotificationMigrating = "MIGRATING"
- NotificationMigrated = "MIGRATED"
- NotificationFailingOver = "FAILING_OVER"
- NotificationFailedOver = "FAILED_OVER"
+ NotificationMoving = "MOVING" // Per-connection handoff notification
+ NotificationMigrating = "MIGRATING" // Per-connection migration start notification - relaxes timeouts
+ NotificationMigrated = "MIGRATED" // Per-connection migration complete notification - clears relaxed timeouts
+ NotificationFailingOver = "FAILING_OVER" // Per-connection failover start notification - relaxes timeouts
+ NotificationFailedOver = "FAILED_OVER" // Per-connection failover complete notification - clears relaxed timeouts
+ NotificationSMigrating = "SMIGRATING" // Cluster slot migrating notification - relaxes timeouts
+ NotificationSMigrated = "SMIGRATED" // Cluster slot migrated notification - unrelaxes timeouts and triggers cluster state reload
)
// maintenanceNotificationTypes contains all notification types that maintenance handles
@@ -32,6 +34,8 @@ var maintenanceNotificationTypes = []string{
NotificationMigrated,
NotificationFailingOver,
NotificationFailedOver,
+ NotificationSMigrating,
+ NotificationSMigrated,
}
// NotificationHook is called before and after notification processing
@@ -65,6 +69,10 @@ type Manager struct {
// MOVING operation tracking - using sync.Map for better concurrent performance
activeMovingOps sync.Map // map[MovingOperationKey]*MovingOperation
+ // SMIGRATED notification deduplication - tracks processed SeqIDs
+ // Multiple connections may receive the same SMIGRATED notification
+ processedSMigratedSeqIDs sync.Map // map[int64]bool
+
// Atomic state tracking - no locks needed for state queries
activeOperationCount atomic.Int64 // Number of active operations
closed atomic.Bool // Manager closed state
@@ -73,6 +81,9 @@ type Manager struct {
hooks []NotificationHook
hooksMu sync.RWMutex // Protects hooks slice
poolHooksRef *PoolHook
+
+ // Cluster state reload callback for SMIGRATED notifications
+ clusterStateReloadCallback ClusterStateReloadCallback
}
// MovingOperation tracks an active MOVING operation.
@@ -83,6 +94,14 @@ type MovingOperation struct {
Deadline time.Time
}
+// ClusterStateReloadCallback is a callback function that triggers cluster state reload.
+// This is used by node clients to notify their parent ClusterClient about SMIGRATED notifications.
+// The hostPort parameter indicates the destination node (e.g., "127.0.0.1:6379").
+// The slotRanges parameter contains the migrated slots (e.g., ["1234", "5000-6000"]).
+// Currently, implementations typically reload the entire cluster state, but in the future
+// this could be optimized to reload only the specific slots.
+type ClusterStateReloadCallback func(ctx context.Context, hostPort string, slotRanges []string)
+
// NewManager creates a new simplified manager.
func NewManager(client interfaces.ClientInterface, pool pool.Pooler, config *Config) (*Manager, error) {
if client == nil {
@@ -223,6 +242,15 @@ func (hm *Manager) GetActiveOperationCount() int64 {
return hm.activeOperationCount.Load()
}
+// MarkSMigratedSeqIDProcessed attempts to mark a SMIGRATED SeqID as processed.
+// Returns true if this is the first time processing this SeqID (should process),
+// false if it was already processed (should skip).
+// This prevents duplicate processing when multiple connections receive the same notification.
+func (hm *Manager) MarkSMigratedSeqIDProcessed(seqID int64) bool {
+ _, alreadyProcessed := hm.processedSMigratedSeqIDs.LoadOrStore(seqID, true)
+ return !alreadyProcessed // Return true if NOT already processed
+}
+
// Close closes the manager.
func (hm *Manager) Close() error {
// Use atomic operation for thread-safe close check
@@ -318,3 +346,17 @@ func (hm *Manager) AddNotificationHook(notificationHook NotificationHook) {
defer hm.hooksMu.Unlock()
hm.hooks = append(hm.hooks, notificationHook)
}
+
+// SetClusterStateReloadCallback sets the callback function that will be called when a SMIGRATED notification is received.
+// This allows node clients to notify their parent ClusterClient to reload cluster state.
+func (hm *Manager) SetClusterStateReloadCallback(callback ClusterStateReloadCallback) {
+ hm.clusterStateReloadCallback = callback
+}
+
+// TriggerClusterStateReload calls the cluster state reload callback if it's set.
+// This is called when a SMIGRATED notification is received.
+func (hm *Manager) TriggerClusterStateReload(ctx context.Context, hostPort string, slotRanges []string) {
+ if hm.clusterStateReloadCallback != nil {
+ hm.clusterStateReloadCallback(ctx, hostPort, slotRanges)
+ }
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/push_notification_handler.go b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/push_notification_handler.go
index 937b4ae8..7108265b 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/push_notification_handler.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/maintnotifications/push_notification_handler.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
+ "strings"
"time"
"github.com/redis/go-redis/v9/internal"
@@ -49,11 +50,22 @@ func (snh *NotificationHandler) HandlePushNotification(ctx context.Context, hand
err = snh.handleFailingOver(ctx, handlerCtx, modifiedNotification)
case NotificationFailedOver:
err = snh.handleFailedOver(ctx, handlerCtx, modifiedNotification)
+ case NotificationSMigrating:
+ err = snh.handleSMigrating(ctx, handlerCtx, modifiedNotification)
+ case NotificationSMigrated:
+ err = snh.handleSMigrated(ctx, handlerCtx, modifiedNotification)
default:
// Ignore other notification types (e.g., pub/sub messages)
err = nil
}
+ // Record maintenance notification metric
+ if maintenanceCallback := pool.GetMetricMaintenanceNotificationCallback(); maintenanceCallback != nil {
+ if conn, ok := handlerCtx.Conn.(*pool.Conn); ok {
+ maintenanceCallback(ctx, conn, notificationType)
+ }
+ }
+
// Process post-hooks with the result
snh.manager.processPostHooks(ctx, handlerCtx, notificationType, modifiedNotification, err)
@@ -61,7 +73,9 @@ func (snh *NotificationHandler) HandlePushNotification(ctx context.Context, hand
}
// handleMoving processes MOVING notifications.
-// ["MOVING", seqNum, timeS, endpoint] - per-connection handoff
+// MOVING indicates that a connection should be handed off to a new endpoint.
+// This is a per-connection notification that triggers connection handoff.
+// Expected format: ["MOVING", seqNum, timeS, endpoint]
func (snh *NotificationHandler) handleMoving(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {
if len(notification) < 3 {
internal.Logger.Printf(ctx, logs.InvalidNotification("MOVING", notification))
@@ -140,7 +154,28 @@ func (snh *NotificationHandler) handleMoving(ctx context.Context, handlerCtx pus
if err := snh.markConnForHandoff(poolConn, newEndpoint, seqID, deadline); err != nil {
// Log error but don't fail the goroutine - use background context since original may be cancelled
internal.Logger.Printf(context.Background(), logs.FailedToMarkForHandoff(poolConn.GetID(), err))
+ return
}
+
+ // Queue the handoff immediately if the connection is idle in the pool.
+ // If the connection is in use (StateInUse), it will be queued when returned to the pool via OnPut.
+ // This handles the case where the connection is idle and might never be retrieved again.
+ if poolConn.GetStateMachine().GetState() == pool.StateIdle {
+ if snh.manager.poolHooksRef != nil && snh.manager.poolHooksRef.workerManager != nil {
+ if err := snh.manager.poolHooksRef.workerManager.queueHandoff(poolConn); err != nil {
+ internal.Logger.Printf(context.Background(), logs.FailedToQueueHandoff(poolConn.GetID(), err))
+ } else {
+ // Mark the connection as queued for handoff to prevent it from being retrieved
+ // This transitions the connection to StateUnusable
+ if err := poolConn.MarkQueuedForHandoff(); err != nil {
+ internal.Logger.Printf(context.Background(), logs.FailedToMarkForHandoff(poolConn.GetID(), err))
+ } else {
+ internal.Logger.Printf(context.Background(), logs.MarkedForHandoff(poolConn.GetID()))
+ }
+ }
+ }
+ }
+ // If connection is StateInUse, the handoff will be queued when it's returned to the pool
})
return nil
}
@@ -167,9 +202,10 @@ func (snh *NotificationHandler) markConnForHandoff(conn *pool.Conn, newEndpoint
}
// handleMigrating processes MIGRATING notifications.
+// MIGRATING indicates that a connection migration is starting.
+// This is a per-connection notification that applies relaxed timeouts.
+// Expected format: ["MIGRATING", ...]
func (snh *NotificationHandler) handleMigrating(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {
- // MIGRATING notifications indicate that a connection is about to be migrated
- // Apply relaxed timeouts to the specific connection that received this notification
if len(notification) < 2 {
internal.Logger.Printf(ctx, logs.InvalidNotification("MIGRATING", notification))
return ErrInvalidNotification
@@ -191,13 +227,20 @@ func (snh *NotificationHandler) handleMigrating(ctx context.Context, handlerCtx
internal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(conn.GetID(), "MIGRATING", snh.manager.config.RelaxedTimeout))
}
conn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout)
+
+ // Record relaxed timeout metric
+ if relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil {
+ relaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, "MIGRATING")
+ }
+
return nil
}
// handleMigrated processes MIGRATED notifications.
+// MIGRATED indicates that a connection migration has completed.
+// This is a per-connection notification that clears relaxed timeouts.
+// Expected format: ["MIGRATED", ...]
func (snh *NotificationHandler) handleMigrated(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {
- // MIGRATED notifications indicate that a connection migration has completed
- // Restore normal timeouts for the specific connection that received this notification
if len(notification) < 2 {
internal.Logger.Printf(ctx, logs.InvalidNotification("MIGRATED", notification))
return ErrInvalidNotification
@@ -224,9 +267,10 @@ func (snh *NotificationHandler) handleMigrated(ctx context.Context, handlerCtx p
}
// handleFailingOver processes FAILING_OVER notifications.
+// FAILING_OVER indicates that a failover is starting.
+// This is a per-connection notification that applies relaxed timeouts.
+// Expected format: ["FAILING_OVER", ...]
func (snh *NotificationHandler) handleFailingOver(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {
- // FAILING_OVER notifications indicate that a connection is about to failover
- // Apply relaxed timeouts to the specific connection that received this notification
if len(notification) < 2 {
internal.Logger.Printf(ctx, logs.InvalidNotification("FAILING_OVER", notification))
return ErrInvalidNotification
@@ -249,13 +293,20 @@ func (snh *NotificationHandler) handleFailingOver(ctx context.Context, handlerCt
internal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(connID, "FAILING_OVER", snh.manager.config.RelaxedTimeout))
}
conn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout)
+
+ // Record relaxed timeout metric
+ if relaxedTimeoutCallback := pool.GetMetricConnectionRelaxedTimeoutCallback(); relaxedTimeoutCallback != nil {
+ relaxedTimeoutCallback(ctx, 1, conn, PoolNameMain, "FAILING_OVER")
+ }
+
return nil
}
// handleFailedOver processes FAILED_OVER notifications.
+// FAILED_OVER indicates that a failover has completed.
+// This is a per-connection notification that clears relaxed timeouts.
+// Expected format: ["FAILED_OVER", ...]
func (snh *NotificationHandler) handleFailedOver(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {
- // FAILED_OVER notifications indicate that a connection failover has completed
- // Restore normal timeouts for the specific connection that received this notification
if len(notification) < 2 {
internal.Logger.Printf(ctx, logs.InvalidNotification("FAILED_OVER", notification))
return ErrInvalidNotification
@@ -280,3 +331,194 @@ func (snh *NotificationHandler) handleFailedOver(ctx context.Context, handlerCtx
conn.ClearRelaxedTimeout()
return nil
}
+
+// handleSMigrating processes SMIGRATING notifications.
+// SMIGRATING indicates that a cluster slot is in the process of migrating to a different node.
+// This is a per-connection notification that applies relaxed timeouts during slot migration.
+// Expected format: ["SMIGRATING", SeqID, slot/range1-range2, ...]
+func (snh *NotificationHandler) handleSMigrating(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {
+ if len(notification) < 3 {
+ internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATING", notification))
+ return ErrInvalidNotification
+ }
+
+ // Validate SeqID (position 1)
+ if _, ok := notification[1].(int64); !ok {
+ internal.Logger.Printf(ctx, logs.InvalidSeqIDInSMigratingNotification(notification[1]))
+ return ErrInvalidNotification
+ }
+
+ if handlerCtx.Conn == nil {
+ internal.Logger.Printf(ctx, logs.NoConnectionInHandlerContext("SMIGRATING"))
+ return ErrInvalidNotification
+ }
+
+ conn, ok := handlerCtx.Conn.(*pool.Conn)
+ if !ok {
+ internal.Logger.Printf(ctx, logs.InvalidConnectionTypeInHandlerContext("SMIGRATING", handlerCtx.Conn, handlerCtx))
+ return ErrInvalidNotification
+ }
+
+ // Apply relaxed timeout to this specific connection
+ if internal.LogLevel.InfoOrAbove() {
+ internal.Logger.Printf(ctx, logs.RelaxedTimeoutDueToNotification(conn.GetID(), "SMIGRATING", snh.manager.config.RelaxedTimeout))
+ }
+ conn.SetRelaxedTimeout(snh.manager.config.RelaxedTimeout, snh.manager.config.RelaxedTimeout)
+ return nil
+}
+
+// handleSMigrated processes SMIGRATED notifications.
+// SMIGRATED indicates that a cluster slot has finished migrating to a different node.
+// This is a cluster-level notification that triggers cluster state reload.
+//
+// Expected RESP3 format:
+//
+// >3
+// +SMIGRATED
+// :SeqID
+// * <- array of triplet arrays
+// *3 <- each triplet is a 3-element array
+// + <- node from which slots are migrating FROM
+// + <- node to which slots are migrating TO
+// + <- comma-separated slots and/or ranges (e.g., "123,789-1000")
+//
+// A source and target endpoint may appear in multiple triplets.
+// The notification is only processed if the connection's NodeAddress matches one of the source endpoints.
+//
+// Note: Multiple connections may receive the same notification, so we deduplicate by SeqID before triggering reload.
+// but we still process the notification on each connection to clear the relaxed timeout.
+// In the case when the connection is from MOVED/ASK, the connection's original endpoint is not set,
+// so we will not be able to match the source endpoint. In such case, we will trigger the reload callback with the first target endpoint.
+func (snh *NotificationHandler) handleSMigrated(ctx context.Context, handlerCtx push.NotificationHandlerContext, notification []interface{}) error {
+ // Expected: ["SMIGRATED", SeqID, [[source, target, slots], ...]]
+ // Minimum 3 elements: SMIGRATED, SeqID, and the array of triplets
+ if len(notification) < 3 {
+ internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED", notification))
+ return ErrInvalidNotification
+ }
+
+ // Extract SeqID (position 1)
+ seqID, ok := notification[1].(int64)
+ if !ok {
+ internal.Logger.Printf(ctx, logs.InvalidSeqIDInSMigratedNotification(notification[1]))
+ return ErrInvalidNotification
+ }
+
+ // Extract the array of triplets (position 2)
+ triplets, ok := notification[2].([]interface{})
+ if !ok {
+ internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (triplets array)", notification[2]))
+ return ErrInvalidNotification
+ }
+
+ if len(triplets) == 0 {
+ internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (empty triplets)", notification))
+ return ErrInvalidNotification
+ }
+
+ // Get the connection's endpoints to check if this notification is relevant
+ // We check against both nodeAddress (from CLUSTER SLOTS) and addr (after resolution)
+ // since we cannot be certain which format the notification source will use
+ var connectionNodeAddress string
+ var connectionAddr string
+ if snh.manager.options != nil {
+ connectionNodeAddress = snh.manager.options.GetNodeAddress()
+ connectionAddr = snh.manager.options.GetAddr()
+ }
+
+ // Helper function to check if source matches either of our endpoints
+ // notification source can be either the node address or the addr after resolution
+ sourceMatchesConnection := func(source string) bool {
+ if source == connectionNodeAddress {
+ return true
+ }
+ if source == connectionAddr {
+ return true
+ }
+ return false
+ }
+
+ // Parse triplets and check if any source matches our connection's endpoints
+ var matchingTriplets []struct {
+ source string
+ target string
+ slots string
+ }
+ var allSlotRanges []string
+
+ for _, tripletInterface := range triplets {
+ // Each triplet should be a 3-element array: [source, target, slots]
+ triplet, ok := tripletInterface.([]interface{})
+ if !ok || len(triplet) != 3 {
+ internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (triplet format)", tripletInterface))
+ continue
+ }
+
+ // Extract source endpoint
+ source, ok := triplet[0].(string)
+ if !ok {
+ internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (source)", triplet[0]))
+ continue
+ }
+
+ // Extract target endpoint
+ target, ok := triplet[1].(string)
+ if !ok {
+ internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (target)", triplet[1]))
+ continue
+ }
+
+ // Extract slots
+ slots, ok := triplet[2].(string)
+ if !ok {
+ internal.Logger.Printf(ctx, logs.InvalidNotification("SMIGRATED (slots)", triplet[2]))
+ continue
+ }
+
+ // Check if this triplet's source matches our connection's endpoints
+ if sourceMatchesConnection(source) {
+ matchingTriplets = append(matchingTriplets, struct {
+ source string
+ target string
+ slots string
+ }{source, target, slots})
+ slotRanges := strings.Split(slots, ",")
+ allSlotRanges = append(allSlotRanges, slotRanges...)
+ }
+ }
+
+ var connID uint64
+ // Reset relaxed timeout for this specific connection
+ if handlerCtx.Conn != nil {
+ conn, ok := handlerCtx.Conn.(*pool.Conn)
+ if ok {
+ if internal.LogLevel.InfoOrAbove() {
+ connID = conn.GetID()
+ internal.Logger.Printf(ctx, logs.UnrelaxedTimeout(connID))
+ }
+ conn.ClearRelaxedTimeout()
+ }
+ }
+
+ // If no matching triplets, this notification is not relevant to this connection
+ if len(matchingTriplets) == 0 {
+ return nil
+ }
+
+ // Deduplicate by SeqID - multiple connections may receive the same notification
+ // Only trigger cluster state reload once per seqID
+ if snh.manager.MarkSMigratedSeqIDProcessed(seqID) {
+ // Use the first matching triplet
+ target := matchingTriplets[0].target
+ slotsForLog := allSlotRanges
+
+ if internal.LogLevel.InfoOrAbove() {
+ internal.Logger.Printf(ctx, logs.TriggeringClusterStateReload(seqID, target, slotsForLog))
+ }
+
+ // Trigger cluster state reload via callback
+ snh.manager.TriggerClusterStateReload(ctx, target, slotsForLog)
+ }
+
+ return nil
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/options.go b/backend/vendor/github.com/redis/go-redis/v9/options.go
index 9773e86f..5db27102 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/options.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/options.go
@@ -11,6 +11,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync/atomic"
"time"
"github.com/redis/go-redis/v9/auth"
@@ -21,6 +22,16 @@ import (
"github.com/redis/go-redis/v9/push"
)
+// poolIDCounter is a global auto-increment counter for generating unique pool IDs.
+var poolIDCounter atomic.Uint64
+
+// generateUniqueID generates a short unique identifier for pool names using auto-increment.
+// This makes it easier to identify and track pools in order of creation.
+func generateUniqueID() string {
+ id := poolIDCounter.Add(1)
+ return strconv.FormatUint(id, 10)
+}
+
// Limiter is the interface of a rate limiter or a circuit breaker.
type Limiter interface {
// Allow returns nil if operation is allowed or an error otherwise.
@@ -42,6 +53,17 @@ type Options struct {
// Addr is the address formated as host:port
Addr string
+ // NodeAddress is the address of the Redis node as reported by the server.
+ // For cluster clients, this is the exact endpoint string returned by CLUSTER SLOTS
+ // before any resolution or transformation (e.g., loopback replacement).
+ // For standalone clients, this defaults to Addr.
+ //
+ // This is used to match the source endpoint in maintenance notifications
+ // (e.g. SMIGRATED).
+ //
+ // Use Client.NodeAddress() to access this value.
+ NodeAddress string
+
// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
ClientName string
@@ -200,6 +222,8 @@ type Options struct {
// MaxActiveConns is the maximum number of connections allocated by the pool at a given time.
// When zero, there is no limit on the number of connections in the pool.
// If the pool is full, the next call to Get() will block until a connection is released.
+ //
+ // default: 0
MaxActiveConns int
// ConnMaxIdleTime is the maximum amount of time a connection may be idle.
@@ -220,6 +244,19 @@ type Options struct {
// default: 0
ConnMaxLifetime time.Duration
+ // ConnMaxLifetimeJitter is the absolute jitter duration applied to ConnMaxLifetime
+ // to prevent all connections from expiring simultaneously.
+ //
+ // The jitter is applied as a random offset in the range [-jitter, +jitter].
+ // For example, if ConnMaxLifetime is 1 hour and ConnMaxLifetimeJitter is 6 minutes,
+ // connections will expire between 54 minutes and 66 minutes.
+ //
+ // If <= 0, no jitter is applied.
+ // If > ConnMaxLifetime, it will be capped at ConnMaxLifetime.
+ //
+ // default: 0
+ ConnMaxLifetimeJitter time.Duration
+
// TLSConfig to use. When set, TLS will be negotiated.
TLSConfig *tls.Config
@@ -280,6 +317,12 @@ func (opt *Options) init() {
opt.Network = "tcp"
}
}
+ // For standalone clients, default NodeAddress to Addr if not set.
+ // This ensures maintenance notifications (SMIGRATED, etc.) can match
+ // the connection's endpoint even for non-cluster clients.
+ if opt.NodeAddress == "" {
+ opt.NodeAddress = opt.Addr
+ }
if opt.Protocol < 2 {
opt.Protocol = 3
}
@@ -336,6 +379,8 @@ func (opt *Options) init() {
opt.ConnMaxIdleTime = 30 * time.Minute
}
+ opt.ConnMaxLifetimeJitter = min(opt.ConnMaxLifetimeJitter, opt.ConnMaxLifetime)
+
switch opt.MaxRetries {
case -1:
opt.MaxRetries = 0
@@ -645,6 +690,9 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) {
} else {
o.ConnMaxLifetime = q.duration("max_conn_age")
}
+ if q.has("conn_max_lifetime_jitter") {
+ o.ConnMaxLifetimeJitter = min(q.duration("conn_max_lifetime_jitter"), o.ConnMaxLifetime)
+ }
if q.err != nil {
return nil, q.err
}
@@ -674,6 +722,7 @@ func getUserPassword(u *url.URL) (string, string) {
func newConnPool(
opt *Options,
dialer func(ctx context.Context, network, addr string) (net.Conn, error),
+ poolName string,
) (*pool.ConnPool, error) {
poolSize, err := util.SafeIntToInt32(opt.PoolSize, "PoolSize")
if err != nil {
@@ -711,13 +760,18 @@ func newConnPool(
MaxActiveConns: maxActiveConns,
ConnMaxIdleTime: opt.ConnMaxIdleTime,
ConnMaxLifetime: opt.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,
ReadBufferSize: opt.ReadBufferSize,
WriteBufferSize: opt.WriteBufferSize,
PushNotificationsEnabled: opt.Protocol == 3,
+ Name: poolName,
}), nil
}
-func newPubSubPool(opt *Options, dialer func(ctx context.Context, network, addr string) (net.Conn, error),
+func newPubSubPool(
+ opt *Options,
+ dialer func(ctx context.Context, network, addr string) (net.Conn, error),
+ poolName string,
) (*pool.PubSubPool, error) {
poolSize, err := util.SafeIntToInt32(opt.PoolSize, "PoolSize")
if err != nil {
@@ -752,8 +806,10 @@ func newPubSubPool(opt *Options, dialer func(ctx context.Context, network, addr
MaxActiveConns: maxActiveConns,
ConnMaxIdleTime: opt.ConnMaxIdleTime,
ConnMaxLifetime: opt.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,
ReadBufferSize: 32 * 1024,
WriteBufferSize: 32 * 1024,
PushNotificationsEnabled: opt.Protocol == 3,
+ Name: poolName,
}, dialer), nil
}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/osscluster.go b/backend/vendor/github.com/redis/go-redis/v9/osscluster.go
index 7925d2c6..6fb51dc2 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/osscluster.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/osscluster.go
@@ -3,6 +3,7 @@ package redis
import (
"context"
"crypto/tls"
+ "errors"
"fmt"
"math"
"net"
@@ -17,9 +18,11 @@ import (
"github.com/redis/go-redis/v9/auth"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/hashtag"
+ "github.com/redis/go-redis/v9/internal/otel"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/internal/rand"
+ "github.com/redis/go-redis/v9/internal/routing"
"github.com/redis/go-redis/v9/maintnotifications"
"github.com/redis/go-redis/v9/push"
)
@@ -28,7 +31,11 @@ const (
minLatencyMeasurementInterval = 10 * time.Second
)
-var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes")
+var (
+ errClusterNoNodes = errors.New("redis: cluster has no nodes")
+ errNoWatchKeys = errors.New("redis: Watch requires at least one key")
+ errWatchCrosslot = errors.New("redis: Watch requires all keys to be in the same slot")
+)
// ClusterOptions are used to configure a cluster client and should be
// passed to NewClusterClient.
@@ -85,19 +92,35 @@ type ClusterOptions struct {
MinRetryBackoff time.Duration
MaxRetryBackoff time.Duration
- DialTimeout time.Duration
+ DialTimeout time.Duration
+
+ // DialerRetries is the maximum number of retry attempts when dialing fails.
+ //
+ // default: 5
+ DialerRetries int
+
+ // DialerRetryTimeout is the backoff duration between retry attempts.
+ //
+ // default: 100 milliseconds
+ DialerRetryTimeout time.Duration
+
ReadTimeout time.Duration
WriteTimeout time.Duration
ContextTimeoutEnabled bool
- PoolFIFO bool
- PoolSize int // applies per cluster node and not for the whole cluster
- PoolTimeout time.Duration
- MinIdleConns int
- MaxIdleConns int
- MaxActiveConns int // applies per cluster node and not for the whole cluster
- ConnMaxIdleTime time.Duration
- ConnMaxLifetime time.Duration
+ // MaxConcurrentDials is the maximum number of concurrent connection creation goroutines.
+ // If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize.
+ MaxConcurrentDials int
+
+ PoolFIFO bool
+ PoolSize int // applies per cluster node and not for the whole cluster
+ PoolTimeout time.Duration
+ MinIdleConns int
+ MaxIdleConns int
+ MaxActiveConns int // applies per cluster node and not for the whole cluster
+ ConnMaxIdleTime time.Duration
+ ConnMaxLifetime time.Duration
+ ConnMaxLifetimeJitter time.Duration
// ReadBufferSize is the size of the bufio.Reader buffer for each connection.
// Larger buffers can improve performance for commands that return large responses.
@@ -115,6 +138,11 @@ type ClusterOptions struct {
TLSConfig *tls.Config
+ // DisableRoutingPolicies disables the request/response policy routing system.
+ // When disabled, all commands use the legacy routing behavior.
+ // Experimental. Will be removed when shard picker is fully implemented.
+ DisableRoutingPolicies bool
+
// DisableIndentity - Disable set-lib on connect.
//
// default: false
@@ -146,8 +174,16 @@ type ClusterOptions struct {
// cluster upgrade notifications gracefully and manage connection/pool state
// transitions seamlessly. Requires Protocol: 3 (RESP3) for push notifications.
// If nil, maintnotifications upgrades are in "auto" mode and will be enabled if the server supports it.
- // The ClusterClient does not directly work with maintnotifications, it is up to the clients in the Nodes map to work with maintnotifications.
+ // The ClusterClient supports SMIGRATING and SMIGRATED notifications for cluster state management.
+ // Individual node clients handle other maintenance notifications (MOVING, MIGRATING, etc.).
MaintNotificationsConfig *maintnotifications.Config
+ // ShardPicker is used to pick a shard when the request_policy is
+ // ReqDefault and the command has no keys.
+ ShardPicker routing.ShardPicker
+
+ // ClusterStateReloadInterval is the interval for reloading the cluster state.
+ // Default is 10 seconds.
+ ClusterStateReloadInterval time.Duration
}
func (opt *ClusterOptions) init() {
@@ -162,9 +198,24 @@ func (opt *ClusterOptions) init() {
opt.ReadOnly = true
}
+ if opt.DialTimeout == 0 {
+ opt.DialTimeout = 5 * time.Second
+ }
+ if opt.DialerRetries == 0 {
+ opt.DialerRetries = 5
+ }
+ if opt.DialerRetryTimeout == 0 {
+ opt.DialerRetryTimeout = 100 * time.Millisecond
+ }
+
if opt.PoolSize == 0 {
opt.PoolSize = 5 * runtime.GOMAXPROCS(0)
}
+ if opt.MaxConcurrentDials <= 0 {
+ opt.MaxConcurrentDials = opt.PoolSize
+ } else if opt.MaxConcurrentDials > opt.PoolSize {
+ opt.MaxConcurrentDials = opt.PoolSize
+ }
if opt.ReadBufferSize == 0 {
opt.ReadBufferSize = proto.DefaultBufferSize
}
@@ -208,6 +259,14 @@ func (opt *ClusterOptions) init() {
if opt.FailingTimeoutSeconds == 0 {
opt.FailingTimeoutSeconds = 15
}
+
+ if opt.ShardPicker == nil {
+ opt.ShardPicker = &routing.RoundRobinPicker{}
+ }
+
+ if opt.ClusterStateReloadInterval == 0 {
+ opt.ClusterStateReloadInterval = 10 * time.Second
+ }
}
// ParseClusterURL parses a URL into ClusterOptions that can be used to connect to Redis.
@@ -302,15 +361,21 @@ func setupClusterQueryParams(u *url.URL, o *ClusterOptions) (*ClusterOptions, er
o.MinRetryBackoff = q.duration("min_retry_backoff")
o.MaxRetryBackoff = q.duration("max_retry_backoff")
o.DialTimeout = q.duration("dial_timeout")
+ o.DialerRetries = q.int("dialer_retries")
+ o.DialerRetryTimeout = q.duration("dialer_retry_timeout")
o.ReadTimeout = q.duration("read_timeout")
o.WriteTimeout = q.duration("write_timeout")
o.PoolFIFO = q.bool("pool_fifo")
o.PoolSize = q.int("pool_size")
+ o.MaxConcurrentDials = q.int("max_concurrent_dials")
o.MinIdleConns = q.int("min_idle_conns")
o.MaxIdleConns = q.int("max_idle_conns")
o.MaxActiveConns = q.int("max_active_conns")
o.PoolTimeout = q.duration("pool_timeout")
o.ConnMaxLifetime = q.duration("conn_max_lifetime")
+ if q.has("conn_max_lifetime_jitter") {
+ o.ConnMaxLifetimeJitter = min(q.duration("conn_max_lifetime_jitter"), o.ConnMaxLifetime)
+ }
o.ConnMaxIdleTime = q.duration("conn_max_idle_time")
o.FailingTimeoutSeconds = q.int("failing_timeout_seconds")
@@ -361,19 +426,24 @@ func (opt *ClusterOptions) clientOptions() *Options {
MinRetryBackoff: opt.MinRetryBackoff,
MaxRetryBackoff: opt.MaxRetryBackoff,
- DialTimeout: opt.DialTimeout,
- ReadTimeout: opt.ReadTimeout,
- WriteTimeout: opt.WriteTimeout,
+ DialTimeout: opt.DialTimeout,
+ DialerRetries: opt.DialerRetries,
+ DialerRetryTimeout: opt.DialerRetryTimeout,
+ ReadTimeout: opt.ReadTimeout,
+ WriteTimeout: opt.WriteTimeout,
+
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
PoolFIFO: opt.PoolFIFO,
PoolSize: opt.PoolSize,
+ MaxConcurrentDials: opt.MaxConcurrentDials,
PoolTimeout: opt.PoolTimeout,
MinIdleConns: opt.MinIdleConns,
MaxIdleConns: opt.MaxIdleConns,
MaxActiveConns: opt.MaxActiveConns,
ConnMaxIdleTime: opt.ConnMaxIdleTime,
ConnMaxLifetime: opt.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,
ReadBufferSize: opt.ReadBufferSize,
WriteBufferSize: opt.WriteBufferSize,
DisableIdentity: opt.DisableIdentity,
@@ -407,9 +477,10 @@ type clusterNode struct {
lastLatencyMeasurement int64 // atomic
}
-func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode {
+func newClusterNodeWithNodeAddress(clOpt *ClusterOptions, addr, nodeAddress string) *clusterNode {
opt := clOpt.clientOptions()
opt.Addr = addr
+ opt.NodeAddress = nodeAddress
node := clusterNode{
Client: clOpt.NewClient(opt),
}
@@ -637,6 +708,10 @@ func (c *clusterNodes) GC(generation uint32) {
}
func (c *clusterNodes) GetOrCreate(addr string) (*clusterNode, error) {
+ return c.GetOrCreateWithNodeAddress(addr, "")
+}
+
+func (c *clusterNodes) GetOrCreateWithNodeAddress(addr, nodeAddress string) (*clusterNode, error) {
node, err := c.get(addr)
if err != nil {
return nil, err
@@ -657,7 +732,7 @@ func (c *clusterNodes) GetOrCreate(addr string) (*clusterNode, error) {
return node, nil
}
- node = newClusterNode(c.opt, addr)
+ node = newClusterNodeWithNodeAddress(c.opt, addr, nodeAddress)
for _, fn := range c.onNewNode {
fn(node.Client)
}
@@ -754,12 +829,14 @@ func newClusterState(
for _, slot := range slots {
var nodes []*clusterNode
for i, slotNode := range slot.Nodes {
- addr := slotNode.Addr
+ // slotNode.Addr is the node address from CLUSTER SLOTS
+ nodeAddress := slotNode.Addr
+ addr := nodeAddress
if !isLoopbackOrigin {
addr = replaceLoopbackHost(addr, originHost)
}
- node, err := c.nodes.GetOrCreate(addr)
+ node, err := c.nodes.GetOrCreateWithNodeAddress(addr, nodeAddress)
if err != nil {
return nil, err
}
@@ -926,6 +1003,29 @@ func (c *clusterState) slotRandomNode(slot int) (*clusterNode, error) {
return nodes[randomNodes[0]], nil
}
+func (c *clusterState) slotShardPickerSlaveNode(slot int, shardPicker routing.ShardPicker) (*clusterNode, error) {
+ nodes := c.slotNodes(slot)
+ if len(nodes) == 0 {
+ return c.nodes.Random()
+ }
+
+ // nodes[0] is master, nodes[1:] are slaves
+ // First, try all slave nodes for this slot using ShardPicker order
+ slaves := nodes[1:]
+ if len(slaves) > 0 {
+ for i := 0; i < len(slaves); i++ {
+ idx := shardPicker.Next(len(slaves))
+ slave := slaves[idx]
+ if !slave.Failing() && !slave.Loading() {
+ return slave, nil
+ }
+ }
+ }
+
+ // All slaves are failing or loading - return master
+ return nodes[0], nil
+}
+
func (c *clusterState) slotNodes(slot int) []*clusterNode {
i := sort.Search(len(c.slots), func(i int) bool {
return c.slots[i].end >= slot
@@ -945,13 +1045,16 @@ func (c *clusterState) slotNodes(slot int) []*clusterNode {
type clusterStateHolder struct {
load func(ctx context.Context) (*clusterState, error)
- state atomic.Value
- reloading uint32 // atomic
+ reloadInterval time.Duration
+ state atomic.Value
+ reloading uint32 // atomic
+ reloadPending uint32 // atomic - set to 1 when reload is requested during active reload
}
-func newClusterStateHolder(fn func(ctx context.Context) (*clusterState, error)) *clusterStateHolder {
+func newClusterStateHolder(load func(ctx context.Context) (*clusterState, error), reloadInterval time.Duration) *clusterStateHolder {
return &clusterStateHolder{
- load: fn,
+ load: load,
+ reloadInterval: reloadInterval,
}
}
@@ -965,17 +1068,37 @@ func (c *clusterStateHolder) Reload(ctx context.Context) (*clusterState, error)
}
func (c *clusterStateHolder) LazyReload() {
+ // If already reloading, mark that another reload is pending
if !atomic.CompareAndSwapUint32(&c.reloading, 0, 1) {
+ atomic.StoreUint32(&c.reloadPending, 1)
return
}
- go func() {
- defer atomic.StoreUint32(&c.reloading, 0)
- _, err := c.Reload(context.Background())
- if err != nil {
- return
+ go func() {
+ for {
+ _, err := c.Reload(context.Background())
+ if err != nil {
+ atomic.StoreUint32(&c.reloadPending, 0)
+ atomic.StoreUint32(&c.reloading, 0)
+ return
+ }
+
+ // Clear pending flag after reload completes, before cooldown
+ // This captures notifications that arrived during the reload
+ atomic.StoreUint32(&c.reloadPending, 0)
+
+ // Wait cooldown period
+ time.Sleep(200 * time.Millisecond)
+
+ // Check if another reload was requested during cooldown
+ if atomic.LoadUint32(&c.reloadPending) == 0 {
+ // No pending reload, we're done
+ atomic.StoreUint32(&c.reloading, 0)
+ return
+ }
+
+ // Pending reload requested, loop to reload again
}
- time.Sleep(200 * time.Millisecond)
}()
}
@@ -986,7 +1109,7 @@ func (c *clusterStateHolder) Get(ctx context.Context) (*clusterState, error) {
}
state := v.(*clusterState)
- if time.Since(state.createdAt) > 10*time.Second {
+ if time.Since(state.createdAt) > c.reloadInterval {
c.LazyReload()
}
return state, nil
@@ -1006,10 +1129,11 @@ func (c *clusterStateHolder) ReloadOrGet(ctx context.Context) (*clusterState, er
// or more underlying connections. It's safe for concurrent use by
// multiple goroutines.
type ClusterClient struct {
- opt *ClusterOptions
- nodes *clusterNodes
- state *clusterStateHolder
- cmdsInfoCache *cmdsInfoCache
+ opt *ClusterOptions
+ nodes *clusterNodes
+ state *clusterStateHolder
+ cmdsInfoCache *cmdsInfoCache
+ cmdInfoResolver *commandInfoResolver
cmdable
hooksMixin
}
@@ -1017,9 +1141,6 @@ type ClusterClient struct {
// NewClusterClient returns a Redis Cluster client as described in
// http://redis.io/topics/cluster-spec.
func NewClusterClient(opt *ClusterOptions) *ClusterClient {
- if opt == nil {
- panic("redis: NewClusterClient nil options")
- }
opt.init()
c := &ClusterClient{
@@ -1027,10 +1148,13 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient {
nodes: newClusterNodes(opt),
}
- c.state = newClusterStateHolder(c.loadState)
c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo)
- c.cmdable = c.Process
+ c.state = newClusterStateHolder(c.loadState, opt.ClusterStateReloadInterval)
+
+ c.SetCommandInfoResolver(NewDefaultCommandPolicyResolver())
+
+ c.cmdable = c.Process
c.initHooks(hooks{
dial: nil,
process: c.process,
@@ -1038,6 +1162,26 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient {
txPipeline: c.processTxPipeline,
})
+ // Set up SMIGRATED notification handling for cluster state reload
+ // When a node client receives a SMIGRATED notification, it should trigger
+ // cluster state reload on the parent ClusterClient
+ if opt.MaintNotificationsConfig != nil {
+ c.nodes.OnNewNode(func(nodeClient *Client) {
+ manager := nodeClient.GetMaintNotificationsManager()
+ if manager != nil {
+ manager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {
+ // Log the migration details for now
+ if internal.LogLevel.InfoOrAbove() {
+ internal.Logger.Printf(ctx, "cluster: slots %v migrated to %s, reloading cluster state", slotRanges, hostPort)
+ }
+ // Currently we reload the entire cluster state
+ // In the future, this could be optimized to reload only the specific slots
+ c.state.LazyReload()
+ })
+ }
+ })
+ }
+
return c
}
@@ -1083,7 +1227,11 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
if node == nil {
var err error
- node, err = c.cmdNode(ctx, cmd.Name(), slot)
+ if !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil {
+ node, err = c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)
+ } else {
+ node, err = c.cmdNode(ctx, cmd.Name(), slot)
+ }
if err != nil {
return err
}
@@ -1091,13 +1239,16 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
if ask {
ask = false
-
pipe := node.Client.Pipeline()
_ = pipe.Process(ctx, NewCmd(ctx, "asking"))
_ = pipe.Process(ctx, cmd)
_, lastErr = pipe.Exec(ctx)
} else {
- lastErr = node.Client.Process(ctx, cmd)
+ if !c.opt.DisableRoutingPolicies {
+ lastErr = c.routeAndRun(ctx, cmd, node)
+ } else {
+ lastErr = node.Client.Process(ctx, cmd)
+ }
}
// If there is no error - we are done.
@@ -1124,6 +1275,18 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
if moved || ask {
c.state.LazyReload()
+ // Record error metrics
+ if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {
+ errorType := "MOVED"
+ statusCode := "MOVED"
+ if ask {
+ errorType = "ASK"
+ statusCode = "ASK"
+ }
+ // MOVED/ASK are not internal errors, and this is the first attempt (retry count = 0)
+ errorCallback(ctx, errorType, nil, statusCode, false, 0)
+ }
+
var err error
node, err = c.nodes.GetOrCreate(addr)
if err != nil {
@@ -1371,17 +1534,35 @@ func (c *ClusterClient) Pipelined(ctx context.Context, fn func(Pipeliner) error)
}
func (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) error {
+ // Only call time.Now() if pipeline operation duration callback is set to avoid overhead
+ var operationStart time.Time
+ pipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback()
+ if pipelineOpDurationCallback != nil {
+ operationStart = time.Now()
+ }
+ totalAttempts := 0
+
cmdsMap := newCmdsMap()
if err := c.mapCmdsByNode(ctx, cmdsMap, cmds); err != nil {
setCmdsErr(cmds, err)
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, "PIPELINE", len(cmds), 1, err, nil, 0)
+ }
return err
}
+ var lastErr error
for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ {
+ totalAttempts++
if attempt > 0 {
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
setCmdsErr(cmds, err)
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, "PIPELINE", len(cmds), totalAttempts, err, nil, 0)
+ }
return err
}
}
@@ -1402,6 +1583,17 @@ func (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) error
break
}
cmdsMap = failedCmds
+ lastErr = cmdsFirstErr(cmds)
+ }
+
+ // Record pipeline operation duration
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ finalErr := cmdsFirstErr(cmds)
+ if finalErr == nil {
+ finalErr = lastErr
+ }
+ pipelineOpDurationCallback(ctx, operationDuration, "PIPELINE", len(cmds), totalAttempts, finalErr, nil, 0)
}
return cmdsFirstErr(cmds)
@@ -1413,16 +1605,33 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
return err
}
- preferredRandomSlot := -1
if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) {
for _, cmd := range cmds {
- slot := c.cmdSlot(cmd, preferredRandomSlot)
- if preferredRandomSlot == -1 {
- preferredRandomSlot = slot
+ var policy *routing.CommandPolicy
+ if c.cmdInfoResolver != nil {
+ policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)
}
- node, err := c.slotReadOnlyNode(state, slot)
- if err != nil {
- return err
+ if policy != nil && !policy.CanBeUsedInPipeline() {
+ return fmt.Errorf(
+ "redis: cannot pipeline command %q with request policy ReqAllNodes/ReqAllShards/ReqMultiShard; Note: This behavior is subject to change in the future", cmd.Name(),
+ )
+ }
+ slot := c.cmdSlot(cmd, -1)
+ var node *clusterNode
+ // For keyless commands (slot == -1), use ShardPicker if routing policies are enabled
+ if slot == -1 && !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil {
+ if len(state.Masters) == 0 {
+ return errClusterNoNodes
+ }
+ // For read-only keyless commands, pick from all nodes (masters + slaves)
+ allNodes := append(state.Masters, state.Slaves...)
+ idx := c.opt.ShardPicker.Next(len(allNodes))
+ node = allNodes[idx]
+ } else {
+ node, err = c.slotReadOnlyNode(state, slot)
+ if err != nil {
+ return err
+ }
}
cmdsMap.Add(node, cmd)
}
@@ -1430,13 +1639,29 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
}
for _, cmd := range cmds {
- slot := c.cmdSlot(cmd, preferredRandomSlot)
- if preferredRandomSlot == -1 {
- preferredRandomSlot = slot
+ var policy *routing.CommandPolicy
+ if c.cmdInfoResolver != nil {
+ policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)
}
- node, err := state.slotMasterNode(slot)
- if err != nil {
- return err
+ if policy != nil && !policy.CanBeUsedInPipeline() {
+ return fmt.Errorf(
+ "redis: cannot pipeline command %q with request policy ReqAllNodes/ReqAllShards/ReqMultiShard; Note: This behavior is subject to change in the future", cmd.Name(),
+ )
+ }
+ slot := c.cmdSlot(cmd, -1)
+ var node *clusterNode
+ // For keyless commands (slot == -1), use ShardPicker if routing policies are enabled
+ if slot == -1 && !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil {
+ if len(state.Masters) == 0 {
+ return errClusterNoNodes
+ }
+ idx := c.opt.ShardPicker.Next(len(state.Masters))
+ node = state.Masters[idx]
+ } else {
+ node, err = state.slotMasterNode(slot)
+ if err != nil {
+ return err
+ }
}
cmdsMap.Add(node, cmd)
}
@@ -1582,6 +1807,14 @@ func (c *ClusterClient) TxPipelined(ctx context.Context, fn func(Pipeliner) erro
}
func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {
+ // Only call time.Now() if pipeline operation duration callback is set to avoid overhead
+ var operationStart time.Time
+ pipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback()
+ if pipelineOpDurationCallback != nil {
+ operationStart = time.Now()
+ }
+ totalAttempts := 0
+
// Trim multi .. exec.
cmds = cmds[1 : len(cmds)-1]
@@ -1592,10 +1825,14 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
state, err := c.state.Get(ctx)
if err != nil {
setCmdsErr(cmds, err)
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), 1, err, nil, 0)
+ }
return err
}
- keyedCmdsBySlot := c.slottedKeyedCommands(cmds)
+ keyedCmdsBySlot := c.slottedKeyedCommands(ctx, cmds)
slot := -1
switch len(keyedCmdsBySlot) {
case 0:
@@ -1608,20 +1845,34 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
default:
// TxPipeline does not support cross slot transaction.
setCmdsErr(cmds, ErrCrossSlot)
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), 1, ErrCrossSlot, nil, 0)
+ }
return ErrCrossSlot
}
node, err := state.slotMasterNode(slot)
if err != nil {
setCmdsErr(cmds, err)
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), 1, err, nil, 0)
+ }
return err
}
+ var lastErr error
cmdsMap := map[*clusterNode][]Cmder{node: cmds}
for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ {
+ totalAttempts++
if attempt > 0 {
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
setCmdsErr(cmds, err)
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), totalAttempts, err, nil, 0)
+ }
return err
}
}
@@ -1642,6 +1893,16 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
break
}
cmdsMap = failedCmds.m
+ lastErr = cmdsFirstErr(cmds)
+ }
+
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ finalErr := cmdsFirstErr(cmds)
+ if finalErr == nil {
+ finalErr = lastErr
+ }
+ pipelineOpDurationCallback(ctx, operationDuration, "MULTI", len(cmds), totalAttempts, finalErr, nil, 0)
}
return cmdsFirstErr(cmds)
@@ -1649,18 +1910,18 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
// slottedKeyedCommands returns a map of slot to commands taking into account
// only commands that have keys.
-func (c *ClusterClient) slottedKeyedCommands(cmds []Cmder) map[int][]Cmder {
+func (c *ClusterClient) slottedKeyedCommands(ctx context.Context, cmds []Cmder) map[int][]Cmder {
cmdsSlots := map[int][]Cmder{}
- preferredRandomSlot := -1
+ prefferedRandomSlot := -1
for _, cmd := range cmds {
if cmdFirstKeyPos(cmd) == 0 {
continue
}
- slot := c.cmdSlot(cmd, preferredRandomSlot)
- if preferredRandomSlot == -1 {
- preferredRandomSlot = slot
+ slot := c.cmdSlot(cmd, prefferedRandomSlot)
+ if prefferedRandomSlot == -1 {
+ prefferedRandomSlot = slot
}
cmdsSlots[slot] = append(cmdsSlots[slot], cmd)
@@ -1819,14 +2080,13 @@ func (c *ClusterClient) cmdsMoved(
func (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
if len(keys) == 0 {
- return fmt.Errorf("redis: Watch requires at least one key")
+ return errNoWatchKeys
}
slot := hashtag.Slot(keys[0])
for _, key := range keys[1:] {
if hashtag.Slot(key) != slot {
- err := fmt.Errorf("redis: Watch requires all keys to be in the same slot")
- return err
+ return errWatchCrosslot
}
}
@@ -1995,7 +2255,6 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
for _, idx := range perm {
addr := addrs[idx]
-
node, err := c.nodes.GetOrCreate(addr)
if err != nil {
if firstErr == nil {
@@ -2008,6 +2267,7 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
if err == nil {
return info, nil
}
+
if firstErr == nil {
firstErr = err
}
@@ -2019,35 +2279,48 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
return nil, firstErr
}
+// cmdInfo will fetch and cache the command policies after the first execution
func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo {
- cmdsInfo, err := c.cmdsInfoCache.Get(ctx)
+ // Use a separate context that won't be canceled to ensure command info lookup
+ // doesn't fail due to original context cancellation
+ cmdInfoCtx := c.context(ctx)
+ if c.opt.ContextTimeoutEnabled && ctx != nil {
+ // If context timeout is enabled, still use a reasonable timeout
+ var cancel context.CancelFunc
+ cmdInfoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ }
+
+ cmdsInfo, err := c.cmdsInfoCache.Get(cmdInfoCtx)
if err != nil {
- internal.Logger.Printf(context.TODO(), "getting command info: %s", err)
+ internal.Logger.Printf(cmdInfoCtx, "getting command info: %s", err)
return nil
}
info := cmdsInfo[name]
if info == nil {
- internal.Logger.Printf(context.TODO(), "info for cmd=%s not found", name)
+ internal.Logger.Printf(cmdInfoCtx, "info for cmd=%s not found", name)
}
+
return info
}
-func (c *ClusterClient) cmdSlot(cmd Cmder, preferredRandomSlot int) int {
+func (c *ClusterClient) cmdSlot(cmd Cmder, prefferedSlot int) int {
args := cmd.Args()
if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") {
return args[2].(int)
}
- return cmdSlot(cmd, cmdFirstKeyPos(cmd), preferredRandomSlot)
+ return cmdSlot(cmd, cmdFirstKeyPos(cmd), prefferedSlot)
}
-func cmdSlot(cmd Cmder, pos int, preferredRandomSlot int) int {
+func cmdSlot(cmd Cmder, pos int, prefferedRandomSlot int) int {
if pos == 0 {
- if preferredRandomSlot != -1 {
- return preferredRandomSlot
+ if prefferedRandomSlot != -1 {
+ return prefferedRandomSlot
}
- return hashtag.RandomSlot()
+ // Return -1 for keyless commands to signal that ShardPicker should be used
+ return -1
}
firstKey := cmd.stringArg(pos)
return hashtag.Slot(firstKey)
@@ -2072,6 +2345,36 @@ func (c *ClusterClient) cmdNode(
return state.slotMasterNode(slot)
}
+func (c *ClusterClient) cmdNodeWithShardPicker(
+ ctx context.Context,
+ cmdName string,
+ slot int,
+ shardPicker routing.ShardPicker,
+) (*clusterNode, error) {
+ state, err := c.state.Get(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ // For keyless commands (slot == -1), use ShardPicker to select a shard
+ // This respects the user's configured ShardPicker policy
+ if slot == -1 {
+ if len(state.Masters) == 0 {
+ return nil, errClusterNoNodes
+ }
+ idx := shardPicker.Next(len(state.Masters))
+ return state.Masters[idx], nil
+ }
+
+ if c.opt.ReadOnly {
+ cmdInfo := c.cmdInfo(ctx, cmdName)
+ if cmdInfo != nil && cmdInfo.ReadOnly {
+ return c.slotReadOnlyNode(state, slot)
+ }
+ }
+ return state.slotMasterNode(slot)
+}
+
func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) {
if c.opt.RouteByLatency {
return state.slotClosestNode(slot)
@@ -2079,6 +2382,11 @@ func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*cluste
if c.opt.RouteRandomly {
return state.slotRandomNode(slot)
}
+
+ if c.opt.ShardPicker != nil {
+ return state.slotShardPickerSlaveNode(slot, c.opt.ShardPicker)
+ }
+
return state.slotSlaveNode(slot)
}
@@ -2126,6 +2434,31 @@ func (c *ClusterClient) context(ctx context.Context) context.Context {
return context.Background()
}
+func (c *ClusterClient) GetResolver() *commandInfoResolver {
+ return c.cmdInfoResolver
+}
+
+func (c *ClusterClient) SetCommandInfoResolver(cmdInfoResolver *commandInfoResolver) {
+ c.cmdInfoResolver = cmdInfoResolver
+}
+
+// extractCommandInfo retrieves the routing policy for a command
+func (c *ClusterClient) extractCommandInfo(ctx context.Context, cmd Cmder) *routing.CommandPolicy {
+ if cmdInfo := c.cmdInfo(ctx, cmd.Name()); cmdInfo != nil && cmdInfo.CommandPolicy != nil {
+ return cmdInfo.CommandPolicy
+ }
+
+ return nil
+}
+
+// NewDynamicResolver returns a CommandInfoResolver
+// that uses the underlying cmdInfo cache to resolve the policies
+func (c *ClusterClient) NewDynamicResolver() *commandInfoResolver {
+ return &commandInfoResolver{
+ resolveFunc: c.extractCommandInfo,
+ }
+}
+
func appendIfNotExist[T comparable](vals []T, newVal T) []T {
for _, v := range vals {
if v == newVal {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/osscluster_router.go b/backend/vendor/github.com/redis/go-redis/v9/osscluster_router.go
new file mode 100644
index 00000000..3b001fef
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/osscluster_router.go
@@ -0,0 +1,992 @@
+package redis
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "reflect"
+ "sync"
+ "time"
+
+ "github.com/redis/go-redis/v9/internal/hashtag"
+ "github.com/redis/go-redis/v9/internal/routing"
+)
+
+var (
+ errInvalidCmdPointer = errors.New("redis: invalid command pointer")
+ errNoCmdsToAggregate = errors.New("redis: no commands to aggregate")
+ errNoResToAggregate = errors.New("redis: no results to aggregate")
+ errInvalidCursorCmdArgsCount = errors.New("redis: FT.CURSOR command requires at least 3 arguments")
+ errInvalidCursorIdType = errors.New("redis: invalid cursor ID type")
+)
+
+// slotResult represents the result of executing a command on a specific slot
+type slotResult struct {
+ cmd Cmder
+ keys []string
+ err error
+}
+
+// routeAndRun routes a command to the appropriate cluster nodes and executes it
+func (c *ClusterClient) routeAndRun(ctx context.Context, cmd Cmder, node *clusterNode) error {
+ var policy *routing.CommandPolicy
+ if c.cmdInfoResolver != nil {
+ policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)
+ }
+
+ // Set stepCount from cmdInfo if not already set
+ if cmd.stepCount() == 0 {
+ if cmdInfo := c.cmdInfo(ctx, cmd.Name()); cmdInfo != nil && cmdInfo.StepCount > 0 {
+ cmd.SetStepCount(cmdInfo.StepCount)
+ }
+ }
+
+ if policy == nil {
+ return c.executeDefault(ctx, cmd, policy, node)
+ }
+ switch policy.Request {
+ case routing.ReqAllNodes:
+ return c.executeOnAllNodes(ctx, cmd, policy)
+ case routing.ReqAllShards:
+ return c.executeOnAllShards(ctx, cmd, policy)
+ case routing.ReqMultiShard:
+ return c.executeMultiShard(ctx, cmd, policy)
+ case routing.ReqSpecial:
+ return c.executeSpecialCommand(ctx, cmd, policy, node)
+ default:
+ return c.executeDefault(ctx, cmd, policy, node)
+ }
+}
+
+// executeDefault handles standard command routing based on keys
+func (c *ClusterClient) executeDefault(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy, node *clusterNode) error {
+ if policy != nil && !c.hasKeys(cmd) {
+ if c.readOnlyEnabled() && policy.IsReadOnly() {
+ return c.executeOnArbitraryNode(ctx, cmd)
+ }
+ }
+
+ return node.Client.Process(ctx, cmd)
+}
+
+// executeOnArbitraryNode routes command to an arbitrary node
+func (c *ClusterClient) executeOnArbitraryNode(ctx context.Context, cmd Cmder) error {
+ node := c.pickArbitraryNode(ctx)
+ if node == nil {
+ return errClusterNoNodes
+ }
+ return node.Client.Process(ctx, cmd)
+}
+
+// executeOnAllNodes executes command on all nodes (masters and replicas)
+func (c *ClusterClient) executeOnAllNodes(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {
+ state, err := c.state.Get(ctx)
+ if err != nil {
+ return err
+ }
+
+ nodes := append(state.Masters, state.Slaves...)
+ if len(nodes) == 0 {
+ return errClusterNoNodes
+ }
+
+ return c.executeParallel(ctx, cmd, nodes, policy)
+}
+
+// executeOnAllShards executes command on all master shards
+func (c *ClusterClient) executeOnAllShards(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {
+ state, err := c.state.Get(ctx)
+ if err != nil {
+ return err
+ }
+
+ if len(state.Masters) == 0 {
+ return errClusterNoNodes
+ }
+
+ return c.executeParallel(ctx, cmd, state.Masters, policy)
+}
+
+// executeMultiShard handles commands that operate on multiple keys across shards
+func (c *ClusterClient) executeMultiShard(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {
+ args := cmd.Args()
+ firstKeyPos := int(cmdFirstKeyPos(cmd))
+ stepCount := int(cmd.stepCount())
+ if stepCount == 0 {
+ stepCount = 1 // Default to 1 if not set
+ }
+
+ if firstKeyPos == 0 || firstKeyPos >= len(args) {
+ return fmt.Errorf("redis: multi-shard command %s has no key arguments", cmd.Name())
+ }
+
+ // Group keys by slot
+ slotMap := make(map[int][]string)
+ keyOrder := make([]string, 0)
+
+ for i := firstKeyPos; i < len(args); i += stepCount {
+ key, ok := args[i].(string)
+ if !ok {
+ return fmt.Errorf("redis: non-string key at position %d: %v", i, args[i])
+ }
+
+ slot := hashtag.Slot(key)
+ slotMap[slot] = append(slotMap[slot], key)
+ for j := 1; j < stepCount; j++ {
+ if i+j >= len(args) {
+ break
+ }
+ slotMap[slot] = append(slotMap[slot], args[i+j].(string))
+ }
+ keyOrder = append(keyOrder, key)
+ }
+
+ return c.executeMultiSlot(ctx, cmd, slotMap, keyOrder, policy)
+}
+
+// executeMultiSlot executes commands across multiple slots concurrently
+func (c *ClusterClient) executeMultiSlot(ctx context.Context, cmd Cmder, slotMap map[int][]string, keyOrder []string, policy *routing.CommandPolicy) error {
+ results := make(chan slotResult, len(slotMap))
+ var wg sync.WaitGroup
+
+ // Execute on each slot concurrently
+ for slot, keys := range slotMap {
+ wg.Add(1)
+ go func(slot int, keys []string) {
+ defer wg.Done()
+
+ node, err := c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)
+ if err != nil {
+ results <- slotResult{nil, keys, err}
+ return
+ }
+
+ // Create a command for this specific slot's keys
+ subCmd := c.createSlotSpecificCommand(ctx, cmd, keys)
+ err = node.Client.Process(ctx, subCmd)
+ results <- slotResult{subCmd, keys, err}
+ }(slot, keys)
+ }
+
+ go func() {
+ wg.Wait()
+ close(results)
+ }()
+
+ return c.aggregateMultiSlotResults(ctx, cmd, results, keyOrder, policy)
+}
+
+// createSlotSpecificCommand creates a new command for a specific slot's keys
+func (c *ClusterClient) createSlotSpecificCommand(ctx context.Context, originalCmd Cmder, keys []string) Cmder {
+ originalArgs := originalCmd.Args()
+ firstKeyPos := int(cmdFirstKeyPos(originalCmd))
+
+ // Build new args with only the specified keys
+ newArgs := make([]interface{}, 0, firstKeyPos+len(keys))
+
+ // Copy command name and arguments before the keys
+ newArgs = append(newArgs, originalArgs[:firstKeyPos]...)
+
+ // Add the slot-specific keys
+ for _, key := range keys {
+ newArgs = append(newArgs, key)
+ }
+
+ // Create a new command of the same type using the helper function
+ return createCommandByType(ctx, originalCmd.GetCmdType(), newArgs...)
+}
+
+// createCommandByType creates a new command of the specified type with the given arguments
+func createCommandByType(ctx context.Context, cmdType CmdType, args ...interface{}) Cmder {
+ switch cmdType {
+ case CmdTypeString:
+ return NewStringCmd(ctx, args...)
+ case CmdTypeInt:
+ return NewIntCmd(ctx, args...)
+ case CmdTypeBool:
+ return NewBoolCmd(ctx, args...)
+ case CmdTypeFloat:
+ return NewFloatCmd(ctx, args...)
+ case CmdTypeStringSlice:
+ return NewStringSliceCmd(ctx, args...)
+ case CmdTypeIntSlice:
+ return NewIntSliceCmd(ctx, args...)
+ case CmdTypeFloatSlice:
+ return NewFloatSliceCmd(ctx, args...)
+ case CmdTypeBoolSlice:
+ return NewBoolSliceCmd(ctx, args...)
+ case CmdTypeStatus:
+ return NewStatusCmd(ctx, args...)
+ case CmdTypeTime:
+ return NewTimeCmd(ctx, args...)
+ case CmdTypeMapStringString:
+ return NewMapStringStringCmd(ctx, args...)
+ case CmdTypeMapStringInt:
+ return NewMapStringIntCmd(ctx, args...)
+ case CmdTypeMapStringInterface:
+ return NewMapStringInterfaceCmd(ctx, args...)
+ case CmdTypeMapStringInterfaceSlice:
+ return NewMapStringInterfaceSliceCmd(ctx, args...)
+ case CmdTypeSlice:
+ return NewSliceCmd(ctx, args...)
+ case CmdTypeStringStructMap:
+ return NewStringStructMapCmd(ctx, args...)
+ case CmdTypeXMessageSlice:
+ return NewXMessageSliceCmd(ctx, args...)
+ case CmdTypeXStreamSlice:
+ return NewXStreamSliceCmd(ctx, args...)
+ case CmdTypeXPending:
+ return NewXPendingCmd(ctx, args...)
+ case CmdTypeXPendingExt:
+ return NewXPendingExtCmd(ctx, args...)
+ case CmdTypeXAutoClaim:
+ return NewXAutoClaimCmd(ctx, args...)
+ case CmdTypeXAutoClaimJustID:
+ return NewXAutoClaimJustIDCmd(ctx, args...)
+ case CmdTypeXInfoStreamFull:
+ return NewXInfoStreamFullCmd(ctx, args...)
+ case CmdTypeZSlice:
+ return NewZSliceCmd(ctx, args...)
+ case CmdTypeZWithKey:
+ return NewZWithKeyCmd(ctx, args...)
+ case CmdTypeClusterSlots:
+ return NewClusterSlotsCmd(ctx, args...)
+ case CmdTypeGeoPos:
+ return NewGeoPosCmd(ctx, args...)
+ case CmdTypeCommandsInfo:
+ return NewCommandsInfoCmd(ctx, args...)
+ case CmdTypeSlowLog:
+ return NewSlowLogCmd(ctx, args...)
+ case CmdTypeKeyValues:
+ return NewKeyValuesCmd(ctx, args...)
+ case CmdTypeZSliceWithKey:
+ return NewZSliceWithKeyCmd(ctx, args...)
+ case CmdTypeFunctionList:
+ return NewFunctionListCmd(ctx, args...)
+ case CmdTypeFunctionStats:
+ return NewFunctionStatsCmd(ctx, args...)
+ case CmdTypeKeyFlags:
+ return NewKeyFlagsCmd(ctx, args...)
+ case CmdTypeDuration:
+ return NewDurationCmd(ctx, time.Millisecond, args...)
+ }
+ return NewCmd(ctx, args...)
+}
+
+// executeSpecialCommand handles commands with special routing requirements
+func (c *ClusterClient) executeSpecialCommand(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy, node *clusterNode) error {
+ switch cmd.Name() {
+ case "ft.cursor":
+ return c.executeCursorCommand(ctx, cmd)
+ default:
+ return c.executeDefault(ctx, cmd, policy, node)
+ }
+}
+
+// executeCursorCommand handles FT.CURSOR commands with sticky routing
+func (c *ClusterClient) executeCursorCommand(ctx context.Context, cmd Cmder) error {
+ args := cmd.Args()
+ if len(args) < 4 {
+ return errInvalidCursorCmdArgsCount
+ }
+
+ cursorID, ok := args[3].(string)
+ if !ok {
+ return errInvalidCursorIdType
+ }
+
+ // Route based on cursor ID to maintain stickiness
+ slot := hashtag.Slot(cursorID)
+ node, err := c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)
+ if err != nil {
+ return err
+ }
+
+ return node.Client.Process(ctx, cmd)
+}
+
+// executeParallel executes a command on multiple nodes concurrently
+func (c *ClusterClient) executeParallel(ctx context.Context, cmd Cmder, nodes []*clusterNode, policy *routing.CommandPolicy) error {
+ if len(nodes) == 0 {
+ return errClusterNoNodes
+ }
+
+ if len(nodes) == 1 {
+ return nodes[0].Client.Process(ctx, cmd)
+ }
+
+ type nodeResult struct {
+ cmd Cmder
+ err error
+ }
+
+ results := make(chan nodeResult, len(nodes))
+ var wg sync.WaitGroup
+
+ for _, node := range nodes {
+ wg.Add(1)
+ go func(n *clusterNode) {
+ defer wg.Done()
+ cmdCopy := cmd.Clone()
+ err := n.Client.Process(ctx, cmdCopy)
+ results <- nodeResult{cmdCopy, err}
+ }(node)
+ }
+
+ go func() {
+ wg.Wait()
+ close(results)
+ }()
+
+ // Collect results and check for errors
+ cmds := make([]Cmder, 0, len(nodes))
+ var firstErr error
+
+ for result := range results {
+ if result.err != nil && firstErr == nil {
+ firstErr = result.err
+ }
+ cmds = append(cmds, result.cmd)
+ }
+
+ // If there was an error and no policy specified, fail fast
+ if firstErr != nil && (policy == nil || policy.Response == routing.RespDefaultKeyless) {
+ cmd.SetErr(firstErr)
+ return firstErr
+ }
+
+ return c.aggregateResponses(cmd, cmds, policy)
+}
+
+// aggregateMultiSlotResults aggregates results from multi-slot execution
+func (c *ClusterClient) aggregateMultiSlotResults(ctx context.Context, cmd Cmder, results <-chan slotResult, keyOrder []string, policy *routing.CommandPolicy) error {
+ keyedResults := make(map[string]routing.AggregatorResErr)
+ var firstErr error
+
+ for result := range results {
+ if result.err != nil && firstErr == nil {
+ firstErr = result.err
+ }
+ if result.cmd != nil && result.err == nil {
+ value, err := ExtractCommandValue(result.cmd)
+
+ // Check if the result is a slice (e.g., from MGET)
+ if sliceValue, ok := value.([]interface{}); ok {
+ // Map each element to its corresponding key
+ for i, key := range result.keys {
+ if i < len(sliceValue) {
+ keyedResults[key] = routing.AggregatorResErr{Result: sliceValue[i], Err: err}
+ } else {
+ keyedResults[key] = routing.AggregatorResErr{Result: nil, Err: err}
+ }
+ }
+ } else {
+ // For non-slice results, map the entire result to each key
+ for _, key := range result.keys {
+ keyedResults[key] = routing.AggregatorResErr{Result: value, Err: err}
+ }
+ }
+ }
+
+ // TODO: return multiple errors by order when we will implement multiple errors returning
+ if result.err != nil {
+ firstErr = result.err
+ }
+ }
+
+ return c.aggregateKeyedValues(cmd, keyedResults, keyOrder, policy)
+}
+
+// aggregateKeyedValues aggregates individual key-value pairs while preserving key order
+func (c *ClusterClient) aggregateKeyedValues(cmd Cmder, keyedResults map[string]routing.AggregatorResErr, keyOrder []string, policy *routing.CommandPolicy) error {
+ if len(keyedResults) == 0 {
+ return errNoResToAggregate
+ }
+
+ aggregator := c.createAggregator(policy, cmd, true)
+
+ // Set key order for keyed aggregators
+ var keyedAgg *routing.DefaultKeyedAggregator
+ var isKeyedAgg bool
+ var err error
+ if keyedAgg, isKeyedAgg = aggregator.(*routing.DefaultKeyedAggregator); isKeyedAgg {
+ err = keyedAgg.BatchAddWithKeyOrder(keyedResults, keyOrder)
+ } else {
+ err = aggregator.BatchAdd(keyedResults)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ return c.finishAggregation(cmd, aggregator)
+}
+
+// aggregateResponses aggregates multiple shard responses
+func (c *ClusterClient) aggregateResponses(cmd Cmder, cmds []Cmder, policy *routing.CommandPolicy) error {
+ if len(cmds) == 0 {
+ return errNoCmdsToAggregate
+ }
+
+ if len(cmds) == 1 {
+ shardCmd := cmds[0]
+ if err := shardCmd.Err(); err != nil {
+ cmd.SetErr(err)
+ return err
+ }
+ value, _ := ExtractCommandValue(shardCmd)
+ return c.setCommandValue(cmd, value)
+ }
+
+ aggregator := c.createAggregator(policy, cmd, false)
+
+ batchWithErrs := []routing.AggregatorResErr{}
+ // Add all results to aggregator
+ for _, shardCmd := range cmds {
+ value, err := ExtractCommandValue(shardCmd)
+ batchWithErrs = append(batchWithErrs, routing.AggregatorResErr{
+ Result: value,
+ Err: err,
+ })
+ }
+
+ err := aggregator.BatchSlice(batchWithErrs)
+ if err != nil {
+ return err
+ }
+
+ return c.finishAggregation(cmd, aggregator)
+}
+
+// createAggregator creates the appropriate response aggregator
+func (c *ClusterClient) createAggregator(policy *routing.CommandPolicy, cmd Cmder, isKeyed bool) routing.ResponseAggregator {
+ if policy != nil {
+ return routing.NewResponseAggregator(policy.Response, cmd.Name())
+ }
+
+ if !isKeyed {
+ firstKeyPos := cmdFirstKeyPos(cmd)
+ isKeyed = firstKeyPos > 0
+ }
+
+ return routing.NewDefaultAggregator(isKeyed)
+}
+
+// finishAggregation completes the aggregation process and sets the result
+func (c *ClusterClient) finishAggregation(cmd Cmder, aggregator routing.ResponseAggregator) error {
+ finalValue, finalErr := aggregator.Result()
+ if finalErr != nil {
+ cmd.SetErr(finalErr)
+ return finalErr
+ }
+
+ return c.setCommandValue(cmd, finalValue)
+}
+
+// pickArbitraryNode selects a master or slave shard using the configured ShardPicker
+func (c *ClusterClient) pickArbitraryNode(ctx context.Context) *clusterNode {
+ state, err := c.state.Get(ctx)
+ if err != nil || len(state.Masters) == 0 {
+ return nil
+ }
+
+ allNodes := append(state.Masters, state.Slaves...)
+
+ idx := c.opt.ShardPicker.Next(len(allNodes))
+ return allNodes[idx]
+}
+
+// hasKeys checks if a command operates on keys
+func (c *ClusterClient) hasKeys(cmd Cmder) bool {
+ firstKeyPos := cmdFirstKeyPos(cmd)
+ return firstKeyPos > 0
+}
+
+func (c *ClusterClient) readOnlyEnabled() bool {
+ return c.opt.ReadOnly
+}
+
+// setCommandValue sets the aggregated value on a command using the enum-based approach
+func (c *ClusterClient) setCommandValue(cmd Cmder, value interface{}) error {
+ // If value is nil, it might mean ExtractCommandValue couldn't extract the value
+ // but the command might have executed successfully. In this case, don't set an error.
+ if value == nil {
+ // ExtractCommandValue returned nil - this means the command type is not supported
+ // in the aggregation flow. This is a programming error, not a runtime error.
+ if cmd.Err() != nil {
+ // Command already has an error, preserve it
+ return cmd.Err()
+ }
+ // Command executed successfully but we can't extract/set the aggregated value
+ // This indicates the command type needs to be added to ExtractCommandValue
+ return fmt.Errorf("redis: cannot aggregate command %s: unsupported command type %d",
+ cmd.Name(), cmd.GetCmdType())
+ }
+
+ switch cmd.GetCmdType() {
+ case CmdTypeGeneric:
+ if c, ok := cmd.(*Cmd); ok {
+ c.SetVal(value)
+ }
+ case CmdTypeString:
+ if c, ok := cmd.(*StringCmd); ok {
+ if v, ok := value.(string); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeInt:
+ if c, ok := cmd.(*IntCmd); ok {
+ if v, ok := value.(int64); ok {
+ c.SetVal(v)
+ } else if v, ok := value.(float64); ok {
+ c.SetVal(int64(v))
+ }
+ }
+ case CmdTypeBool:
+ if c, ok := cmd.(*BoolCmd); ok {
+ if v, ok := value.(bool); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeFloat:
+ if c, ok := cmd.(*FloatCmd); ok {
+ if v, ok := value.(float64); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeStringSlice:
+ if c, ok := cmd.(*StringSliceCmd); ok {
+ if v, ok := value.([]string); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeIntSlice:
+ if c, ok := cmd.(*IntSliceCmd); ok {
+ if v, ok := value.([]int64); ok {
+ c.SetVal(v)
+ } else if v, ok := value.([]float64); ok {
+ els := len(v)
+ intSlc := make([]int, els)
+ for i := range v {
+ intSlc[i] = int(v[i])
+ }
+ }
+ }
+ case CmdTypeFloatSlice:
+ if c, ok := cmd.(*FloatSliceCmd); ok {
+ if v, ok := value.([]float64); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeBoolSlice:
+ if c, ok := cmd.(*BoolSliceCmd); ok {
+ if v, ok := value.([]bool); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeMapStringString:
+ if c, ok := cmd.(*MapStringStringCmd); ok {
+ if v, ok := value.(map[string]string); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeMapStringInt:
+ if c, ok := cmd.(*MapStringIntCmd); ok {
+ if v, ok := value.(map[string]int64); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeMapStringInterface:
+ if c, ok := cmd.(*MapStringInterfaceCmd); ok {
+ if v, ok := value.(map[string]interface{}); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeSlice:
+ if c, ok := cmd.(*SliceCmd); ok {
+ if v, ok := value.([]interface{}); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeStatus:
+ if c, ok := cmd.(*StatusCmd); ok {
+ if v, ok := value.(string); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeDuration:
+ if c, ok := cmd.(*DurationCmd); ok {
+ if v, ok := value.(time.Duration); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeTime:
+ if c, ok := cmd.(*TimeCmd); ok {
+ if v, ok := value.(time.Time); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeKeyValueSlice:
+ if c, ok := cmd.(*KeyValueSliceCmd); ok {
+ if v, ok := value.([]KeyValue); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeStringStructMap:
+ if c, ok := cmd.(*StringStructMapCmd); ok {
+ if v, ok := value.(map[string]struct{}); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeXMessageSlice:
+ if c, ok := cmd.(*XMessageSliceCmd); ok {
+ if v, ok := value.([]XMessage); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeXStreamSlice:
+ if c, ok := cmd.(*XStreamSliceCmd); ok {
+ if v, ok := value.([]XStream); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeXPending:
+ if c, ok := cmd.(*XPendingCmd); ok {
+ if v, ok := value.(*XPending); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeXPendingExt:
+ if c, ok := cmd.(*XPendingExtCmd); ok {
+ if v, ok := value.([]XPendingExt); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeXAutoClaim:
+ if c, ok := cmd.(*XAutoClaimCmd); ok {
+ if v, ok := value.(CmdTypeXAutoClaimValue); ok {
+ c.SetVal(v.messages, v.start)
+ }
+ }
+ case CmdTypeXAutoClaimJustID:
+ if c, ok := cmd.(*XAutoClaimJustIDCmd); ok {
+ if v, ok := value.(CmdTypeXAutoClaimJustIDValue); ok {
+ c.SetVal(v.ids, v.start)
+ }
+ }
+ case CmdTypeXInfoConsumers:
+ if c, ok := cmd.(*XInfoConsumersCmd); ok {
+ if v, ok := value.([]XInfoConsumer); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeXInfoGroups:
+ if c, ok := cmd.(*XInfoGroupsCmd); ok {
+ if v, ok := value.([]XInfoGroup); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeXInfoStream:
+ if c, ok := cmd.(*XInfoStreamCmd); ok {
+ if v, ok := value.(*XInfoStream); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeXInfoStreamFull:
+ if c, ok := cmd.(*XInfoStreamFullCmd); ok {
+ if v, ok := value.(*XInfoStreamFull); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeZSlice:
+ if c, ok := cmd.(*ZSliceCmd); ok {
+ if v, ok := value.([]Z); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeZWithKey:
+ if c, ok := cmd.(*ZWithKeyCmd); ok {
+ if v, ok := value.(*ZWithKey); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeScan:
+ if c, ok := cmd.(*ScanCmd); ok {
+ if v, ok := value.(CmdTypeScanValue); ok {
+ c.SetVal(v.keys, v.cursor)
+ }
+ }
+ case CmdTypeClusterSlots:
+ if c, ok := cmd.(*ClusterSlotsCmd); ok {
+ if v, ok := value.([]ClusterSlot); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeGeoLocation:
+ if c, ok := cmd.(*GeoLocationCmd); ok {
+ if v, ok := value.([]GeoLocation); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeGeoSearchLocation:
+ if c, ok := cmd.(*GeoSearchLocationCmd); ok {
+ if v, ok := value.([]GeoLocation); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeGeoPos:
+ if c, ok := cmd.(*GeoPosCmd); ok {
+ if v, ok := value.([]*GeoPos); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeCommandsInfo:
+ if c, ok := cmd.(*CommandsInfoCmd); ok {
+ if v, ok := value.(map[string]*CommandInfo); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeSlowLog:
+ if c, ok := cmd.(*SlowLogCmd); ok {
+ if v, ok := value.([]SlowLog); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeMapStringStringSlice:
+ if c, ok := cmd.(*MapStringStringSliceCmd); ok {
+ if v, ok := value.([]map[string]string); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeMapMapStringInterface:
+ if c, ok := cmd.(*MapMapStringInterfaceCmd); ok {
+ if v, ok := value.(map[string]interface{}); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeMapStringInterfaceSlice:
+ if c, ok := cmd.(*MapStringInterfaceSliceCmd); ok {
+ if v, ok := value.([]map[string]interface{}); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeKeyValues:
+ if c, ok := cmd.(*KeyValuesCmd); ok {
+ // KeyValuesCmd needs a key string and values slice
+ if v, ok := value.(CmdTypeKeyValuesValue); ok {
+ c.SetVal(v.key, v.values)
+ }
+ }
+ case CmdTypeZSliceWithKey:
+ if c, ok := cmd.(*ZSliceWithKeyCmd); ok {
+ // ZSliceWithKeyCmd needs a key string and Z slice
+ if v, ok := value.(CmdTypeZSliceWithKeyValue); ok {
+ c.SetVal(v.key, v.zSlice)
+ }
+ }
+ case CmdTypeFunctionList:
+ if c, ok := cmd.(*FunctionListCmd); ok {
+ if v, ok := value.([]Library); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeFunctionStats:
+ if c, ok := cmd.(*FunctionStatsCmd); ok {
+ if v, ok := value.(FunctionStats); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeLCS:
+ if c, ok := cmd.(*LCSCmd); ok {
+ if v, ok := value.(*LCSMatch); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeKeyFlags:
+ if c, ok := cmd.(*KeyFlagsCmd); ok {
+ if v, ok := value.([]KeyFlags); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeClusterLinks:
+ if c, ok := cmd.(*ClusterLinksCmd); ok {
+ if v, ok := value.([]ClusterLink); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeClusterShards:
+ if c, ok := cmd.(*ClusterShardsCmd); ok {
+ if v, ok := value.([]ClusterShard); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeRankWithScore:
+ if c, ok := cmd.(*RankWithScoreCmd); ok {
+ if v, ok := value.(RankScore); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeClientInfo:
+ if c, ok := cmd.(*ClientInfoCmd); ok {
+ if v, ok := value.(*ClientInfo); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeACLLog:
+ if c, ok := cmd.(*ACLLogCmd); ok {
+ if v, ok := value.([]*ACLLogEntry); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeInfo:
+ if c, ok := cmd.(*InfoCmd); ok {
+ if v, ok := value.(map[string]map[string]string); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeMonitor:
+ // MonitorCmd doesn't have SetVal method
+ // Skip setting value for MonitorCmd
+ case CmdTypeJSON:
+ if c, ok := cmd.(*JSONCmd); ok {
+ if v, ok := value.(string); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeJSONSlice:
+ if c, ok := cmd.(*JSONSliceCmd); ok {
+ if v, ok := value.([]interface{}); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeIntPointerSlice:
+ if c, ok := cmd.(*IntPointerSliceCmd); ok {
+ if v, ok := value.([]*int64); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeScanDump:
+ if c, ok := cmd.(*ScanDumpCmd); ok {
+ if v, ok := value.(ScanDump); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeBFInfo:
+ if c, ok := cmd.(*BFInfoCmd); ok {
+ if v, ok := value.(BFInfo); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeCFInfo:
+ if c, ok := cmd.(*CFInfoCmd); ok {
+ if v, ok := value.(CFInfo); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeCMSInfo:
+ if c, ok := cmd.(*CMSInfoCmd); ok {
+ if v, ok := value.(CMSInfo); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeTopKInfo:
+ if c, ok := cmd.(*TopKInfoCmd); ok {
+ if v, ok := value.(TopKInfo); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeTDigestInfo:
+ if c, ok := cmd.(*TDigestInfoCmd); ok {
+ if v, ok := value.(TDigestInfo); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeFTSynDump:
+ if c, ok := cmd.(*FTSynDumpCmd); ok {
+ if v, ok := value.([]FTSynDumpResult); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeAggregate:
+ if c, ok := cmd.(*AggregateCmd); ok {
+ if v, ok := value.(*FTAggregateResult); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeFTInfo:
+ if c, ok := cmd.(*FTInfoCmd); ok {
+ if v, ok := value.(FTInfoResult); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeFTSpellCheck:
+ if c, ok := cmd.(*FTSpellCheckCmd); ok {
+ if v, ok := value.([]SpellCheckResult); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeFTSearch:
+ if c, ok := cmd.(*FTSearchCmd); ok {
+ if v, ok := value.(FTSearchResult); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeTSTimestampValue:
+ if c, ok := cmd.(*TSTimestampValueCmd); ok {
+ if v, ok := value.(TSTimestampValue); ok {
+ c.SetVal(v)
+ }
+ }
+ case CmdTypeTSTimestampValueSlice:
+ if c, ok := cmd.(*TSTimestampValueSliceCmd); ok {
+ if v, ok := value.([]TSTimestampValue); ok {
+ c.SetVal(v)
+ }
+ }
+ default:
+ // Fallback to reflection for unknown types
+ return c.setCommandValueReflection(cmd, value)
+ }
+
+ return nil
+}
+
+// setCommandValueReflection is a fallback function that uses reflection
+func (c *ClusterClient) setCommandValueReflection(cmd Cmder, value interface{}) error {
+ cmdValue := reflect.ValueOf(cmd)
+ if cmdValue.Kind() != reflect.Ptr || cmdValue.IsNil() {
+ return errInvalidCmdPointer
+ }
+
+ setValMethod := cmdValue.MethodByName("SetVal")
+ if !setValMethod.IsValid() {
+ return fmt.Errorf("redis: command %T does not have SetVal method", cmd)
+ }
+
+ args := []reflect.Value{reflect.ValueOf(value)}
+
+ switch cmd.(type) {
+ case *XAutoClaimCmd, *XAutoClaimJustIDCmd:
+ args = append(args, reflect.ValueOf(""))
+ case *ScanCmd:
+ args = append(args, reflect.ValueOf(uint64(0)))
+ case *KeyValuesCmd, *ZSliceWithKeyCmd:
+ if key, ok := value.(string); ok {
+ args = []reflect.Value{reflect.ValueOf(key)}
+ if _, ok := cmd.(*ZSliceWithKeyCmd); ok {
+ args = append(args, reflect.ValueOf([]Z{}))
+ } else {
+ args = append(args, reflect.ValueOf([]string{}))
+ }
+ }
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ cmd.SetErr(fmt.Errorf("redis: failed to set command value: %v", r))
+ }
+ }()
+
+ setValMethod.Call(args)
+ return nil
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/otel.go b/backend/vendor/github.com/redis/go-redis/v9/otel.go
new file mode 100644
index 00000000..a81377d4
--- /dev/null
+++ b/backend/vendor/github.com/redis/go-redis/v9/otel.go
@@ -0,0 +1,204 @@
+package redis
+
+import (
+ "context"
+ "net"
+ "time"
+
+ "github.com/redis/go-redis/v9/internal/otel"
+ "github.com/redis/go-redis/v9/internal/pool"
+)
+
+// ConnInfo provides information about a Redis connection for metrics.
+type ConnInfo interface {
+ RemoteAddr() net.Addr
+ PoolName() string
+}
+
+type Pooler interface {
+ PoolStats() *pool.Stats
+}
+
+type PubSubPooler interface {
+ Stats() *pool.PubSubStats
+}
+
+// OTelRecorder is the interface for recording OpenTelemetry metrics.
+
+type OTelRecorder interface {
+ // RecordOperationDuration records the total operation duration (including all retries)
+ RecordOperationDuration(ctx context.Context, duration time.Duration, cmd Cmder, attempts int, err error, cn ConnInfo, dbIndex int)
+
+ // RecordPipelineOperationDuration records the total pipeline/transaction duration.
+ // operationName should be "PIPELINE" for regular pipelines or "MULTI" for transactions.
+ RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn ConnInfo, dbIndex int)
+
+ // RecordConnectionCreateTime records the time it took to create a new connection
+ RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn ConnInfo)
+
+ // RecordConnectionRelaxedTimeout records when connection timeout is relaxed/unrelaxed
+ // delta: +1 for relaxed, -1 for unrelaxed
+ // poolName: name of the connection pool (e.g., "main", "pubsub")
+ // notificationType: the notification type that triggered the timeout relaxation (e.g., "MOVING", "HANDOFF")
+ RecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn ConnInfo, poolName, notificationType string)
+
+ // RecordConnectionHandoff records when a connection is handed off to another node
+ // poolName: name of the connection pool (e.g., "main", "pubsub")
+ RecordConnectionHandoff(ctx context.Context, cn ConnInfo, poolName string)
+
+ // RecordError records client errors (ASK, MOVED, handshake failures, etc.)
+ // errorType: type of error (e.g., "ASK", "MOVED", "HANDSHAKE_FAILED")
+ // statusCode: Redis response status code if available (e.g., "MOVED", "ASK")
+ // isInternal: whether this is an internal error
+ // retryAttempts: number of retry attempts made
+ RecordError(ctx context.Context, errorType string, cn ConnInfo, statusCode string, isInternal bool, retryAttempts int)
+
+ // RecordMaintenanceNotification records when a maintenance notification is received
+ // notificationType: the type of notification (e.g., "MOVING", "MIGRATING", etc.)
+ RecordMaintenanceNotification(ctx context.Context, cn ConnInfo, notificationType string)
+
+ // RecordConnectionWaitTime records the time spent waiting for a connection from the pool
+ RecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn ConnInfo)
+
+ // RecordConnectionClosed records when a connection is closed
+ // reason: reason for closing (e.g., "idle", "max_lifetime", "error", "pool_closed")
+ // err: the error that caused the close (nil for non-error closures)
+ RecordConnectionClosed(ctx context.Context, cn ConnInfo, reason string, err error)
+
+ // RecordPubSubMessage records a Pub/Sub message
+ // direction: "sent" or "received"
+ // channel: channel name (may be hidden for cardinality reduction)
+ // sharded: true for sharded pub/sub (SPUBLISH/SSUBSCRIBE)
+ RecordPubSubMessage(ctx context.Context, cn ConnInfo, direction, channel string, sharded bool)
+
+ // RecordStreamLag records the lag for stream consumer group processing
+ // lag: time difference between message creation and consumption
+ // streamName: name of the stream (may be hidden for cardinality reduction)
+ // consumerGroup: name of the consumer group
+ // consumerName: name of the consumer
+ RecordStreamLag(ctx context.Context, lag time.Duration, cn ConnInfo, streamName, consumerGroup, consumerName string)
+}
+
+// This is used for async gauge metrics that need to pull stats from pools periodically.
+type OTelPoolRegistrar interface {
+ // RegisterPool is called when a new client is created with its main connection pool.
+ // poolName: unique identifier for the pool (e.g., "main_abc123")
+ RegisterPool(poolName string, pool Pooler)
+ // UnregisterPool is called when a client is closed to remove its pool from the registry.
+ UnregisterPool(pool Pooler)
+ // RegisterPubSubPool is called when a new client is created with a PubSub pool.
+ // poolName: unique identifier for the pool (e.g., "main_abc123_pubsub")
+ RegisterPubSubPool(poolName string, pool PubSubPooler)
+ // UnregisterPubSubPool is called when a PubSub client is closed to remove its pool.
+ UnregisterPubSubPool(pool PubSubPooler)
+}
+
+// SetOTelRecorder sets the global OpenTelemetry recorder.
+func SetOTelRecorder(r OTelRecorder) {
+ if r == nil {
+ otel.SetGlobalRecorder(nil)
+ return
+ }
+ otel.SetGlobalRecorder(&otelRecorderAdapter{r})
+}
+
+type otelRecorderAdapter struct {
+ recorder OTelRecorder
+}
+
+// toConnInfo converts *pool.Conn to ConnInfo interface properly.
+// This ensures that a nil *pool.Conn becomes a true nil interface,
+// not a non-nil interface containing a nil pointer.
+func toConnInfo(cn *pool.Conn) ConnInfo {
+ if cn == nil {
+ return nil
+ }
+ return cn
+}
+
+func (a *otelRecorderAdapter) RecordOperationDuration(ctx context.Context, duration time.Duration, cmd otel.Cmder, attempts int, err error, cn *pool.Conn, dbIndex int) {
+ // Convert internal Cmder to public Cmder
+ if publicCmd, ok := cmd.(Cmder); ok {
+ a.recorder.RecordOperationDuration(ctx, duration, publicCmd, attempts, err, toConnInfo(cn), dbIndex)
+ }
+}
+
+func (a *otelRecorderAdapter) RecordPipelineOperationDuration(ctx context.Context, duration time.Duration, operationName string, cmdCount int, attempts int, err error, cn *pool.Conn, dbIndex int) {
+ a.recorder.RecordPipelineOperationDuration(ctx, duration, operationName, cmdCount, attempts, err, toConnInfo(cn), dbIndex)
+}
+
+func (a *otelRecorderAdapter) RecordConnectionCreateTime(ctx context.Context, duration time.Duration, cn *pool.Conn) {
+ a.recorder.RecordConnectionCreateTime(ctx, duration, toConnInfo(cn))
+}
+
+func (a *otelRecorderAdapter) RecordConnectionRelaxedTimeout(ctx context.Context, delta int, cn *pool.Conn, poolName, notificationType string) {
+ a.recorder.RecordConnectionRelaxedTimeout(ctx, delta, toConnInfo(cn), poolName, notificationType)
+}
+
+func (a *otelRecorderAdapter) RecordConnectionHandoff(ctx context.Context, cn *pool.Conn, poolName string) {
+ a.recorder.RecordConnectionHandoff(ctx, toConnInfo(cn), poolName)
+}
+
+func (a *otelRecorderAdapter) RecordError(ctx context.Context, errorType string, cn *pool.Conn, statusCode string, isInternal bool, retryAttempts int) {
+ a.recorder.RecordError(ctx, errorType, toConnInfo(cn), statusCode, isInternal, retryAttempts)
+}
+
+func (a *otelRecorderAdapter) RecordMaintenanceNotification(ctx context.Context, cn *pool.Conn, notificationType string) {
+ a.recorder.RecordMaintenanceNotification(ctx, toConnInfo(cn), notificationType)
+}
+
+func (a *otelRecorderAdapter) RecordConnectionWaitTime(ctx context.Context, duration time.Duration, cn *pool.Conn) {
+ a.recorder.RecordConnectionWaitTime(ctx, duration, toConnInfo(cn))
+}
+
+func (a *otelRecorderAdapter) RecordConnectionClosed(ctx context.Context, cn *pool.Conn, reason string, err error) {
+ a.recorder.RecordConnectionClosed(ctx, toConnInfo(cn), reason, err)
+}
+
+func (a *otelRecorderAdapter) RecordPubSubMessage(ctx context.Context, cn *pool.Conn, direction, channel string, sharded bool) {
+ a.recorder.RecordPubSubMessage(ctx, toConnInfo(cn), direction, channel, sharded)
+}
+
+func (a *otelRecorderAdapter) RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string) {
+ a.recorder.RecordStreamLag(ctx, lag, toConnInfo(cn), streamName, consumerGroup, consumerName)
+}
+
+func (a *otelRecorderAdapter) RegisterPool(poolName string, p pool.Pooler) {
+ if registrar, ok := a.recorder.(OTelPoolRegistrar); ok {
+ registrar.RegisterPool(poolName, &poolerAdapter{p})
+ }
+}
+
+func (a *otelRecorderAdapter) UnregisterPool(p pool.Pooler) {
+ if registrar, ok := a.recorder.(OTelPoolRegistrar); ok {
+ registrar.UnregisterPool(&poolerAdapter{p})
+ }
+}
+
+func (a *otelRecorderAdapter) RegisterPubSubPool(poolName string, p otel.PubSubPooler) {
+ if registrar, ok := a.recorder.(OTelPoolRegistrar); ok {
+ registrar.RegisterPubSubPool(poolName, &pubSubPoolerAdapter{p})
+ }
+}
+
+func (a *otelRecorderAdapter) UnregisterPubSubPool(p otel.PubSubPooler) {
+ if registrar, ok := a.recorder.(OTelPoolRegistrar); ok {
+ registrar.UnregisterPubSubPool(&pubSubPoolerAdapter{p})
+ }
+}
+
+type poolerAdapter struct {
+ p pool.Pooler
+}
+
+func (a *poolerAdapter) PoolStats() *pool.Stats {
+ return a.p.Stats()
+}
+
+type pubSubPoolerAdapter struct {
+ p otel.PubSubPooler
+}
+
+func (a *pubSubPoolerAdapter) Stats() *pool.PubSubStats {
+ return a.p.Stats()
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/probabilistic.go b/backend/vendor/github.com/redis/go-redis/v9/probabilistic.go
index c26e7cad..ee67911e 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/probabilistic.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/probabilistic.go
@@ -225,8 +225,9 @@ type ScanDumpCmd struct {
func newScanDumpCmd(ctx context.Context, args ...interface{}) *ScanDumpCmd {
return &ScanDumpCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeScanDump,
},
}
}
@@ -270,6 +271,13 @@ func (cmd *ScanDumpCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *ScanDumpCmd) Clone() Cmder {
+ return &ScanDumpCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val, // ScanDump is a simple struct, can be copied directly
+ }
+}
+
// Returns information about a Bloom filter.
// For more information - https://redis.io/commands/bf.info/
func (c cmdable) BFInfo(ctx context.Context, key string) *BFInfoCmd {
@@ -296,8 +304,9 @@ type BFInfoCmd struct {
func NewBFInfoCmd(ctx context.Context, args ...interface{}) *BFInfoCmd {
return &BFInfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeBFInfo,
},
}
}
@@ -388,6 +397,13 @@ func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *BFInfoCmd) Clone() Cmder {
+ return &BFInfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val, // BFInfo is a simple struct, can be copied directly
+ }
+}
+
// BFInfoCapacity returns information about the capacity of a Bloom filter.
// For more information - https://redis.io/commands/bf.info/
func (c cmdable) BFInfoCapacity(ctx context.Context, key string) *BFInfoCmd {
@@ -625,8 +641,9 @@ type CFInfoCmd struct {
func NewCFInfoCmd(ctx context.Context, args ...interface{}) *CFInfoCmd {
return &CFInfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeCFInfo,
},
}
}
@@ -692,6 +709,13 @@ func (cmd *CFInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *CFInfoCmd) Clone() Cmder {
+ return &CFInfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val, // CFInfo is a simple struct, can be copied directly
+ }
+}
+
// CFInfo returns information about a Cuckoo filter.
// For more information - https://redis.io/commands/cf.info/
func (c cmdable) CFInfo(ctx context.Context, key string) *CFInfoCmd {
@@ -787,8 +811,9 @@ type CMSInfoCmd struct {
func NewCMSInfoCmd(ctx context.Context, args ...interface{}) *CMSInfoCmd {
return &CMSInfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeCMSInfo,
},
}
}
@@ -843,6 +868,13 @@ func (cmd *CMSInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *CMSInfoCmd) Clone() Cmder {
+ return &CMSInfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val, // CMSInfo is a simple struct, can be copied directly
+ }
+}
+
// CMSInfo returns information about a Count-Min Sketch filter.
// For more information - https://redis.io/commands/cms.info/
func (c cmdable) CMSInfo(ctx context.Context, key string) *CMSInfoCmd {
@@ -980,8 +1012,9 @@ type TopKInfoCmd struct {
func NewTopKInfoCmd(ctx context.Context, args ...interface{}) *TopKInfoCmd {
return &TopKInfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeTopKInfo,
},
}
}
@@ -1038,6 +1071,13 @@ func (cmd *TopKInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *TopKInfoCmd) Clone() Cmder {
+ return &TopKInfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val, // TopKInfo is a simple struct, can be copied directly
+ }
+}
+
// TopKInfo returns information about a Top-K filter.
// For more information - https://redis.io/commands/topk.info/
func (c cmdable) TopKInfo(ctx context.Context, key string) *TopKInfoCmd {
@@ -1227,8 +1267,9 @@ type TDigestInfoCmd struct {
func NewTDigestInfoCmd(ctx context.Context, args ...interface{}) *TDigestInfoCmd {
return &TDigestInfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeTDigestInfo,
},
}
}
@@ -1295,6 +1336,13 @@ func (cmd *TDigestInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *TDigestInfoCmd) Clone() Cmder {
+ return &TDigestInfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val, // TDigestInfo is a simple struct, can be copied directly
+ }
+}
+
// TDigestInfo returns information about a t-Digest data structure.
// For more information - https://redis.io/commands/tdigest.info/
func (c cmdable) TDigestInfo(ctx context.Context, key string) *TDigestInfoCmd {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/pubsub.go b/backend/vendor/github.com/redis/go-redis/v9/pubsub.go
index 959a5c45..49eec935 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/pubsub.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/pubsub.go
@@ -8,6 +8,7 @@ import (
"time"
"github.com/redis/go-redis/v9/internal"
+ "github.com/redis/go-redis/v9/internal/otel"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/push"
@@ -403,7 +404,7 @@ func (p *Pong) String() string {
return "Pong"
}
-func (c *PubSub) newMessage(reply interface{}) (interface{}, error) {
+func (c *PubSub) newMessage(ctx context.Context, cn *pool.Conn, reply interface{}) (interface{}, error) {
switch reply := reply.(type) {
case string:
return &Pong{
@@ -420,30 +421,42 @@ func (c *PubSub) newMessage(reply interface{}) (interface{}, error) {
Count: int(reply[2].(int64)),
}, nil
case "message", "smessage":
+ channel := reply[1].(string)
+ sharded := kind == "smessage"
switch payload := reply[2].(type) {
case string:
- return &Message{
- Channel: reply[1].(string),
+ msg := &Message{
+ Channel: channel,
Payload: payload,
- }, nil
+ }
+ // Record PubSub message received
+ otel.RecordPubSubMessage(ctx, cn, "received", channel, sharded)
+ return msg, nil
case []interface{}:
ss := make([]string, len(payload))
for i, s := range payload {
ss[i] = s.(string)
}
- return &Message{
- Channel: reply[1].(string),
+ msg := &Message{
+ Channel: channel,
PayloadSlice: ss,
- }, nil
+ }
+ // Record PubSub message received
+ otel.RecordPubSubMessage(ctx, cn, "received", channel, sharded)
+ return msg, nil
default:
return nil, fmt.Errorf("redis: unsupported pubsub message payload: %T", payload)
}
case "pmessage":
- return &Message{
+ channel := reply[2].(string)
+ msg := &Message{
Pattern: reply[1].(string),
- Channel: reply[2].(string),
+ Channel: channel,
Payload: reply[3].(string),
- }, nil
+ }
+ // Record PubSub message received (pattern message, not sharded)
+ otel.RecordPubSubMessage(ctx, cn, "received", channel, false)
+ return msg, nil
case "pong":
return &Pong{
Payload: reply[1].(string),
@@ -485,7 +498,7 @@ func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (int
return nil, err
}
- return c.newMessage(c.cmd.Val())
+ return c.newMessage(ctx, cn, c.cmd.Val())
}
// Receive returns a message as a Subscription, Message, Pong or error.
@@ -734,7 +747,7 @@ func (c *channel) initMsgChan() {
}
case <-timer.C:
internal.Logger.Printf(
- ctx, "redis: %s channel is full for %s (message is dropped)",
+ ctx, "redis: %v channel is full for %s (message is dropped)",
c, c.chanSendTimeout)
}
default:
@@ -788,7 +801,7 @@ func (c *channel) initAllChan() {
}
case <-timer.C:
internal.Logger.Printf(
- ctx, "redis: %s channel is full for %s (message is dropped)",
+ ctx, "redis: %v channel is full for %s (message is dropped)",
c, c.chanSendTimeout)
}
default:
diff --git a/backend/vendor/github.com/redis/go-redis/v9/pubsub_commands.go b/backend/vendor/github.com/redis/go-redis/v9/pubsub_commands.go
index 28622aa6..ccc0ed52 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/pubsub_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/pubsub_commands.go
@@ -1,6 +1,10 @@
package redis
-import "context"
+import (
+ "context"
+
+ "github.com/redis/go-redis/v9/internal/otel"
+)
type PubSubCmdable interface {
Publish(ctx context.Context, channel string, message interface{}) *IntCmd
@@ -16,12 +20,20 @@ type PubSubCmdable interface {
func (c cmdable) Publish(ctx context.Context, channel string, message interface{}) *IntCmd {
cmd := NewIntCmd(ctx, "publish", channel, message)
_ = c(ctx, cmd)
+ // Record PubSub message sent (if command succeeded)
+ if cmd.Err() == nil {
+ otel.RecordPubSubMessage(ctx, nil, "sent", channel, false)
+ }
return cmd
}
func (c cmdable) SPublish(ctx context.Context, channel string, message interface{}) *IntCmd {
cmd := NewIntCmd(ctx, "spublish", channel, message)
_ = c(ctx, cmd)
+ // Record PubSub message sent (if command succeeded)
+ if cmd.Err() == nil {
+ otel.RecordPubSubMessage(ctx, nil, "sent", channel, true)
+ }
return cmd
}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/redis.go b/backend/vendor/github.com/redis/go-redis/v9/redis.go
index a6a71067..85622e43 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/redis.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/redis.go
@@ -13,6 +13,7 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/auth/streaming"
"github.com/redis/go-redis/v9/internal/hscan"
+ "github.com/redis/go-redis/v9/internal/otel"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/maintnotifications"
@@ -27,7 +28,11 @@ const Nil = proto.Nil
// SetLogger set custom log
// Use with VoidLogger to disable logging.
+// If logger is nil, the call is ignored and the existing logger is kept.
func SetLogger(logger internal.Logging) {
+ if logger == nil {
+ return
+ }
internal.Logger = logger
}
@@ -238,6 +243,7 @@ func (c *baseClient) clone() *baseClient {
clone := &baseClient{
opt: c.opt,
connPool: c.connPool,
+ pubSubPool: c.pubSubPool,
onClose: c.onClose,
pushProcessor: c.pushProcessor,
maintNotificationsManager: maintNotificationsManager,
@@ -298,6 +304,13 @@ func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) {
return nil, err
}
+ if dialStartNs := cn.GetDialStartNs(); dialStartNs > 0 {
+ if cb := pool.GetMetricConnectionCreateTimeCallback(); cb != nil {
+ duration := time.Duration(time.Now().UnixNano() - dialStartNs)
+ cb(ctx, duration, cn)
+ }
+ }
+
// initConn will transition to IDLE state, so we need to acquire it
// before returning it to the user.
if !cn.TryAcquire() {
@@ -537,7 +550,10 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
c.optLock.RLock()
maintNotifEnabled := c.opt.MaintNotificationsConfig != nil && c.opt.MaintNotificationsConfig.Mode != maintnotifications.ModeDisabled
protocol := c.opt.Protocol
- endpointType := c.opt.MaintNotificationsConfig.EndpointType
+ var endpointType maintnotifications.EndpointType
+ if maintNotifEnabled {
+ endpointType = c.opt.MaintNotificationsConfig.EndpointType
+ }
c.optLock.RUnlock()
var maintNotifHandshakeErr error
if maintNotifEnabled && protocol == 3 {
@@ -559,6 +575,12 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
// enabled mode, fail the connection
c.optLock.Unlock()
cn.GetStateMachine().Transition(pool.StateClosed)
+
+ // Record handshake failure metric
+ if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {
+ errorCallback(ctx, "HANDSHAKE_FAILED", cn, "HANDSHAKE_FAILED", true, 0)
+ }
+
return fmt.Errorf("failed to enable maintnotifications: %w", maintNotifHandshakeErr)
default: // will handle auto and any other
// Disabling logging here as it's too noisy.
@@ -662,20 +684,119 @@ func (c *baseClient) dial(ctx context.Context, network, addr string) (net.Conn,
}
func (c *baseClient) process(ctx context.Context, cmd Cmder) error {
+ // Start measuring total operation duration (includes all retries)
+ // Only call time.Now() if operation duration callback is set to avoid overhead
+ var operationStart time.Time
+ opDurationCallback := otel.GetOperationDurationCallback()
+ if opDurationCallback != nil {
+ operationStart = time.Now()
+ }
+ var lastConn *pool.Conn
+
var lastErr error
+ totalAttempts := 0
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
+ totalAttempts++
attempt := attempt
- retry, err := c._process(ctx, cmd, attempt)
+ retry, cn, err := c._process(ctx, cmd, attempt)
+ if cn != nil {
+ lastConn = cn
+ }
if err == nil || !retry {
+ // Record total operation duration
+ if opDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ opDurationCallback(ctx, operationDuration, cmd, totalAttempts, err, lastConn, c.opt.DB)
+ }
+
+ if err != nil {
+ if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {
+ errorType, statusCode, isInternal := classifyCommandError(err)
+ errorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1)
+ }
+ }
return err
}
lastErr = err
}
+
+ // Record failed operation after all retries
+ if opDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ opDurationCallback(ctx, operationDuration, cmd, totalAttempts, lastErr, lastConn, c.opt.DB)
+ }
+
+ // Record error metric for exhausted retries
+ if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {
+ errorType, statusCode, isInternal := classifyCommandError(lastErr)
+ errorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1)
+ }
+
return lastErr
}
+// classifyCommandError classifies an error for metrics reporting.
+// Returns: errorType, statusCode, isInternal
+// - errorType: A string describing the error type (e.g., "TIMEOUT", "NETWORK", "ERR")
+// - statusCode: The Redis error prefix or error category
+// - isInternal: true for network/timeout errors, false for Redis server errors
+func classifyCommandError(err error) (errorType, statusCode string, isInternal bool) {
+ if err == nil {
+ return "", "", false
+ }
+
+ errStr := err.Error()
+
+ // Check for timeout errors
+ if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+ return "TIMEOUT", "TIMEOUT", true
+ }
+
+ // Check for network errors
+ if _, ok := err.(net.Error); ok {
+ return "NETWORK", "NETWORK", true
+ }
+
+ // Check for context errors
+ if errors.Is(err, context.Canceled) {
+ return "CONTEXT_CANCELED", "CONTEXT_CANCELED", true
+ }
+ if errors.Is(err, context.DeadlineExceeded) {
+ return "CONTEXT_TIMEOUT", "CONTEXT_TIMEOUT", true
+ }
+
+ // Check for Redis errors
+ // Examples: "ERR ...", "WRONGTYPE ...", "CLUSTERDOWN ..."
+ if len(errStr) > 0 {
+ // Find the first space to extract the prefix
+ spaceIdx := 0
+ for i, c := range errStr {
+ if c == ' ' {
+ spaceIdx = i
+ break
+ }
+ }
+ if spaceIdx == 0 {
+ spaceIdx = len(errStr)
+ }
+ prefix := errStr[:spaceIdx]
+ isUppercase := true
+ for _, c := range prefix {
+ if c < 'A' || c > 'Z' {
+ isUppercase = false
+ break
+ }
+ }
+ if isUppercase && len(prefix) > 0 {
+ return prefix, prefix, false
+ }
+ }
+
+ return "UNKNOWN", "UNKNOWN", true
+}
+
func (c *baseClient) assertUnstableCommand(cmd Cmder) (bool, error) {
switch cmd.(type) {
case *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd:
@@ -689,15 +810,17 @@ func (c *baseClient) assertUnstableCommand(cmd Cmder) (bool, error) {
}
}
-func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) {
+func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, *pool.Conn, error) {
if attempt > 0 {
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
- return false, err
+ return false, nil, err
}
}
+ var usedConn *pool.Conn
retryTimeout := uint32(0)
if err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
+ usedConn = cn
// Process any pending push notifications before executing the command
if err := c.processPushNotifications(ctx, cn); err != nil {
internal.Logger.Printf(ctx, "push: error processing pending notifications before command: %v", err)
@@ -738,10 +861,10 @@ func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool
return nil
}); err != nil {
retry := shouldRetry(err, atomic.LoadUint32(&retryTimeout) == 1)
- return retry, err
+ return retry, usedConn, err
}
- return false, nil
+ return false, usedConn, nil
}
func (c *baseClient) retryBackoff(attempt int) time.Duration {
@@ -830,6 +953,10 @@ func (c *baseClient) Close() error {
firstErr = err
}
}
+
+ // Unregister pools from OTel before closing them
+ otel.UnregisterPools(c.connPool, c.pubSubPool)
+
if c.connPool != nil {
if err := c.connPool.Close(); err != nil && firstErr == nil {
firstErr = err
@@ -848,14 +975,14 @@ func (c *baseClient) getAddr() string {
}
func (c *baseClient) processPipeline(ctx context.Context, cmds []Cmder) error {
- if err := c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds); err != nil {
+ if err := c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds, "PIPELINE"); err != nil {
return err
}
return cmdsFirstErr(cmds)
}
func (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {
- if err := c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds); err != nil {
+ if err := c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds, "MULTI"); err != nil {
return err
}
return cmdsFirstErr(cmds)
@@ -864,13 +991,27 @@ func (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error
type pipelineProcessor func(context.Context, *pool.Conn, []Cmder) (bool, error)
func (c *baseClient) generalProcessPipeline(
- ctx context.Context, cmds []Cmder, p pipelineProcessor,
+ ctx context.Context, cmds []Cmder, p pipelineProcessor, operationName string,
) error {
+ // Only call time.Now() if pipeline operation duration callback is set to avoid overhead
+ var operationStart time.Time
+ pipelineOpDurationCallback := otel.GetPipelineOperationDurationCallback()
+ if pipelineOpDurationCallback != nil {
+ operationStart = time.Now()
+ }
+ var lastConn *pool.Conn
+ totalAttempts := 0
+
var lastErr error
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
+ totalAttempts++
if attempt > 0 {
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
setCmdsErr(cmds, err)
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, err, lastConn, c.opt.DB)
+ }
return err
}
}
@@ -878,6 +1019,7 @@ func (c *baseClient) generalProcessPipeline(
// Enable retries by default to retry dial errors returned by withConn.
canRetry := true
lastErr = c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
+ lastConn = cn
// Process any pending push notifications before executing the pipeline
if err := c.processPushNotifications(ctx, cn); err != nil {
internal.Logger.Printf(ctx, "push: error processing pending notifications before processing pipeline: %v", err)
@@ -891,9 +1033,31 @@ func (c *baseClient) generalProcessPipeline(
if !isRedisError(lastErr) {
setCmdsErr(cmds, lastErr)
}
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, lastErr, lastConn, c.opt.DB)
+ }
+
+ if lastErr != nil {
+ if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {
+ errorType, statusCode, isInternal := classifyCommandError(lastErr)
+ errorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1)
+ }
+ }
return lastErr
}
}
+
+ if pipelineOpDurationCallback != nil {
+ operationDuration := time.Since(operationStart)
+ pipelineOpDurationCallback(ctx, operationDuration, operationName, len(cmds), totalAttempts, lastErr, lastConn, c.opt.DB)
+ }
+
+ if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil {
+ errorType, statusCode, isInternal := classifyCommandError(lastErr)
+ errorCallback(ctx, errorType, lastConn, statusCode, isInternal, totalAttempts-1)
+ }
+
return lastErr
}
@@ -1055,13 +1219,18 @@ func NewClient(opt *Options) *Client {
// set opt push processor for child clients
c.opt.PushNotificationProcessor = c.pushProcessor
+ // Generate unique pool names for metrics
+ uniqueID := generateUniqueID()
+ mainPoolName := opt.Addr + "_" + uniqueID
+ pubsubPoolName := opt.Addr + "_" + uniqueID + "_pubsub"
+
// Create connection pools
var err error
- c.connPool, err = newConnPool(opt, c.dialHook)
+ c.connPool, err = newConnPool(opt, c.dialHook, mainPoolName)
if err != nil {
panic(fmt.Errorf("redis: failed to create connection pool: %w", err))
}
- c.pubSubPool, err = newPubSubPool(opt, c.dialHook)
+ c.pubSubPool, err = newPubSubPool(opt, c.dialHook, pubsubPoolName)
if err != nil {
panic(fmt.Errorf("redis: failed to create pubsub pool: %w", err))
}
@@ -1092,6 +1261,10 @@ func NewClient(opt *Options) *Client {
}
}
+ // Register pools with OTel recorder if it supports pool registration
+ // This allows async gauge metrics to pull stats from pools periodically
+ otel.RegisterPools(c.connPool, c.pubSubPool, opt.Addr)
+
return &c
}
@@ -1127,6 +1300,16 @@ func (c *Client) Options() *Options {
return c.opt
}
+// NodeAddress returns the address of the Redis node as reported by the server.
+// For cluster clients, this is the endpoint from CLUSTER SLOTS before any transformation
+// (e.g., loopback replacement). For standalone clients, this defaults to Addr.
+//
+// This is useful for matching the source field in maintenance notifications
+// (e.g. SMIGRATED).
+func (c *Client) NodeAddress() string {
+ return c.opt.NodeAddress
+}
+
// GetMaintNotificationsManager returns the maintnotifications manager instance for monitoring and control.
// Returns nil if maintnotifications are not enabled.
func (c *Client) GetMaintNotificationsManager() *maintnotifications.Manager {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/ring.go b/backend/vendor/github.com/redis/go-redis/v9/ring.go
index 3381460a..d9220ddb 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/ring.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/ring.go
@@ -108,7 +108,18 @@ type RingOptions struct {
MinRetryBackoff time.Duration
MaxRetryBackoff time.Duration
- DialTimeout time.Duration
+ DialTimeout time.Duration
+
+ // DialerRetries is the maximum number of retry attempts when dialing fails.
+ //
+ // default: 5
+ DialerRetries int
+
+ // DialerRetryTimeout is the backoff duration between retry attempts.
+ //
+ // default: 100 milliseconds
+ DialerRetryTimeout time.Duration
+
ReadTimeout time.Duration
WriteTimeout time.Duration
ContextTimeoutEnabled bool
@@ -116,13 +127,14 @@ type RingOptions struct {
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
PoolFIFO bool
- PoolSize int
- PoolTimeout time.Duration
- MinIdleConns int
- MaxIdleConns int
- MaxActiveConns int
- ConnMaxIdleTime time.Duration
- ConnMaxLifetime time.Duration
+ PoolSize int
+ PoolTimeout time.Duration
+ MinIdleConns int
+ MaxIdleConns int
+ MaxActiveConns int
+ ConnMaxIdleTime time.Duration
+ ConnMaxLifetime time.Duration
+ ConnMaxLifetimeJitter time.Duration
// ReadBufferSize is the size of the bufio.Reader buffer for each connection.
// Larger buffers can improve performance for commands that return large responses.
@@ -219,20 +231,23 @@ func (opt *RingOptions) clientOptions() *Options {
MaxRetries: -1,
DialTimeout: opt.DialTimeout,
+ DialerRetries: opt.DialerRetries,
+ DialerRetryTimeout: opt.DialerRetryTimeout,
ReadTimeout: opt.ReadTimeout,
WriteTimeout: opt.WriteTimeout,
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
- PoolFIFO: opt.PoolFIFO,
- PoolSize: opt.PoolSize,
- PoolTimeout: opt.PoolTimeout,
- MinIdleConns: opt.MinIdleConns,
- MaxIdleConns: opt.MaxIdleConns,
- MaxActiveConns: opt.MaxActiveConns,
- ConnMaxIdleTime: opt.ConnMaxIdleTime,
- ConnMaxLifetime: opt.ConnMaxLifetime,
- ReadBufferSize: opt.ReadBufferSize,
- WriteBufferSize: opt.WriteBufferSize,
+ PoolFIFO: opt.PoolFIFO,
+ PoolSize: opt.PoolSize,
+ PoolTimeout: opt.PoolTimeout,
+ MinIdleConns: opt.MinIdleConns,
+ MaxIdleConns: opt.MaxIdleConns,
+ MaxActiveConns: opt.MaxActiveConns,
+ ConnMaxIdleTime: opt.ConnMaxIdleTime,
+ ConnMaxLifetime: opt.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,
+ ReadBufferSize: opt.ReadBufferSize,
+ WriteBufferSize: opt.WriteBufferSize,
TLSConfig: opt.TLSConfig,
Limiter: opt.Limiter,
diff --git a/backend/vendor/github.com/redis/go-redis/v9/search_commands.go b/backend/vendor/github.com/redis/go-redis/v9/search_commands.go
index 9018b3de..0fef8ffc 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/search_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/search_commands.go
@@ -372,12 +372,18 @@ const (
// FTHybridVectorExpression represents a vector expression in hybrid search
type FTHybridVectorExpression struct {
- VectorField string
- VectorData Vector
- Method FTHybridVectorMethod
- MethodParams []interface{}
- Filter string
- YieldScoreAs string
+ VectorField string
+ VectorData Vector
+ // VectorParamName specifies the parameter name for passing vector data via PARAMS mechanism.
+ // REQUIRED for Redis 8.6+ (inline vector blobs are not supported in 8.6+).
+ // Optional for Redis 8.4-8.5 (both inline and PARAMS are supported).
+ // When set, the vector blob will be passed as: VSIM @field $VectorParamName PARAMS ... VectorParamName
+ // When empty, the vector blob will be inlined: VSIM @field (fails on Redis 8.6+)
+ VectorParamName string
+ Method FTHybridVectorMethod
+ MethodParams []interface{}
+ Filter string
+ YieldScoreAs string
}
// FTHybridCombineOptions represents options for result fusion
@@ -768,8 +774,9 @@ func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) {
func NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd {
return &AggregateCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeAggregate,
},
}
}
@@ -810,6 +817,31 @@ func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *AggregateCmd) Clone() Cmder {
+ var val *FTAggregateResult
+ if cmd.val != nil {
+ val = &FTAggregateResult{
+ Total: cmd.val.Total,
+ }
+ if cmd.val.Rows != nil {
+ val.Rows = make([]AggregateRow, len(cmd.val.Rows))
+ for i, row := range cmd.val.Rows {
+ val.Rows[i] = AggregateRow{}
+ if row.Fields != nil {
+ val.Rows[i].Fields = make(map[string]interface{}, len(row.Fields))
+ for k, v := range row.Fields {
+ val.Rows[i].Fields[k] = v
+ }
+ }
+ }
+ }
+ }
+ return &AggregateCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// FTAggregateWithArgs - Performs a search query on an index and applies a series of aggregate transformations to the result.
// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
// This function also allows for specifying additional options such as: Verbatim, LoadAll, Load, Timeout, GroupBy, SortBy, SortByMax, Apply, LimitOffset, Limit, Filter, WithCursor, Params, and DialectVersion.
@@ -1597,8 +1629,9 @@ type FTInfoCmd struct {
func newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd {
return &FTInfoCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeFTInfo,
},
}
}
@@ -1660,6 +1693,68 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *FTInfoCmd) Clone() Cmder {
+ val := FTInfoResult{
+ IndexErrors: cmd.val.IndexErrors,
+ BytesPerRecordAvg: cmd.val.BytesPerRecordAvg,
+ Cleaning: cmd.val.Cleaning,
+ CursorStats: cmd.val.CursorStats,
+ DocTableSizeMB: cmd.val.DocTableSizeMB,
+ GCStats: cmd.val.GCStats,
+ GeoshapesSzMB: cmd.val.GeoshapesSzMB,
+ HashIndexingFailures: cmd.val.HashIndexingFailures,
+ IndexDefinition: cmd.val.IndexDefinition,
+ IndexName: cmd.val.IndexName,
+ Indexing: cmd.val.Indexing,
+ InvertedSzMB: cmd.val.InvertedSzMB,
+ KeyTableSizeMB: cmd.val.KeyTableSizeMB,
+ MaxDocID: cmd.val.MaxDocID,
+ NumDocs: cmd.val.NumDocs,
+ NumRecords: cmd.val.NumRecords,
+ NumTerms: cmd.val.NumTerms,
+ NumberOfUses: cmd.val.NumberOfUses,
+ OffsetBitsPerRecordAvg: cmd.val.OffsetBitsPerRecordAvg,
+ OffsetVectorsSzMB: cmd.val.OffsetVectorsSzMB,
+ OffsetsPerTermAvg: cmd.val.OffsetsPerTermAvg,
+ PercentIndexed: cmd.val.PercentIndexed,
+ RecordsPerDocAvg: cmd.val.RecordsPerDocAvg,
+ SortableValuesSizeMB: cmd.val.SortableValuesSizeMB,
+ TagOverheadSzMB: cmd.val.TagOverheadSzMB,
+ TextOverheadSzMB: cmd.val.TextOverheadSzMB,
+ TotalIndexMemorySzMB: cmd.val.TotalIndexMemorySzMB,
+ TotalIndexingTime: cmd.val.TotalIndexingTime,
+ TotalInvertedIndexBlocks: cmd.val.TotalInvertedIndexBlocks,
+ VectorIndexSzMB: cmd.val.VectorIndexSzMB,
+ }
+ // Clone slices and maps
+ if cmd.val.Attributes != nil {
+ val.Attributes = make([]FTAttribute, len(cmd.val.Attributes))
+ copy(val.Attributes, cmd.val.Attributes)
+ }
+ if cmd.val.DialectStats != nil {
+ val.DialectStats = make(map[string]int, len(cmd.val.DialectStats))
+ for k, v := range cmd.val.DialectStats {
+ val.DialectStats[k] = v
+ }
+ }
+ if cmd.val.FieldStatistics != nil {
+ val.FieldStatistics = make([]FieldStatistic, len(cmd.val.FieldStatistics))
+ copy(val.FieldStatistics, cmd.val.FieldStatistics)
+ }
+ if cmd.val.IndexOptions != nil {
+ val.IndexOptions = make([]string, len(cmd.val.IndexOptions))
+ copy(val.IndexOptions, cmd.val.IndexOptions)
+ }
+ if cmd.val.IndexDefinition.Prefixes != nil {
+ val.IndexDefinition.Prefixes = make([]string, len(cmd.val.IndexDefinition.Prefixes))
+ copy(val.IndexDefinition.Prefixes, cmd.val.IndexDefinition.Prefixes)
+ }
+ return &FTInfoCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// FTInfo - Retrieves information about an index.
// The 'index' parameter specifies the index to retrieve information about.
// For more information, please refer to the Redis documentation:
@@ -1716,8 +1811,9 @@ type FTSpellCheckCmd struct {
func newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckCmd {
return &FTSpellCheckCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeFTSpellCheck,
},
}
}
@@ -1813,6 +1909,26 @@ func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) {
return results, nil
}
+func (cmd *FTSpellCheckCmd) Clone() Cmder {
+ var val []SpellCheckResult
+ if cmd.val != nil {
+ val = make([]SpellCheckResult, len(cmd.val))
+ for i, result := range cmd.val {
+ val[i] = SpellCheckResult{
+ Term: result.Term,
+ }
+ if result.Suggestions != nil {
+ val[i].Suggestions = make([]SpellCheckSuggestion, len(result.Suggestions))
+ copy(val[i].Suggestions, result.Suggestions)
+ }
+ }
+ }
+ return &FTSpellCheckCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, withSortKeys bool) (FTSearchResult, error) {
if len(data) < 1 {
return FTSearchResult{}, fmt.Errorf("unexpected search result format")
@@ -1909,8 +2025,9 @@ type FTSearchCmd struct {
func newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...interface{}) *FTSearchCmd {
return &FTSearchCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeFTSearch,
},
options: options,
}
@@ -1952,6 +2069,89 @@ func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *FTSearchCmd) Clone() Cmder {
+ val := FTSearchResult{
+ Total: cmd.val.Total,
+ }
+ if cmd.val.Docs != nil {
+ val.Docs = make([]Document, len(cmd.val.Docs))
+ for i, doc := range cmd.val.Docs {
+ val.Docs[i] = Document{
+ ID: doc.ID,
+ Score: doc.Score,
+ Payload: doc.Payload,
+ SortKey: doc.SortKey,
+ }
+ if doc.Fields != nil {
+ val.Docs[i].Fields = make(map[string]string, len(doc.Fields))
+ for k, v := range doc.Fields {
+ val.Docs[i].Fields[k] = v
+ }
+ }
+ }
+ }
+ var options *FTSearchOptions
+ if cmd.options != nil {
+ options = &FTSearchOptions{
+ NoContent: cmd.options.NoContent,
+ Verbatim: cmd.options.Verbatim,
+ NoStopWords: cmd.options.NoStopWords,
+ WithScores: cmd.options.WithScores,
+ WithPayloads: cmd.options.WithPayloads,
+ WithSortKeys: cmd.options.WithSortKeys,
+ Slop: cmd.options.Slop,
+ Timeout: cmd.options.Timeout,
+ InOrder: cmd.options.InOrder,
+ Language: cmd.options.Language,
+ Expander: cmd.options.Expander,
+ Scorer: cmd.options.Scorer,
+ ExplainScore: cmd.options.ExplainScore,
+ Payload: cmd.options.Payload,
+ SortByWithCount: cmd.options.SortByWithCount,
+ LimitOffset: cmd.options.LimitOffset,
+ Limit: cmd.options.Limit,
+ CountOnly: cmd.options.CountOnly,
+ DialectVersion: cmd.options.DialectVersion,
+ }
+ // Clone slices and maps
+ if cmd.options.Filters != nil {
+ options.Filters = make([]FTSearchFilter, len(cmd.options.Filters))
+ copy(options.Filters, cmd.options.Filters)
+ }
+ if cmd.options.GeoFilter != nil {
+ options.GeoFilter = make([]FTSearchGeoFilter, len(cmd.options.GeoFilter))
+ copy(options.GeoFilter, cmd.options.GeoFilter)
+ }
+ if cmd.options.InKeys != nil {
+ options.InKeys = make([]interface{}, len(cmd.options.InKeys))
+ copy(options.InKeys, cmd.options.InKeys)
+ }
+ if cmd.options.InFields != nil {
+ options.InFields = make([]interface{}, len(cmd.options.InFields))
+ copy(options.InFields, cmd.options.InFields)
+ }
+ if cmd.options.Return != nil {
+ options.Return = make([]FTSearchReturn, len(cmd.options.Return))
+ copy(options.Return, cmd.options.Return)
+ }
+ if cmd.options.SortBy != nil {
+ options.SortBy = make([]FTSearchSortBy, len(cmd.options.SortBy))
+ copy(options.SortBy, cmd.options.SortBy)
+ }
+ if cmd.options.Params != nil {
+ options.Params = make(map[string]interface{}, len(cmd.options.Params))
+ for k, v := range cmd.options.Params {
+ options.Params[k] = v
+ }
+ }
+ }
+ return &FTSearchCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ options: options,
+ }
+}
+
// FTHybridResult represents the result of a hybrid search operation
type FTHybridResult struct {
TotalResults int
@@ -2153,6 +2353,111 @@ func (cmd *FTHybridCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *FTHybridCmd) Clone() Cmder {
+ val := FTHybridResult{
+ TotalResults: cmd.val.TotalResults,
+ ExecutionTime: cmd.val.ExecutionTime,
+ }
+ if cmd.val.Results != nil {
+ val.Results = make([]map[string]interface{}, len(cmd.val.Results))
+ for i, result := range cmd.val.Results {
+ val.Results[i] = make(map[string]interface{}, len(result))
+ for k, v := range result {
+ val.Results[i][k] = v
+ }
+ }
+ }
+ if cmd.val.Warnings != nil {
+ val.Warnings = make([]string, len(cmd.val.Warnings))
+ copy(val.Warnings, cmd.val.Warnings)
+ }
+
+ var cursorVal *FTHybridCursorResult
+ if cmd.cursorVal != nil {
+ cursorVal = &FTHybridCursorResult{
+ SearchCursorID: cmd.cursorVal.SearchCursorID,
+ VsimCursorID: cmd.cursorVal.VsimCursorID,
+ }
+ }
+
+ var options *FTHybridOptions
+ if cmd.options != nil {
+ options = &FTHybridOptions{
+ CountExpressions: cmd.options.CountExpressions,
+ Load: cmd.options.Load,
+ Filter: cmd.options.Filter,
+ LimitOffset: cmd.options.LimitOffset,
+ Limit: cmd.options.Limit,
+ ExplainScore: cmd.options.ExplainScore,
+ Timeout: cmd.options.Timeout,
+ WithCursor: cmd.options.WithCursor,
+ }
+ // Clone slices and maps
+ if cmd.options.SearchExpressions != nil {
+ options.SearchExpressions = make([]FTHybridSearchExpression, len(cmd.options.SearchExpressions))
+ copy(options.SearchExpressions, cmd.options.SearchExpressions)
+ }
+ if cmd.options.VectorExpressions != nil {
+ options.VectorExpressions = make([]FTHybridVectorExpression, len(cmd.options.VectorExpressions))
+ copy(options.VectorExpressions, cmd.options.VectorExpressions)
+ }
+ if cmd.options.Combine != nil {
+ options.Combine = &FTHybridCombineOptions{
+ Method: cmd.options.Combine.Method,
+ Count: cmd.options.Combine.Count,
+ Window: cmd.options.Combine.Window,
+ Constant: cmd.options.Combine.Constant,
+ Alpha: cmd.options.Combine.Alpha,
+ Beta: cmd.options.Combine.Beta,
+ YieldScoreAs: cmd.options.Combine.YieldScoreAs,
+ }
+ }
+ if cmd.options.GroupBy != nil {
+ options.GroupBy = &FTHybridGroupBy{
+ Count: cmd.options.GroupBy.Count,
+ ReduceFunc: cmd.options.GroupBy.ReduceFunc,
+ ReduceCount: cmd.options.GroupBy.ReduceCount,
+ }
+ if cmd.options.GroupBy.Fields != nil {
+ options.GroupBy.Fields = make([]string, len(cmd.options.GroupBy.Fields))
+ copy(options.GroupBy.Fields, cmd.options.GroupBy.Fields)
+ }
+ if cmd.options.GroupBy.ReduceParams != nil {
+ options.GroupBy.ReduceParams = make([]interface{}, len(cmd.options.GroupBy.ReduceParams))
+ copy(options.GroupBy.ReduceParams, cmd.options.GroupBy.ReduceParams)
+ }
+ }
+ if cmd.options.Apply != nil {
+ options.Apply = make([]FTHybridApply, len(cmd.options.Apply))
+ copy(options.Apply, cmd.options.Apply)
+ }
+ if cmd.options.SortBy != nil {
+ options.SortBy = make([]FTSearchSortBy, len(cmd.options.SortBy))
+ copy(options.SortBy, cmd.options.SortBy)
+ }
+ if cmd.options.Params != nil {
+ options.Params = make(map[string]interface{}, len(cmd.options.Params))
+ for k, v := range cmd.options.Params {
+ options.Params[k] = v
+ }
+ }
+ if cmd.options.WithCursorOptions != nil {
+ options.WithCursorOptions = &FTHybridWithCursor{
+ MaxIdle: cmd.options.WithCursorOptions.MaxIdle,
+ Count: cmd.options.WithCursorOptions.Count,
+ }
+ }
+ }
+
+ return &FTHybridCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ cursorVal: cursorVal,
+ options: options,
+ withCursor: cmd.withCursor,
+ }
+}
+
// FTSearch - Executes a search query on an index.
// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
// For more information, please refer to the Redis documentation about [FT.SEARCH].
@@ -2412,8 +2717,9 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin
func NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd {
return &FTSynDumpCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeFTSynDump,
},
}
}
@@ -2479,6 +2785,26 @@ func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error {
return nil
}
+func (cmd *FTSynDumpCmd) Clone() Cmder {
+ var val []FTSynDumpResult
+ if cmd.val != nil {
+ val = make([]FTSynDumpResult, len(cmd.val))
+ for i, result := range cmd.val {
+ val[i] = FTSynDumpResult{
+ Term: result.Term,
+ }
+ if result.Synonyms != nil {
+ val[i].Synonyms = make([]string, len(result.Synonyms))
+ copy(val[i].Synonyms, result.Synonyms)
+ }
+ }
+ }
+ return &FTSynDumpCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// FTSynDump - Dumps the contents of a synonym group.
// The 'index' parameter specifies the index to dump.
// For more information, please refer to the Redis documentation:
@@ -2572,12 +2898,27 @@ func (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FT
// For FT.HYBRID, we need to send just the raw vector bytes, not the Value() format
// Value() returns [format, data] but FT.HYBRID expects just the blob
vectorValue := vectorExpr.VectorData.Value()
+ var vectorBlob interface{}
if len(vectorValue) >= 2 {
// vectorValue is [format, data, ...] - we only want the data part
- args = append(args, vectorValue[1])
+ vectorBlob = vectorValue[1]
} else {
// Fallback for unexpected format
- args = append(args, vectorValue...)
+ vectorBlob = vectorValue
+ }
+
+ // If VectorParamName is provided, use PARAMS mechanism (required for Redis 8.6+)
+ // If not provided, inline the vector blob (works on Redis 8.4/8.5, fails on 8.6+)
+ if vectorExpr.VectorParamName != "" {
+ // Use PARAMS mechanism
+ args = append(args, "$"+vectorExpr.VectorParamName)
+ if options.Params == nil {
+ options.Params = make(map[string]interface{})
+ }
+ options.Params[vectorExpr.VectorParamName] = vectorBlob
+ } else {
+ // Inline the vector blob (deprecated in Redis 8.6+)
+ args = append(args, vectorBlob)
}
if vectorExpr.Method != "" {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/sentinel.go b/backend/vendor/github.com/redis/go-redis/v9/sentinel.go
index 663f7b1a..24646c14 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/sentinel.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/sentinel.go
@@ -16,7 +16,6 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/rand"
- "github.com/redis/go-redis/v9/internal/util"
"github.com/redis/go-redis/v9/maintnotifications"
"github.com/redis/go-redis/v9/push"
)
@@ -89,7 +88,18 @@ type FailoverOptions struct {
MinRetryBackoff time.Duration
MaxRetryBackoff time.Duration
- DialTimeout time.Duration
+ DialTimeout time.Duration
+
+ // DialerRetries is the maximum number of retry attempts when dialing fails.
+ //
+ // default: 5
+ DialerRetries int
+
+ // DialerRetryTimeout is the backoff duration between retry attempts.
+ //
+ // default: 100 milliseconds
+ DialerRetryTimeout time.Duration
+
ReadTimeout time.Duration
WriteTimeout time.Duration
ContextTimeoutEnabled bool
@@ -110,13 +120,19 @@ type FailoverOptions struct {
PoolFIFO bool
- PoolSize int
- PoolTimeout time.Duration
- MinIdleConns int
- MaxIdleConns int
- MaxActiveConns int
- ConnMaxIdleTime time.Duration
- ConnMaxLifetime time.Duration
+ PoolSize int
+
+ // MaxConcurrentDials is the maximum number of concurrent connection creation goroutines.
+ // If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize.
+ MaxConcurrentDials int
+
+ PoolTimeout time.Duration
+ MinIdleConns int
+ MaxIdleConns int
+ MaxActiveConns int
+ ConnMaxIdleTime time.Duration
+ ConnMaxLifetime time.Duration
+ ConnMaxLifetimeJitter time.Duration
TLSConfig *tls.Config
@@ -141,6 +157,10 @@ type FailoverOptions struct {
UnstableResp3 bool
+ // PushNotificationProcessor is the processor for handling push notifications.
+ // If nil, a default processor will be created for RESP3 connections.
+ PushNotificationProcessor push.NotificationProcessor
+
// MaintNotificationsConfig is not supported for FailoverClients at the moment
// MaintNotificationsConfig provides custom configuration for maintnotifications upgrades.
// When MaintNotificationsConfig.Mode is not "disabled", the client will handle
@@ -174,27 +194,33 @@ func (opt *FailoverOptions) clientOptions() *Options {
ReadBufferSize: opt.ReadBufferSize,
WriteBufferSize: opt.WriteBufferSize,
- DialTimeout: opt.DialTimeout,
- ReadTimeout: opt.ReadTimeout,
- WriteTimeout: opt.WriteTimeout,
+ DialTimeout: opt.DialTimeout,
+ DialerRetries: opt.DialerRetries,
+ DialerRetryTimeout: opt.DialerRetryTimeout,
+ ReadTimeout: opt.ReadTimeout,
+ WriteTimeout: opt.WriteTimeout,
+
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
- PoolFIFO: opt.PoolFIFO,
- PoolSize: opt.PoolSize,
- PoolTimeout: opt.PoolTimeout,
- MinIdleConns: opt.MinIdleConns,
- MaxIdleConns: opt.MaxIdleConns,
- MaxActiveConns: opt.MaxActiveConns,
- ConnMaxIdleTime: opt.ConnMaxIdleTime,
- ConnMaxLifetime: opt.ConnMaxLifetime,
+ PoolFIFO: opt.PoolFIFO,
+ PoolSize: opt.PoolSize,
+ MaxConcurrentDials: opt.MaxConcurrentDials,
+ PoolTimeout: opt.PoolTimeout,
+ MinIdleConns: opt.MinIdleConns,
+ MaxIdleConns: opt.MaxIdleConns,
+ MaxActiveConns: opt.MaxActiveConns,
+ ConnMaxIdleTime: opt.ConnMaxIdleTime,
+ ConnMaxLifetime: opt.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,
TLSConfig: opt.TLSConfig,
DisableIdentity: opt.DisableIdentity,
DisableIndentity: opt.DisableIndentity,
- IdentitySuffix: opt.IdentitySuffix,
- UnstableResp3: opt.UnstableResp3,
+ IdentitySuffix: opt.IdentitySuffix,
+ UnstableResp3: opt.UnstableResp3,
+ PushNotificationProcessor: opt.PushNotificationProcessor,
MaintNotificationsConfig: &maintnotifications.Config{
Mode: maintnotifications.ModeDisabled,
@@ -222,27 +248,33 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
ReadBufferSize: 4096,
WriteBufferSize: 4096,
- DialTimeout: opt.DialTimeout,
- ReadTimeout: opt.ReadTimeout,
- WriteTimeout: opt.WriteTimeout,
+ DialTimeout: opt.DialTimeout,
+ DialerRetries: opt.DialerRetries,
+ DialerRetryTimeout: opt.DialerRetryTimeout,
+ ReadTimeout: opt.ReadTimeout,
+ WriteTimeout: opt.WriteTimeout,
+
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
- PoolFIFO: opt.PoolFIFO,
- PoolSize: opt.PoolSize,
- PoolTimeout: opt.PoolTimeout,
- MinIdleConns: opt.MinIdleConns,
- MaxIdleConns: opt.MaxIdleConns,
- MaxActiveConns: opt.MaxActiveConns,
- ConnMaxIdleTime: opt.ConnMaxIdleTime,
- ConnMaxLifetime: opt.ConnMaxLifetime,
+ PoolFIFO: opt.PoolFIFO,
+ PoolSize: opt.PoolSize,
+ MaxConcurrentDials: opt.MaxConcurrentDials,
+ PoolTimeout: opt.PoolTimeout,
+ MinIdleConns: opt.MinIdleConns,
+ MaxIdleConns: opt.MaxIdleConns,
+ MaxActiveConns: opt.MaxActiveConns,
+ ConnMaxIdleTime: opt.ConnMaxIdleTime,
+ ConnMaxLifetime: opt.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: opt.ConnMaxLifetimeJitter,
TLSConfig: opt.TLSConfig,
DisableIdentity: opt.DisableIdentity,
DisableIndentity: opt.DisableIndentity,
- IdentitySuffix: opt.IdentitySuffix,
- UnstableResp3: opt.UnstableResp3,
+ IdentitySuffix: opt.IdentitySuffix,
+ UnstableResp3: opt.UnstableResp3,
+ PushNotificationProcessor: opt.PushNotificationProcessor,
MaintNotificationsConfig: &maintnotifications.Config{
Mode: maintnotifications.ModeDisabled,
@@ -276,26 +308,31 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
ReadBufferSize: opt.ReadBufferSize,
WriteBufferSize: opt.WriteBufferSize,
- DialTimeout: opt.DialTimeout,
- ReadTimeout: opt.ReadTimeout,
- WriteTimeout: opt.WriteTimeout,
+ DialTimeout: opt.DialTimeout,
+ DialerRetries: opt.DialerRetries,
+ DialerRetryTimeout: opt.DialerRetryTimeout,
+ ReadTimeout: opt.ReadTimeout,
+ WriteTimeout: opt.WriteTimeout,
+
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
- PoolFIFO: opt.PoolFIFO,
- PoolSize: opt.PoolSize,
- PoolTimeout: opt.PoolTimeout,
- MinIdleConns: opt.MinIdleConns,
- MaxIdleConns: opt.MaxIdleConns,
- MaxActiveConns: opt.MaxActiveConns,
- ConnMaxIdleTime: opt.ConnMaxIdleTime,
- ConnMaxLifetime: opt.ConnMaxLifetime,
+ PoolFIFO: opt.PoolFIFO,
+ PoolSize: opt.PoolSize,
+ MaxConcurrentDials: opt.MaxConcurrentDials,
+ PoolTimeout: opt.PoolTimeout,
+ MinIdleConns: opt.MinIdleConns,
+ MaxIdleConns: opt.MaxIdleConns,
+ MaxActiveConns: opt.MaxActiveConns,
+ ConnMaxIdleTime: opt.ConnMaxIdleTime,
+ ConnMaxLifetime: opt.ConnMaxLifetime,
TLSConfig: opt.TLSConfig,
- DisableIdentity: opt.DisableIdentity,
- DisableIndentity: opt.DisableIndentity,
- IdentitySuffix: opt.IdentitySuffix,
- FailingTimeoutSeconds: opt.FailingTimeoutSeconds,
+ DisableIdentity: opt.DisableIdentity,
+ DisableIndentity: opt.DisableIndentity,
+ IdentitySuffix: opt.IdentitySuffix,
+ FailingTimeoutSeconds: opt.FailingTimeoutSeconds,
+ PushNotificationProcessor: opt.PushNotificationProcessor,
MaintNotificationsConfig: &maintnotifications.Config{
Mode: maintnotifications.ModeDisabled,
@@ -399,15 +436,21 @@ func setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions,
o.MinRetryBackoff = q.duration("min_retry_backoff")
o.MaxRetryBackoff = q.duration("max_retry_backoff")
o.DialTimeout = q.duration("dial_timeout")
+ o.DialerRetries = q.int("dialer_retries")
+ o.DialerRetryTimeout = q.duration("dialer_retry_timeout")
o.ReadTimeout = q.duration("read_timeout")
o.WriteTimeout = q.duration("write_timeout")
o.ContextTimeoutEnabled = q.bool("context_timeout_enabled")
o.PoolFIFO = q.bool("pool_fifo")
o.PoolSize = q.int("pool_size")
+ o.MaxConcurrentDials = q.int("max_concurrent_dials")
o.MinIdleConns = q.int("min_idle_conns")
o.MaxIdleConns = q.int("max_idle_conns")
o.MaxActiveConns = q.int("max_active_conns")
o.ConnMaxLifetime = q.duration("conn_max_lifetime")
+ if q.has("conn_max_lifetime_jitter") {
+ o.ConnMaxLifetimeJitter = min(q.duration("conn_max_lifetime_jitter"), o.ConnMaxLifetime)
+ }
o.ConnMaxIdleTime = q.duration("conn_max_idle_time")
o.PoolTimeout = q.duration("pool_timeout")
o.DisableIdentity = q.bool("disableIdentity")
@@ -490,12 +533,17 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
// Use void processor by default for RESP2 connections
rdb.pushProcessor = initializePushProcessor(opt)
+ // Generate unique pool names for metrics
+ uniqueID := generateUniqueID()
+ mainPoolName := opt.Addr + "_" + uniqueID
+ pubsubPoolName := opt.Addr + "_" + uniqueID + "_pubsub"
+
var err error
- rdb.connPool, err = newConnPool(opt, rdb.dialHook)
+ rdb.connPool, err = newConnPool(opt, rdb.dialHook, mainPoolName)
if err != nil {
panic(fmt.Errorf("redis: failed to create connection pool: %w", err))
}
- rdb.pubSubPool, err = newPubSubPool(opt, rdb.dialHook)
+ rdb.pubSubPool, err = newPubSubPool(opt, rdb.dialHook, pubsubPoolName)
if err != nil {
panic(fmt.Errorf("redis: failed to create pubsub pool: %w", err))
}
@@ -574,12 +622,18 @@ func NewSentinelClient(opt *Options) *SentinelClient {
dial: c.baseClient.dial,
process: c.baseClient.process,
})
+
+ // Generate unique pool names for metrics
+ uniqueID := generateUniqueID()
+ mainPoolName := opt.Addr + "_" + uniqueID
+ pubsubPoolName := opt.Addr + "_" + uniqueID + "_pubsub"
+
var err error
- c.connPool, err = newConnPool(opt, c.dialHook)
+ c.connPool, err = newConnPool(opt, c.dialHook, mainPoolName)
if err != nil {
panic(fmt.Errorf("redis: failed to create connection pool: %w", err))
}
- c.pubSubPool, err = newPubSubPool(opt, c.dialHook)
+ c.pubSubPool, err = newPubSubPool(opt, c.dialHook, pubsubPoolName)
if err != nil {
panic(fmt.Errorf("redis: failed to create pubsub pool: %w", err))
}
@@ -827,7 +881,7 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
if sentinel != nil {
addr, err := c.getMasterAddr(ctx, sentinel)
if err != nil {
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+ if isContextError(ctx.Err()) {
return "", err
}
// Continue on other errors
@@ -845,7 +899,7 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
addr, err := c.getMasterAddr(ctx, c.sentinel)
if err != nil {
_ = c.closeSentinel()
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+ if isContextError(ctx.Err()) {
return "", err
}
// Continue on other errors
@@ -904,22 +958,7 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
for err := range errCh {
errs = append(errs, err)
}
- return "", fmt.Errorf("redis: all sentinels specified in configuration are unreachable: %s", joinErrors(errs))
-}
-
-func joinErrors(errs []error) string {
- if len(errs) == 0 {
- return ""
- }
- if len(errs) == 1 {
- return errs[0].Error()
- }
- b := []byte(errs[0].Error())
- for _, err := range errs[1:] {
- b = append(b, '\n')
- b = append(b, err.Error()...)
- }
- return util.BytesToString(b)
+ return "", fmt.Errorf("redis: all sentinels specified in configuration are unreachable: %w", errors.Join(errs...))
}
func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) {
@@ -930,7 +969,7 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo
if sentinel != nil {
addrs, err := c.getReplicaAddrs(ctx, sentinel)
if err != nil {
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+ if isContextError(ctx.Err()) {
return nil, err
}
// Continue on other errors
@@ -948,7 +987,7 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo
addrs, err := c.getReplicaAddrs(ctx, c.sentinel)
if err != nil {
_ = c.closeSentinel()
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+ if isContextError(ctx.Err()) {
return nil, err
}
// Continue on other errors
@@ -970,7 +1009,7 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo
replicas, err := sentinel.Replicas(ctx, c.opt.MasterName).Result()
if err != nil {
_ = sentinel.Close()
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+ if isContextError(ctx.Err()) {
return nil, err
}
internal.Logger.Printf(ctx, "sentinel: Replicas master=%q failed: %s",
diff --git a/backend/vendor/github.com/redis/go-redis/v9/set_commands.go b/backend/vendor/github.com/redis/go-redis/v9/set_commands.go
index 79efa6e4..2a465728 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/set_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/set_commands.go
@@ -6,6 +6,8 @@ import (
"github.com/redis/go-redis/v9/internal/hashtag"
)
+// SetCmdable is an interface for Redis set commands.
+// Sets are unordered collections of unique strings.
type SetCmdable interface {
SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd
SCard(ctx context.Context, key string) *IntCmd
@@ -29,8 +31,12 @@ type SetCmdable interface {
SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd
}
-//------------------------------------------------------------------------------
-
+// Returns the number of elements that were added to the set, not including all
+// the elements already present in the set.
+//
+// For more information about the command please refer to [SADD].
+//
+// [SADD]: (https://redis.io/docs/latest/commands/sadd/)
func (c cmdable) SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd {
args := make([]interface{}, 2, 2+len(members))
args[0] = "sadd"
@@ -41,12 +47,25 @@ func (c cmdable) SAdd(ctx context.Context, key string, members ...interface{}) *
return cmd
}
+// Returns the set cardinality (number of elements) of the set stored at key.
+// Returns 0 if key does not exist.
+//
+// For more information about the command please refer to [SCARD].
+//
+// [SCARD]: (https://redis.io/docs/latest/commands/scard/)
func (c cmdable) SCard(ctx context.Context, key string) *IntCmd {
cmd := NewIntCmd(ctx, "scard", key)
_ = c(ctx, cmd)
return cmd
}
+// Returns the members of the set resulting from the difference between the first set
+// and all the successive sets.
+// Keys that do not exist are considered to be empty sets.
+//
+// For more information about the command please refer to [SDIFF].
+//
+// [SDIFF]: (https://redis.io/docs/latest/commands/sdiff/)
func (c cmdable) SDiff(ctx context.Context, keys ...string) *StringSliceCmd {
args := make([]interface{}, 1+len(keys))
args[0] = "sdiff"
@@ -58,6 +77,13 @@ func (c cmdable) SDiff(ctx context.Context, keys ...string) *StringSliceCmd {
return cmd
}
+// Stores the members of the set resulting from the difference between the first set
+// and all the successive sets into destination.
+// If destination already exists, it is overwritten.
+//
+// For more information about the command please refer to [SDIFFSTORE].
+//
+// [SDIFFSTORE]: (https://redis.io/docs/latest/commands/sdiffstore/)
func (c cmdable) SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd {
args := make([]interface{}, 2+len(keys))
args[0] = "sdiffstore"
@@ -70,6 +96,13 @@ func (c cmdable) SDiffStore(ctx context.Context, destination string, keys ...str
return cmd
}
+// Returns the members of the set resulting from the intersection of all the given sets.
+// Keys that do not exist are considered to be empty sets.
+// With one of the keys being an empty set, the resulting set is also empty.
+//
+// For more information about the command please refer to [SINTER].
+//
+// [SINTER]: (https://redis.io/docs/latest/commands/sinter/)
func (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd {
args := make([]interface{}, 1+len(keys))
args[0] = "sinter"
@@ -81,6 +114,16 @@ func (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd {
return cmd
}
+// Returns the cardinality of the set resulting from the intersection of all the given sets.
+// Keys that do not exist are considered to be empty sets.
+// With one of the keys being an empty set, the resulting set is also empty.
+//
+// The limit parameter sets an upper bound on the number of results returned.
+// If limit is 0, no limit is applied.
+//
+// For more information about the command please refer to [SINTERCARD].
+//
+// [SINTERCARD]: (https://redis.io/docs/latest/commands/sintercard/)
func (c cmdable) SInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd {
numKeys := len(keys)
args := make([]interface{}, 4+numKeys)
@@ -96,6 +139,13 @@ func (c cmdable) SInterCard(ctx context.Context, limit int64, keys ...string) *I
return cmd
}
+// Stores the members of the set resulting from the intersection of all the given sets
+// into destination.
+// If destination already exists, it is overwritten.
+//
+// For more information about the command please refer to [SINTERSTORE].
+//
+// [SINTERSTORE]: (https://redis.io/docs/latest/commands/sinterstore/)
func (c cmdable) SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd {
args := make([]interface{}, 2+len(keys))
args[0] = "sinterstore"
@@ -108,13 +158,26 @@ func (c cmdable) SInterStore(ctx context.Context, destination string, keys ...st
return cmd
}
+// Returns if member is a member of the set stored at key.
+// Returns true if the element is a member of the set, false if it is not a member
+// or if key does not exist.
+//
+// For more information about the command please refer to [SISMEMBER].
+//
+// [SISMEMBER]: (https://redis.io/docs/latest/commands/sismember/)
func (c cmdable) SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd {
cmd := NewBoolCmd(ctx, "sismember", key, member)
_ = c(ctx, cmd)
return cmd
}
-// SMIsMember Redis `SMISMEMBER key member [member ...]` command.
+// Returns whether each member is a member of the set stored at key.
+// For each member, returns true if the element is a member of the set, false if it is not
+// a member or if key does not exist.
+//
+// For more information about the command please refer to [SMISMEMBER].
+//
+// [SMISMEMBER]: (https://redis.io/docs/latest/commands/smismember/)
func (c cmdable) SMIsMember(ctx context.Context, key string, members ...interface{}) *BoolSliceCmd {
args := make([]interface{}, 2, 2+len(members))
args[0] = "smismember"
@@ -125,54 +188,100 @@ func (c cmdable) SMIsMember(ctx context.Context, key string, members ...interfac
return cmd
}
-// SMembers Redis `SMEMBERS key` command output as a slice.
+// Returns all the members of the set value stored at key.
+// Returns an empty slice if key does not exist.
+//
+// For more information about the command please refer to [SMEMBERS].
+//
+// [SMEMBERS]: (https://redis.io/docs/latest/commands/smembers/)
func (c cmdable) SMembers(ctx context.Context, key string) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "smembers", key)
_ = c(ctx, cmd)
return cmd
}
-// SMembersMap Redis `SMEMBERS key` command output as a map.
+// Returns all the members of the set value stored at key as a map.
+// Returns an empty map if key does not exist.
+//
+// For more information about the command please refer to [SMEMBERS].
+//
+// [SMEMBERS]: (https://redis.io/docs/latest/commands/smembers/)
func (c cmdable) SMembersMap(ctx context.Context, key string) *StringStructMapCmd {
cmd := NewStringStructMapCmd(ctx, "smembers", key)
_ = c(ctx, cmd)
return cmd
}
+// Moves member from the set at source to the set at destination.
+// This operation is atomic. In every given moment the element will appear to be a member
+// of source or destination for other clients.
+//
+// For more information about the command please refer to [SMOVE].
+//
+// [SMOVE]: (https://redis.io/docs/latest/commands/smove/)
func (c cmdable) SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd {
cmd := NewBoolCmd(ctx, "smove", source, destination, member)
_ = c(ctx, cmd)
return cmd
}
-// SPop Redis `SPOP key` command.
+// Removes and returns one or more random members from the set value stored at key.
+// This version returns a single random member.
+//
+// For more information about the command please refer to [SPOP].
+//
+// [SPOP]: (https://redis.io/docs/latest/commands/spop/)
func (c cmdable) SPop(ctx context.Context, key string) *StringCmd {
cmd := NewStringCmd(ctx, "spop", key)
_ = c(ctx, cmd)
return cmd
}
-// SPopN Redis `SPOP key count` command.
+// Removes and returns one or more random members from the set value stored at key.
+// This version returns up to count random members.
+//
+// For more information about the command please refer to [SPOP].
+//
+// [SPOP]: (https://redis.io/docs/latest/commands/spop/)
func (c cmdable) SPopN(ctx context.Context, key string, count int64) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "spop", key, count)
_ = c(ctx, cmd)
return cmd
}
-// SRandMember Redis `SRANDMEMBER key` command.
+// Returns a random member from the set value stored at key.
+// This version returns a single random member without removing it.
+//
+// For more information about the command please refer to [SRANDMEMBER].
+//
+// [SRANDMEMBER]: (https://redis.io/docs/latest/commands/srandmember/)
func (c cmdable) SRandMember(ctx context.Context, key string) *StringCmd {
cmd := NewStringCmd(ctx, "srandmember", key)
_ = c(ctx, cmd)
return cmd
}
-// SRandMemberN Redis `SRANDMEMBER key count` command.
+// Returns an array of random members from the set value stored at key.
+// This version returns up to count random members without removing them.
+// When called with a positive count, returns distinct elements.
+// When called with a negative count, allows for repeated elements.
+//
+// For more information about the command please refer to [SRANDMEMBER].
+//
+// [SRANDMEMBER]: (https://redis.io/docs/latest/commands/srandmember/)
func (c cmdable) SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "srandmember", key, count)
_ = c(ctx, cmd)
return cmd
}
+// Removes the specified members from the set stored at key.
+// Specified members that are not a member of this set are ignored.
+// If key does not exist, it is treated as an empty set and this command returns 0.
+//
+// For more information about the command please refer to [SREM].
+//
+// [SREM]: (https://redis.io/docs/latest/commands/srem/)
func (c cmdable) SRem(ctx context.Context, key string, members ...interface{}) *IntCmd {
args := make([]interface{}, 2, 2+len(members))
args[0] = "srem"
@@ -183,6 +292,12 @@ func (c cmdable) SRem(ctx context.Context, key string, members ...interface{}) *
return cmd
}
+// Returns the members of the set resulting from the union of all the given sets.
+// Keys that do not exist are considered to be empty sets.
+//
+// For more information about the command please refer to [SUNION].
+//
+// [SUNION]: (https://redis.io/docs/latest/commands/sunion/)
func (c cmdable) SUnion(ctx context.Context, keys ...string) *StringSliceCmd {
args := make([]interface{}, 1+len(keys))
args[0] = "sunion"
@@ -194,6 +309,13 @@ func (c cmdable) SUnion(ctx context.Context, keys ...string) *StringSliceCmd {
return cmd
}
+// Stores the members of the set resulting from the union of all the given sets
+// into destination.
+// If destination already exists, it is overwritten.
+//
+// For more information about the command please refer to [SUNIONSTORE].
+//
+// [SUNIONSTORE]: (https://redis.io/docs/latest/commands/sunionstore/)
func (c cmdable) SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd {
args := make([]interface{}, 2+len(keys))
args[0] = "sunionstore"
@@ -206,6 +328,17 @@ func (c cmdable) SUnionStore(ctx context.Context, destination string, keys ...st
return cmd
}
+// Incrementally iterates the set elements stored at key.
+// This is a cursor-based iterator that allows scanning large sets efficiently.
+//
+// Parameters:
+// - cursor: The cursor value for the iteration (use 0 to start a new scan)
+// - match: Optional pattern to match elements (empty string means no pattern)
+// - count: Optional hint about how many elements to return per iteration
+//
+// For more information about the command please refer to [SSCAN].
+//
+// [SSCAN]: (https://redis.io/docs/latest/commands/sscan/)
func (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
args := []interface{}{"sscan", key, cursor}
if match != "" {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/sortedset_commands.go b/backend/vendor/github.com/redis/go-redis/v9/sortedset_commands.go
index 7827babc..4a6c8f13 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/sortedset_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/sortedset_commands.go
@@ -479,10 +479,16 @@ func (c cmdable) zRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy,
return cmd
}
+// ZRangeByScore returns members in a sorted set within a range of scores.
+//
+// Deprecated: Use ZRangeArgs with ByScore option instead as of Redis 6.2.0.
func (c cmdable) ZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {
return c.zRangeBy(ctx, "zrangebyscore", key, opt, false)
}
+// ZRangeByLex returns members in a sorted set within a lexicographical range.
+//
+// Deprecated: Use ZRangeArgs with ByLex option instead as of Redis 6.2.0.
func (c cmdable) ZRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {
return c.zRangeBy(ctx, "zrangebylex", key, opt, false)
}
@@ -559,6 +565,9 @@ func (c cmdable) ZRemRangeByLex(ctx context.Context, key, min, max string) *IntC
return cmd
}
+// ZRevRange returns members in a sorted set within a range of indexes in reverse order.
+//
+// Deprecated: Use ZRangeArgs with Rev option instead as of Redis 6.2.0.
func (c cmdable) ZRevRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "zrevrange", key, start, stop)
_ = c(ctx, cmd)
@@ -588,10 +597,16 @@ func (c cmdable) zRevRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeB
return cmd
}
+// ZRevRangeByScore returns members in a sorted set within a range of scores in reverse order.
+//
+// Deprecated: Use ZRangeArgs with Rev and ByScore options instead as of Redis 6.2.0.
func (c cmdable) ZRevRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {
return c.zRevRangeBy(ctx, "zrevrangebyscore", key, opt)
}
+// ZRevRangeByLex returns members in a sorted set within a lexicographical range in reverse order.
+//
+// Deprecated: Use ZRangeArgs with Rev and ByLex options instead as of Redis 6.2.0.
func (c cmdable) ZRevRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {
return c.zRevRangeBy(ctx, "zrevrangebylex", key, opt)
}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/stream_commands.go b/backend/vendor/github.com/redis/go-redis/v9/stream_commands.go
index 5573e48b..89ae6a1b 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/stream_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/stream_commands.go
@@ -2,7 +2,11 @@ package redis
import (
"context"
+ "strconv"
+ "strings"
"time"
+
+ "github.com/redis/go-redis/v9/internal/otel"
)
type StreamCmdable interface {
@@ -43,6 +47,7 @@ type StreamCmdable interface {
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:
@@ -52,47 +57,69 @@ type StreamCmdable interface {
//
// 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 int64
- Mode string
- ID string
- Values interface{}
+ Approx bool
+ 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, 11)
+ 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)
+ 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 = append(args, "minid", "=", a.MinID)
}
}
if a.Limit > 0 {
args = append(args, "limit", a.Limit)
}
- if a.Mode != "" {
- args = append(args, a.Mode)
- }
-
if a.ID != "" {
args = append(args, a.ID)
} else {
@@ -299,6 +326,26 @@ func (c cmdable) XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSlic
}
cmd.SetFirstKeyPos(keyPos)
_ = c(ctx, cmd)
+
+ // Record stream lag for each message (if command succeeded)
+ if cmd.Err() == 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
}
@@ -429,6 +476,8 @@ func (c cmdable) xTrim(
args = append(args, "xtrim", key, strategy)
if approx {
args = append(args, "~")
+ } else {
+ args = append(args, "=")
}
args = append(args, threshold)
if limit > 0 {
@@ -466,6 +515,8 @@ func (c cmdable) xTrimMode(
args = append(args, "xtrim", key, strategy)
if approx {
args = append(args, "~")
+ } else {
+ args = append(args, "=")
}
args = append(args, threshold)
if limit > 0 {
@@ -523,3 +574,28 @@ func (c cmdable) XInfoStreamFull(ctx context.Context, key string, count int) *XI
_ = 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
+}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/string_commands.go b/backend/vendor/github.com/redis/go-redis/v9/string_commands.go
index f3c33f4c..f69d3d05 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/string_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/string_commands.go
@@ -143,6 +143,9 @@ func (c cmdable) GetRange(ctx context.Context, key string, start, end int64) *St
return cmd
}
+// GetSet returns the old value stored at key and sets it to the new value.
+//
+// Deprecated: Use SetArgs with Get option instead as of Redis 6.2.0.
func (c cmdable) GetSet(ctx context.Context, key string, value interface{}) *StringCmd {
cmd := NewStringCmd(ctx, "getset", key, value)
_ = c(ctx, cmd)
@@ -415,14 +418,18 @@ func (c cmdable) SetArgs(ctx context.Context, key string, value interface{}, a S
return cmd
}
-// SetEx Redis `SETEx key expiration value` command.
+// SetEx sets the value and expiration of a key.
+//
+// Deprecated: Use Set with expiration instead as of Redis 2.6.12.
func (c cmdable) SetEx(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd {
cmd := NewStatusCmd(ctx, "setex", key, formatSec(ctx, expiration), value)
_ = c(ctx, cmd)
return cmd
}
-// SetNX Redis `SET key value [expiration] NX` command.
+// SetNX sets the value of a key only if the key does not exist.
+//
+// Deprecated: Use Set with NX option instead as of Redis 2.6.12.
//
// Zero expiration means the key has no expiration time.
// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0,
diff --git a/backend/vendor/github.com/redis/go-redis/v9/timeseries_commands.go b/backend/vendor/github.com/redis/go-redis/v9/timeseries_commands.go
index 82d8cdfc..15d80168 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/timeseries_commands.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/timeseries_commands.go
@@ -2,9 +2,9 @@ package redis
import (
"context"
- "strconv"
"github.com/redis/go-redis/v9/internal/proto"
+ "github.com/redis/go-redis/v9/internal/util"
)
type TimeseriesCmdable interface {
@@ -96,6 +96,8 @@ const (
VarP
VarS
Twa
+ CountNaN
+ CountAll
)
func (a Aggregator) String() string {
@@ -128,6 +130,10 @@ func (a Aggregator) String() string {
return "VAR.S"
case Twa:
return "TWA"
+ case CountNaN:
+ return "COUNTNAN"
+ case CountAll:
+ return "COUNTALL"
default:
return ""
}
@@ -486,8 +492,9 @@ type TSTimestampValueCmd struct {
func newTSTimestampValueCmd(ctx context.Context, args ...interface{}) *TSTimestampValueCmd {
return &TSTimestampValueCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeTSTimestampValue,
},
}
}
@@ -524,7 +531,7 @@ func (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) {
return err
}
cmd.val.Timestamp = timestamp
- cmd.val.Value, err = strconv.ParseFloat(value, 64)
+ cmd.val.Value, err = util.ParseStringToFloat(value)
if err != nil {
return err
}
@@ -533,6 +540,13 @@ func (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *TSTimestampValueCmd) Clone() Cmder {
+ return &TSTimestampValueCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: cmd.val, // TSTimestampValue is a simple struct, can be copied directly
+ }
+}
+
// TSInfo - Returns information about a time-series key.
// For more information - https://redis.io/commands/ts.info/
func (c cmdable) TSInfo(ctx context.Context, key string) *MapStringInterfaceCmd {
@@ -704,8 +718,9 @@ type TSTimestampValueSliceCmd struct {
func newTSTimestampValueSliceCmd(ctx context.Context, args ...interface{}) *TSTimestampValueSliceCmd {
return &TSTimestampValueSliceCmd{
baseCmd: baseCmd{
- ctx: ctx,
- args: args,
+ ctx: ctx,
+ args: args,
+ cmdType: CmdTypeTSTimestampValueSlice,
},
}
}
@@ -743,7 +758,7 @@ func (cmd *TSTimestampValueSliceCmd) readReply(rd *proto.Reader) (err error) {
return err
}
cmd.val[i].Timestamp = timestamp
- cmd.val[i].Value, err = strconv.ParseFloat(value, 64)
+ cmd.val[i].Value, err = util.ParseStringToFloat(value)
if err != nil {
return err
}
@@ -752,6 +767,18 @@ func (cmd *TSTimestampValueSliceCmd) readReply(rd *proto.Reader) (err error) {
return nil
}
+func (cmd *TSTimestampValueSliceCmd) Clone() Cmder {
+ var val []TSTimestampValue
+ if cmd.val != nil {
+ val = make([]TSTimestampValue, len(cmd.val))
+ copy(val, cmd.val)
+ }
+ return &TSTimestampValueSliceCmd{
+ baseCmd: cmd.cloneBaseCmd(),
+ val: val,
+ }
+}
+
// TSMRange - Returns a range of samples from multiple time-series keys.
// For more information - https://redis.io/commands/ts.mrange/
func (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd {
diff --git a/backend/vendor/github.com/redis/go-redis/v9/universal.go b/backend/vendor/github.com/redis/go-redis/v9/universal.go
index 1dc9764d..2531cb59 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/universal.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/universal.go
@@ -8,6 +8,7 @@ import (
"github.com/redis/go-redis/v9/auth"
"github.com/redis/go-redis/v9/maintnotifications"
+ "github.com/redis/go-redis/v9/push"
)
// UniversalOptions information is required by UniversalClient to establish
@@ -57,7 +58,18 @@ type UniversalOptions struct {
MinRetryBackoff time.Duration
MaxRetryBackoff time.Duration
- DialTimeout time.Duration
+ DialTimeout time.Duration
+
+ // DialerRetries is the maximum number of retry attempts when dialing fails.
+ //
+ // default: 5
+ DialerRetries int
+
+ // DialerRetryTimeout is the backoff duration between retry attempts.
+ //
+ // default: 100 milliseconds
+ DialerRetryTimeout time.Duration
+
ReadTimeout time.Duration
WriteTimeout time.Duration
ContextTimeoutEnabled bool
@@ -79,13 +91,19 @@ type UniversalOptions struct {
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
PoolFIFO bool
- PoolSize int
- PoolTimeout time.Duration
- MinIdleConns int
- MaxIdleConns int
- MaxActiveConns int
- ConnMaxIdleTime time.Duration
- ConnMaxLifetime time.Duration
+ PoolSize int
+
+ // MaxConcurrentDials is the maximum number of concurrent connection creation goroutines.
+ // If <= 0, defaults to PoolSize. If > PoolSize, it will be capped at PoolSize.
+ MaxConcurrentDials int
+
+ PoolTimeout time.Duration
+ MinIdleConns int
+ MaxIdleConns int
+ MaxActiveConns int
+ ConnMaxIdleTime time.Duration
+ ConnMaxLifetime time.Duration
+ ConnMaxLifetimeJitter time.Duration
TLSConfig *tls.Config
@@ -121,6 +139,10 @@ type UniversalOptions struct {
UnstableResp3 bool
+ // PushNotificationProcessor is the processor for handling push notifications.
+ // If nil, a default processor will be created for RESP3 connections.
+ PushNotificationProcessor push.NotificationProcessor
+
// IsClusterMode can be used when only one Addrs is provided (e.g. Elasticache supports setting up cluster mode with configuration endpoint).
IsClusterMode bool
@@ -156,32 +178,37 @@ func (o *UniversalOptions) Cluster() *ClusterOptions {
MinRetryBackoff: o.MinRetryBackoff,
MaxRetryBackoff: o.MaxRetryBackoff,
- DialTimeout: o.DialTimeout,
- ReadTimeout: o.ReadTimeout,
- WriteTimeout: o.WriteTimeout,
+ DialTimeout: o.DialTimeout,
+ DialerRetries: o.DialerRetries,
+ DialerRetryTimeout: o.DialerRetryTimeout,
+ ReadTimeout: o.ReadTimeout,
+ WriteTimeout: o.WriteTimeout,
+
ContextTimeoutEnabled: o.ContextTimeoutEnabled,
ReadBufferSize: o.ReadBufferSize,
WriteBufferSize: o.WriteBufferSize,
- PoolFIFO: o.PoolFIFO,
-
- PoolSize: o.PoolSize,
- PoolTimeout: o.PoolTimeout,
- MinIdleConns: o.MinIdleConns,
- MaxIdleConns: o.MaxIdleConns,
- MaxActiveConns: o.MaxActiveConns,
- ConnMaxIdleTime: o.ConnMaxIdleTime,
- ConnMaxLifetime: o.ConnMaxLifetime,
+ PoolFIFO: o.PoolFIFO,
+ PoolSize: o.PoolSize,
+ MaxConcurrentDials: o.MaxConcurrentDials,
+ PoolTimeout: o.PoolTimeout,
+ MinIdleConns: o.MinIdleConns,
+ MaxIdleConns: o.MaxIdleConns,
+ MaxActiveConns: o.MaxActiveConns,
+ ConnMaxIdleTime: o.ConnMaxIdleTime,
+ ConnMaxLifetime: o.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter,
TLSConfig: o.TLSConfig,
- DisableIdentity: o.DisableIdentity,
- DisableIndentity: o.DisableIndentity,
- IdentitySuffix: o.IdentitySuffix,
- FailingTimeoutSeconds: o.FailingTimeoutSeconds,
- UnstableResp3: o.UnstableResp3,
- MaintNotificationsConfig: o.MaintNotificationsConfig,
+ DisableIdentity: o.DisableIdentity,
+ DisableIndentity: o.DisableIndentity,
+ IdentitySuffix: o.IdentitySuffix,
+ FailingTimeoutSeconds: o.FailingTimeoutSeconds,
+ UnstableResp3: o.UnstableResp3,
+ PushNotificationProcessor: o.PushNotificationProcessor,
+ MaintNotificationsConfig: o.MaintNotificationsConfig,
}
}
@@ -217,31 +244,37 @@ func (o *UniversalOptions) Failover() *FailoverOptions {
MinRetryBackoff: o.MinRetryBackoff,
MaxRetryBackoff: o.MaxRetryBackoff,
- DialTimeout: o.DialTimeout,
- ReadTimeout: o.ReadTimeout,
- WriteTimeout: o.WriteTimeout,
+ DialTimeout: o.DialTimeout,
+ DialerRetries: o.DialerRetries,
+ DialerRetryTimeout: o.DialerRetryTimeout,
+ ReadTimeout: o.ReadTimeout,
+ WriteTimeout: o.WriteTimeout,
+
ContextTimeoutEnabled: o.ContextTimeoutEnabled,
ReadBufferSize: o.ReadBufferSize,
WriteBufferSize: o.WriteBufferSize,
- PoolFIFO: o.PoolFIFO,
- PoolSize: o.PoolSize,
- PoolTimeout: o.PoolTimeout,
- MinIdleConns: o.MinIdleConns,
- MaxIdleConns: o.MaxIdleConns,
- MaxActiveConns: o.MaxActiveConns,
- ConnMaxIdleTime: o.ConnMaxIdleTime,
- ConnMaxLifetime: o.ConnMaxLifetime,
+ PoolFIFO: o.PoolFIFO,
+ PoolSize: o.PoolSize,
+ MaxConcurrentDials: o.MaxConcurrentDials,
+ PoolTimeout: o.PoolTimeout,
+ MinIdleConns: o.MinIdleConns,
+ MaxIdleConns: o.MaxIdleConns,
+ MaxActiveConns: o.MaxActiveConns,
+ ConnMaxIdleTime: o.ConnMaxIdleTime,
+ ConnMaxLifetime: o.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter,
TLSConfig: o.TLSConfig,
ReplicaOnly: o.ReadOnly,
- DisableIdentity: o.DisableIdentity,
- DisableIndentity: o.DisableIndentity,
- IdentitySuffix: o.IdentitySuffix,
- UnstableResp3: o.UnstableResp3,
+ DisableIdentity: o.DisableIdentity,
+ DisableIndentity: o.DisableIndentity,
+ IdentitySuffix: o.IdentitySuffix,
+ UnstableResp3: o.UnstableResp3,
+ PushNotificationProcessor: o.PushNotificationProcessor,
// Note: MaintNotificationsConfig not supported for FailoverOptions
}
}
@@ -271,30 +304,36 @@ func (o *UniversalOptions) Simple() *Options {
MinRetryBackoff: o.MinRetryBackoff,
MaxRetryBackoff: o.MaxRetryBackoff,
- DialTimeout: o.DialTimeout,
- ReadTimeout: o.ReadTimeout,
- WriteTimeout: o.WriteTimeout,
+ DialTimeout: o.DialTimeout,
+ DialerRetries: o.DialerRetries,
+ DialerRetryTimeout: o.DialerRetryTimeout,
+ ReadTimeout: o.ReadTimeout,
+ WriteTimeout: o.WriteTimeout,
+
ContextTimeoutEnabled: o.ContextTimeoutEnabled,
ReadBufferSize: o.ReadBufferSize,
WriteBufferSize: o.WriteBufferSize,
- PoolFIFO: o.PoolFIFO,
- PoolSize: o.PoolSize,
- PoolTimeout: o.PoolTimeout,
- MinIdleConns: o.MinIdleConns,
- MaxIdleConns: o.MaxIdleConns,
- MaxActiveConns: o.MaxActiveConns,
- ConnMaxIdleTime: o.ConnMaxIdleTime,
- ConnMaxLifetime: o.ConnMaxLifetime,
+ PoolFIFO: o.PoolFIFO,
+ PoolSize: o.PoolSize,
+ MaxConcurrentDials: o.MaxConcurrentDials,
+ PoolTimeout: o.PoolTimeout,
+ MinIdleConns: o.MinIdleConns,
+ MaxIdleConns: o.MaxIdleConns,
+ MaxActiveConns: o.MaxActiveConns,
+ ConnMaxIdleTime: o.ConnMaxIdleTime,
+ ConnMaxLifetime: o.ConnMaxLifetime,
+ ConnMaxLifetimeJitter: o.ConnMaxLifetimeJitter,
TLSConfig: o.TLSConfig,
- DisableIdentity: o.DisableIdentity,
- DisableIndentity: o.DisableIndentity,
- IdentitySuffix: o.IdentitySuffix,
- UnstableResp3: o.UnstableResp3,
- MaintNotificationsConfig: o.MaintNotificationsConfig,
+ DisableIdentity: o.DisableIdentity,
+ DisableIndentity: o.DisableIndentity,
+ IdentitySuffix: o.IdentitySuffix,
+ UnstableResp3: o.UnstableResp3,
+ PushNotificationProcessor: o.PushNotificationProcessor,
+ MaintNotificationsConfig: o.MaintNotificationsConfig,
}
}
diff --git a/backend/vendor/github.com/redis/go-redis/v9/version.go b/backend/vendor/github.com/redis/go-redis/v9/version.go
index 126fa10b..49f001e5 100644
--- a/backend/vendor/github.com/redis/go-redis/v9/version.go
+++ b/backend/vendor/github.com/redis/go-redis/v9/version.go
@@ -2,5 +2,5 @@ package redis
// Version is the current release version.
func Version() string {
- return "9.17.2"
+ return "9.18.0"
}
diff --git a/backend/vendor/github.com/slack-go/slack/CHANGELOG.md b/backend/vendor/github.com/slack-go/slack/CHANGELOG.md
new file mode 100644
index 00000000..d471bbaa
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/CHANGELOG.md
@@ -0,0 +1,500 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [0.21.1] - 2026-04-08
+
+### Added
+
+- **`slackevents.ChannelType*` constants and `MessageEvent` helpers** — Added
+ `ChannelTypeChannel`, `ChannelTypeGroup`, `ChannelTypeIM`, `ChannelTypeMPIM` constants
+ and `IsChannel()`, `IsGroup()`, `IsIM()`, `IsMpIM()` methods on `MessageEvent` so
+ callers no longer need to compare raw strings.
+
+### Fixed
+
+- **Duplicate attachment/block serialization in `MsgOptionAttachments` / `MsgOptionBlocks`** —
+ Attachments and blocks were serialized twice: once into typed struct fields (for the JSON
+ response-URL path) and again into `url.Values` (for the form POST path). Serialization for
+ the form path now happens inside `formSender.BuildRequestContext`, so each sender owns its
+ own marshalling. This fixes the long-standing FIXME and eliminates redundant `json.Marshal`
+ calls in the option functions. ([#1547])
+
+ > [!NOTE]
+ > `UnsafeApplyMsgOptions` returns `config.values` directly. After this change,
+ > `attachments` and `blocks` keys are no longer present in those values since
+ > marshalling is deferred to send time. This function is documented as unsupported.
+
+## [0.21.0] - 2026-04-05
+
+### Deprecated
+
+- **`slackevents.ParseActionEvent`** — Cannot parse `block_actions` payloads (returns
+ unmarshalling error). Use `slack.InteractionCallback` with `json.Unmarshal` instead,
+ or `slack.InteractionCallbackParse` for HTTP requests. `InteractionCallback` handles
+ all interaction types. ([#596])
+- **`slackevents.MessageAction`**, **`MessageActionEntity`**, **`MessageActionResponse`** —
+ Associated types that only support legacy `interactive_message` payloads.
+
+### Removed
+
+- **`IM` struct** — Removed the `IM` struct (and unused internal types `imChannel`,
+ `imResponseFull`). The `IsUserDeleted` field has been moved to `Conversation`, where it
+ is populated for IM-type conversations. Code using `IM` should switch to `Conversation`.
+
+ > [!NOTE]
+ > In practice no user should be affected — `IM` was never returned by any public API
+ > method in this library, so there was no way to obtain one outside of manual construction.
+
+- **`Info.GetBotByID`, `GetUserByID`, `GetChannelByID`, `GetGroupByID`, `GetIMByID`** —
+ These methods were deprecated and returned `nil` unconditionally. They have been removed.
+
+ > [!WARNING]
+ > **Breaking change.** If you are calling any of these methods, remove those calls — they
+ > were already no-ops.
+
+### Added
+
+- **`admin.teams.settings.*` API support** — `AdminTeamsSettingsInfo`,
+ `AdminTeamsSettingsSetDefaultChannels`, `AdminTeamsSettingsSetDescription`,
+ `AdminTeamsSettingsSetDiscoverability`, `AdminTeamsSettingsSetIcon`, and
+ `AdminTeamsSettingsSetName`. Includes `TeamDiscoverability` enum with `Open`,
+ `InviteOnly`, `Closed`, and `Unlisted` variants. ([#960])
+- **`OAuthOptionAPIURL` for package-level OAuth functions** — All package-level OAuth
+ functions (`GetOAuthV2Response`, `GetOpenIDConnectToken`, `RefreshOAuthV2Token`, etc.)
+ now accept variadic `OAuthOption` arguments. Use `OAuthOptionAPIURL(url)` to override
+ the default Slack API URL, enabling integration tests against a local HTTP server.
+ Existing callers are unaffected. ([#744])
+- **`GetOpenIDConnectUserInfo` / `GetOpenIDConnectUserInfoContext`** — Returns identity
+ information about the user associated with the token via `openid.connect.userInfo`.
+ Complements the existing `GetOpenIDConnectToken` method. ([#967])
+- **HTTP response headers** — Slack API response headers (e.g. `X-OAuth-Scopes`,
+ `X-Accepted-OAuth-Scopes`, `X-Ratelimit-*`) are now accessible. `AuthTestResponse`
+ exposes a `Header` field directly. For all other methods, use
+ `OptionOnResponseHeaders(func(method string, headers http.Header))` to register a
+ callback that fires after every API call. ([#1076])
+- **`DNDOptionTeamID`** — `GetDNDInfo` and `GetDNDTeamInfo` now accept optional
+ `ParamOption` arguments. Use `DNDOptionTeamID("T...")` to pass `team_id`, which is
+ required after workspace migration (Slack returns `missing_argument` without it).
+ ([#1157])
+- **`UpdateUserGroupMembersList` / `UpdateUserGroupMembersListContext`** — Convenience
+ wrappers around `UpdateUserGroupMembers` that accept `[]string` instead of a
+ comma-separated string, enabling clean chaining with `GetUserGroupMembers`. ([#1172])
+- **`SetUserProfile` / `SetUserProfileContext`** — Set multiple user profile fields in a
+ single API call by passing a `*UserProfile` struct to `users.profile.set`. Complements
+ the existing single-field methods (`SetUserRealName`, `SetUserCustomStatus`, etc.).
+ ([#1158])
+- **API warning callbacks** — Slack API responses may include a `warnings` field with
+ deprecation notices or usage hints. Use `OptionWarnings(func(warnings []string))` to
+ register a callback that receives these warnings. ([#1540])
+- **RTM support for `user_status_changed`, `user_huddle_changed`, `user_profile_changed`
+ events** — these events are now mapped in `EventMapping` with dedicated structs
+ (`UserStatusChangedEvent`, `UserHuddleChangedEvent`, `UserProfileChangedEvent`).
+ Previously they triggered `UnmarshallingErrorEvent`. ([#1541])
+- **RTM support for `sh_room_join`, `sh_room_leave`, `sh_room_update`, `channel_updated`
+ events** — Slack Call/Huddle room events and channel property updates are now mapped with
+ dedicated structs (`SHRoomJoinEvent`, `SHRoomLeaveEvent`, `SHRoomUpdateEvent`,
+ `ChannelUpdatedEvent`). ([#858])
+- **`CacheTS` and `EventTS` fields on `UserChangeEvent`** — these fields were sent by Slack
+ but silently dropped during unmarshalling.
+- **`workflows.featured` API support** — add, list, remove, and set featured workflows on
+ channels via `WorkflowsFeaturedAdd`, `WorkflowsFeaturedList`, `WorkflowsFeaturedRemove`,
+ and `WorkflowsFeaturedSet`
+- **`IsConnectorBot` and `IsWorkflowBot` in `User`** — boolean flags for connector and
+ workflow bot users
+- **`GuestInvitedBy` in `UserProfile`** — user ID of whoever invited a guest user
+- **`Blocks` field on `MessageEvent`** — block data from webhook payloads is now directly
+ accessible via `event.Blocks` instead of only through `event.Message.Blocks`. ([#1257])
+- **`Username` field on `User`** — Slack's interaction payloads (block_actions, shortcuts)
+ include a `username` field in the user object that was previously dropped during
+ unmarshalling. ([#1218])
+- **`Blocks`, `Attachments`, `Files`, `Upload` fields on `AppMentionEvent`** — these fields
+ are sent by Slack in `app_mention` event payloads but were silently dropped. ([#961])
+- **`HandleShortcut`, `HandleViewSubmission`, `HandleViewClosed` in socketmode handler** —
+ Level 3 handlers that dispatch `shortcut`/`message_action`, `view_submission`, and
+ `view_closed` interactions by `CallbackID`, matching the pattern of
+ `HandleInteractionBlockAction` and `HandleSlashCommand`. ([#1161])
+- **`BlockFromJSON` / `MustBlockFromJSON`** — Create blocks from raw JSON strings, enabling
+ direct use of output from Slack's [Block Kit Builder](https://app.slack.com/block-kit-builder)
+ or quick adoption of new block types before the library adds typed support. The original
+ JSON is preserved through marshalling. ([#1497])
+
+### Documentation
+
+- **`ViewSubmissionResponse` constructors** — `NewClearViewSubmissionResponse`,
+ `NewUpdateViewSubmissionResponse`, `NewPushViewSubmissionResponse`, and
+ `NewErrorsViewSubmissionResponse` now have doc comments explaining the HTTP response
+ pattern (write JSON and return promptly) and the Socket Mode pattern (pass as Ack
+ payload). `NewErrorsViewSubmissionResponse` additionally documents that map keys must
+ be `BlockID`s of `InputBlock` elements. ([#726], [#1013])
+- **Socket Mode examples** — `examples/socketmode/` and `examples/socketmode_handler/` now
+ have doc comments explaining the two-token requirement: app-level token (`xapp-`) for the
+ WebSocket connection and bot token (`xoxb-`) for API calls. ([#941])
+
+### Fixed
+
+- **`UnknownBlock` round-trip data loss** — Unrecognized block types (e.g. new Slack block
+ types not yet supported by this library) now preserve their full JSON through
+ unmarshal/marshal cycles. Previously only `type` and `block_id` were retained, silently
+ discarding all other fields.
+
+### Changed
+
+- Adjusted some `admin` errors that started with uppercase to be lowercase per go
+ conventions.
+
+ > [!WARNING]
+ > **Breaking change.** If you are matching the error content in your code, this is a
+ > BREAKING CHANGE.
+- **`WebhookMessage.UnfurlLinks` and `UnfurlMedia` are now `*bool`** — Previously these
+ were `bool` with `omitempty`, which meant `false` was silently stripped from the JSON
+ payload. Users could not explicitly disable link or media unfurling via webhooks. The
+ fields are now `*bool` so that `nil` (omit), `false`, and `true` all serialize correctly.
+ ([#1231])
+
+ > [!WARNING]
+ > **Breaking change.** Code that sets these fields directly must be updated:
+ >
+ > ```go
+ > // Before
+ > msg := slack.WebhookMessage{UnfurlLinks: true}
+ >
+ > // After — use a helper or a variable
+ > t := true
+ > msg := slack.WebhookMessage{UnfurlLinks: &t}
+ > ```
+ >
+ > Leaving the fields unset (`nil`) preserves the previous default behavior — Slack's
+ > server-side defaults apply (`unfurl_links=false`, `unfurl_media=true`).
+
+- **`User.Has2FA` is now `*bool`** — When using a bot token, Slack's `users.list` API omits
+ `has_2fa` entirely. With a plain `bool`, this was indistinguishable from explicitly `false`.
+ Now `nil` means absent/unknown, `false` means explicitly disabled, `true` means enabled.
+ ([#1121])
+
+ > [!WARNING]
+ > **Breaking change.** Code that reads `Has2FA` must handle the pointer:
+ >
+ > ```go
+ > // Before
+ > if user.Has2FA { ... }
+ >
+ > // After
+ > if user.Has2FA != nil && *user.Has2FA { ... }
+ > ```
+
+- **`ListReactions` now uses cursor-based pagination** — `ListReactionsParameters` replaces
+ `Count`/`Page` with `Cursor`/`Limit`, and `ListReactions`/`ListReactionsContext` now return
+ `([]ReactedItem, string, error)` where the string is the next cursor, instead of
+ `([]ReactedItem, *Paging, error)`. ([#825])
+
+ > [!WARNING]
+ > **Breaking change.** Both the parameters and return signature have changed:
+ >
+ > ```go
+ > // Before
+ > params := slack.NewListReactionsParameters()
+ > params.Count = 100
+ > params.Page = 2
+ > items, paging, err := api.ListReactions(params)
+ >
+ > // After
+ > params := slack.NewListReactionsParameters()
+ > params.Limit = 100
+ > items, nextCursor, err := api.ListReactions(params)
+ > // Use nextCursor for the next page: params.Cursor = nextCursor
+ > ```
+
+- **`ListStars`/`GetStarred` now use cursor-based pagination** — `StarsParameters` replaces
+ `Count`/`Page` with `Cursor`/`Limit` (and adds `TeamID`), and `ListStars`/`ListStarsContext`/
+ `GetStarred`/`GetStarredContext` now return `string` (next cursor) instead of `*Paging`.
+ Slack's `stars.list` API no longer returns `paging` data — only `response_metadata.next_cursor`.
+
+ > [!WARNING]
+ > **Breaking change.** Both the parameters and return signature have changed:
+ >
+ > ```go
+ > // Before
+ > params := slack.NewStarsParameters()
+ > params.Count = 100
+ > params.Page = 2
+ > items, paging, err := api.ListStars(params)
+ >
+ > // After
+ > params := slack.NewStarsParameters()
+ > params.Limit = 100
+ > items, nextCursor, err := api.ListStars(params)
+ > // Use nextCursor for the next page: params.Cursor = nextCursor
+ > ```
+
+- **`GetAccessLogs` now uses cursor-based pagination** — `AccessLogParameters` replaces
+ `Count`/`Page` with `Cursor`/`Limit` (and adds `Before`), and `GetAccessLogs`/
+ `GetAccessLogsContext` now return `string` (next cursor) instead of `*Paging`.
+ Slack's `team.accessLogs` API warns `use_cursor_pagination_instead` when using the old
+ parameters.
+
+ > [!WARNING]
+ > **Breaking change.** Both the parameters and return signature have changed:
+ >
+ > ```go
+ > // Before
+ > params := slack.NewAccessLogParameters()
+ > params.Count = 100
+ > params.Page = 2
+ > logins, paging, err := api.GetAccessLogs(params)
+ >
+ > // After
+ > params := slack.NewAccessLogParameters()
+ > params.Limit = 100
+ > logins, nextCursor, err := api.GetAccessLogs(params)
+ > // Use nextCursor for the next page: params.Cursor = nextCursor
+ > ```
+
+### Fixed
+
+- **Socket Mode: large Ack payloads no longer silently fail** — Two issues caused `Ack()`
+ payloads to be silently dropped by Slack. First, gorilla/websocket's default 4KB write
+ buffer fragmented messages into WebSocket continuation frames that Slack does not
+ reassemble. The library now uses a 32KB write buffer. Second, Slack silently drops
+ Socket Mode responses at or above 20KB — `Ack()`, `Send()`, and `SendCtx()` now return
+ an error when the serialized response reaches this limit. ([#1196])
+
+ > [!WARNING]
+ > **Breaking change.** `Ack()` and `Send()` now return `error`. Existing call sites that
+ > don't capture the return value continue to compile without changes.
+
+- **`MsgOptionBlocks()` with no arguments now sends `blocks=[]`** — Previously, calling
+ `MsgOptionBlocks()` with no arguments or a nil spread was a silent no-op, making it
+ impossible to clear blocks from a message via `chat.update`. The Slack API requires an
+ explicit `blocks=[]` to reliably remove blocks. ([#1214])
+
+ > [!WARNING]
+ > **Breaking change.** `MsgOptionBlocks()` with no arguments now sends `blocks=[]` instead
+ > of being a no-op. If you were relying on this to be a no-op, remove the option entirely:
+ >
+ > ```go
+ > // Before — this was a no-op, now it sends blocks=[]
+ > api.PostMessage(ch, slack.MsgOptionBlocks(), slack.MsgOptionText("text", false))
+ >
+ > // After — omit MsgOptionBlocks entirely to not set blocks
+ > api.PostMessage(ch, slack.MsgOptionText("text", false))
+ > ```
+
+- **`WorkflowButtonBlockElement` missing from `UnmarshalJSON`** — `workflow_button` blocks
+ now unmarshal correctly through `BlockElements`, `InputBlock`, and `Accessory` paths.
+ Also adds missing `multi_*_select` and `file_input` cases to `BlockElements.UnmarshalJSON`,
+ and fixes `toBlockElement` for `RichTextInputElement` and `WorkflowButtonElement`. ([#1539])
+- **`NewBlockHeader` nil pointer dereference** — passing a nil text object no longer panics. ([#1236])
+- **`ValidateUniqueBlockID` rejects empty block IDs** — multiple input blocks with no
+ explicit `block_id` set (empty string) were incorrectly flagged as duplicates, causing
+ `OpenView` to fail. ([#1184])
+
+## [0.20.0] - 2026-03-21
+
+> [!WARNING]
+> `trigger_id` and `workflow_id` are NOT in any documentation or in any of the official
+libraries, so exercise caution if you use these.
+
+### Added
+
+- **`workflow_id` and `trigger_id` in `Message`** — It seems that some types of messages,
+ e.g: `bot_message`, can carry `trigger_id` and `workflow_id`.
+- **`RichTextQuote.Border` field** — optional border toggle (matches the docs now)
+- **`RichTextPreformatted.Language` field** — enables syntax highlighting for preformatted
+ blocks
+
+### Fixed
+
+- **Remove embedding of `RichTextSection`** — `RichTextQuote` and `RichTextPreformatted`
+ are now flattened as they should have always been. This is a breaking change for anyone
+ using these structs directly.
+
+## [0.19.0] - 2026-03-04
+
+### Added
+
+- **Optional HTTP retry for Web API** — Retries are off by default. Enable with `OptionRetry(n)` for 429-only retries or `OptionRetryConfig(cfg)` for full control including 5xx and connection errors with exponential backoff. ([#1532])
+- **`task_card` and `plan` agent blocks** — New block types for task cards and plan agent blocks. ([#1536])
+
+### Changed
+
+- CI: bumped `actions/stale` from 10.1.1 to 10.2.0. ([#1534])
+- Use `golangci-lint` in Makefile. ([#1533])
+
+## [0.18.0] - 2026-02-21
+
+### Added
+
+- **`focus_on_load` support for remaining block elements** — Static/external/users/conversations/channels select, multi-select variants, datepicker, timepicker, plain_text_input, checkboxes, radio_buttons, and number_input. ([#1519])
+- **`PlainText` and `PreviewPlainText` fields on `File`** — Email file objects now include the plain text body fields instead of silently discarding them. ([#1522])
+- **Missing fields on `User`, `UserProfile`, and `EnterpriseUser`** — `who_can_share_contact_card`, `always_active`, `pronouns`, `image_1024`, `is_custom_image`, `status_text_canonical`, `huddle_state`, `huddle_state_expiration_ts`, `start_date`, and `is_primary_owner`. ([#1526])
+- **Work Objects support** — Chat unfurl with Work Object metadata, entity details (flexpane), `entity_details_requested` event, and associated types (`WorkObjectMetadata`, `WorkObjectEntity`, `WorkObjectExternalRef`). ([#1529])
+- **`admin.roles.*` API methods** — `admin.roles.listAssignments`, `admin.roles.addAssignments`, and `admin.roles.removeAssignments`. ([#1520])
+
+### Fixed
+
+- **`UserProfile.Skype` JSON tag** — Corrected typo from `"skyp"` to `"skype"`. ([#1524])
+- **`assistant.threads.setSuggestedPrompts` title parameter** — Title is now sent when non-empty. ([#1528])
+
+### Changed
+
+- CI test matrix updated: dropped Go 1.24, added Go 1.26; bumped golangci-lint to v2.10.1. ([#1530])
+
+## [0.18.0-rc2] - 2026-01-28
+
+### Added
+
+- **Audit Logs example** - New example demonstrating how to use the Audit Logs API. ([#1144])
+- **Admin Conversations API support** - Comprehensive support for `admin.conversations.*`
+ methods including core operations (archive, unarchive, create, delete, rename, invite,
+ search, lookup, getTeams, convertToPrivate, convertToPublic, disconnectShared, setTeams),
+ bulk operations (bulkArchive, bulkDelete, bulkMove), preferences, retention management,
+ restrict access controls, and EKM channel info. ([#1329])
+
+### Changed
+
+- **BREAKING**: Removed deprecated `UploadFile`, `UploadFileContext`, and
+ `FileUploadParameters`. The `files.upload` API was discontinued by Slack on November
+ 12, 2025. ([#1481])
+- **BREAKING**: Renamed `UploadFileV2` → `UploadFile`, `UploadFileV2Context` →
+ `UploadFileContext`, and `UploadFileV2Parameters` → `UploadFileParameters`. The "V2"
+ suffix is no longer needed now that the old API is removed. ([#1481])
+
+### Fixed
+
+- **File upload error wrapping** - `UploadFile` now wraps errors with the step name
+ (`GetUploadURLExternal`, `UploadToURL`, or `CompleteUploadExternal`) so callers can
+ identify which of the three upload steps failed. ([#1491])
+- **Audit Logs API endpoint** - Fixed `GetAuditLogs` to use the correct endpoint
+ (`api.slack.com`) instead of the regular API endpoint (`slack.com/api`). The Audit
+ Logs API requires a different base URL. Added `OptionAuditAPIURL` for testing. ([#1144])
+- **Socket mode websocket dial debugging** - Added debug logging when a custom dialer is
+ used including HTTP response status on dial failures. This helps diagnose proxy/TLS
+ issues like "bad handshake" errors. ([#1360])
+- **`MsgOptionPostMessageParameters` now passes `MetaData`** - Previously, metadata was
+ silently dropped when using `PostMessageParameters`. ([#1343])
+
+## [0.18.0-rc1] - 2026-01-26
+
+### Added
+
+- **Huddle support** - New `HuddleRoom`, `HuddleParticipantEvent`, and `HuddleRecording`
+ types for handling Slack huddle events (`huddle_thread` subtype messages).
+- **Call block data parsing** - `CallBlock` now includes full call data when retrieved
+ from Slack messages, with new `CallBlockData`, `CallBlockDataV1`, and `CallBlockIconURLs`
+ types. ([#897])
+- **Chat Streaming API support** - New streaming API for real-time chat interactions
+ with example usage. ([#1506])
+- **Data Access API support** - Full support for Slack's Data Access API with
+ example implementation. ([#1439])
+- **Cursor-based pagination for `GetUsers`** - More efficient user retrieval
+ with cursor pagination. ([#1465])
+- **`GetAllConversations` with pagination** - Retrieve all conversations with
+ automatic pagination handling, including rate limit and server error handling. ([#1463])
+- **Table blocks support** - Parse and create table blocks with proper
+ unmarshaling. ([#1490], [#1511])
+- **Context actions block support** - New `context_actions` block type. ([#1495])
+- **Workflow button block element** - Support for `workflow_button` in block
+ elements. ([#1499])
+- **`loading_messages` parameter for `SetAssistantThreadsStatus`** - Optional
+ parameter to customize loading state messages. ([#1489])
+- **Attachment image fields** - Added `ImageBytes`, `ImageHeight`, and `ImageWidth`
+ fields to attachments. ([#1516])
+- **`RecordChannel` to conversation properties** - New property for conversation
+ metadata. ([#1513])
+- **Title argument for `CreateChannelCanvas`** - Canvas creation now supports
+ custom titles. ([#1483])
+- **`PostEphemeral` handler for slacktest** - Audit outgoing ephemeral messages
+ in test environments. ([#1517])
+- **`PreviewImageName` for remote files** - Customize preview image filename
+ instead of using the default `preview.jpg`.
+
+### Fixed
+
+- **`PublishView` no longer sends empty hash** - Prevents unnecessary payload
+ when hash is empty. ([#1515])
+- **`ImageBlockElement` validation** - Now properly validates that either
+ `imageURL` or `SlackFile` is provided. ([#1488])
+- **Rich text section channel return** - Correctly returns channel for section
+ channel rich text elements. ([#1472])
+- **`KickUserFromConversation` error handling** - Errors are now properly parsed
+ as a map structure. ([#1471])
+
+### Changed
+
+- **BREAKING**: `GetReactions` now returns `ReactedItem` instead of `[]ItemReaction`.
+ This aligns the response with the actual Slack API, which includes the item itself
+ (message, file, or file_comment) alongside reactions. To migrate, use `resp.Reactions`
+ to access the slice of reactions. ([#1480])
+- **BREAKING**: `Settings` struct fields `Interactivity` and `EventSubscriptions`
+ are now pointers, allowing them to be omitted when empty. ([#1461])
+- Minimum Go version bumped to 1.24. ([#1504])
+
+## [0.17.3] - 2025-07-04
+
+Previous release. See [GitHub releases](https://github.com/slack-go/slack/releases/tag/v0.17.3)
+for details.
+
+[#897]: https://github.com/slack-go/slack/issues/897
+[#1236]: https://github.com/slack-go/slack/issues/1236
+[#1257]: https://github.com/slack-go/slack/issues/1257
+[#1144]: https://github.com/slack-go/slack/issues/1144
+[#1329]: https://github.com/slack-go/slack/issues/1329
+[#1343]: https://github.com/slack-go/slack/issues/1343
+[#1360]: https://github.com/slack-go/slack/issues/1360
+[#1439]: https://github.com/slack-go/slack/pull/1439
+[#1461]: https://github.com/slack-go/slack/pull/1461
+[#1463]: https://github.com/slack-go/slack/pull/1463
+[#1465]: https://github.com/slack-go/slack/pull/1465
+[#1471]: https://github.com/slack-go/slack/pull/1471
+[#1472]: https://github.com/slack-go/slack/pull/1472
+[#1480]: https://github.com/slack-go/slack/pull/1480
+[#1483]: https://github.com/slack-go/slack/pull/1483
+[#1488]: https://github.com/slack-go/slack/pull/1488
+[#1489]: https://github.com/slack-go/slack/pull/1489
+[#1490]: https://github.com/slack-go/slack/pull/1490
+[#1491]: https://github.com/slack-go/slack/issues/1491
+[#1495]: https://github.com/slack-go/slack/pull/1495
+[#1497]: https://github.com/slack-go/slack/pull/1497
+[#1499]: https://github.com/slack-go/slack/pull/1499
+[#1504]: https://github.com/slack-go/slack/pull/1504
+[#1506]: https://github.com/slack-go/slack/pull/1506
+[#1511]: https://github.com/slack-go/slack/pull/1511
+[#1513]: https://github.com/slack-go/slack/pull/1513
+[#1515]: https://github.com/slack-go/slack/pull/1515
+[#1516]: https://github.com/slack-go/slack/pull/1516
+[#1517]: https://github.com/slack-go/slack/pull/1517
+[#1519]: https://github.com/slack-go/slack/pull/1519
+[#1520]: https://github.com/slack-go/slack/pull/1520
+[#1522]: https://github.com/slack-go/slack/pull/1522
+[#1524]: https://github.com/slack-go/slack/pull/1524
+[#1526]: https://github.com/slack-go/slack/pull/1526
+[#1528]: https://github.com/slack-go/slack/pull/1528
+[#1529]: https://github.com/slack-go/slack/pull/1529
+[#1530]: https://github.com/slack-go/slack/pull/1530
+[#1532]: https://github.com/slack-go/slack/pull/1532
+[#1533]: https://github.com/slack-go/slack/pull/1533
+[#1534]: https://github.com/slack-go/slack/pull/1534
+[#1536]: https://github.com/slack-go/slack/pull/1536
+[#596]: https://github.com/slack-go/slack/issues/596
+[#1541]: https://github.com/slack-go/slack/issues/1541
+[#1172]: https://github.com/slack-go/slack/issues/1172
+[#1076]: https://github.com/slack-go/slack/issues/1076
+[#1157]: https://github.com/slack-go/slack/issues/1157
+[#1196]: https://github.com/slack-go/slack/issues/1196
+[#1547]: https://github.com/slack-go/slack/pull/1547
+
+[Unreleased]: https://github.com/slack-go/slack/compare/v0.21.1...HEAD
+[0.21.1]: https://github.com/slack-go/slack/compare/v0.21.0...v0.21.1
+[0.21.0]: https://github.com/slack-go/slack/compare/v0.20.0...v0.21.0
+[0.20.0]: https://github.com/slack-go/slack/compare/v0.19.0...v0.20.0
+[0.19.0]: https://github.com/slack-go/slack/compare/v0.18.0...v0.19.0
+[0.18.0]: https://github.com/slack-go/slack/compare/v0.18.0-rc2...v0.18.0
+[0.18.0-rc2]: https://github.com/slack-go/slack/releases/tag/v0.18.0-rc2
+[0.18.0-rc1]: https://github.com/slack-go/slack/releases/tag/v0.18.0-rc1
+[0.17.3]: https://github.com/slack-go/slack/releases/tag/v0.17.3
diff --git a/backend/vendor/github.com/slack-go/slack/Makefile b/backend/vendor/github.com/slack-go/slack/Makefile
index 72796401..a8104014 100644
--- a/backend/vendor/github.com/slack-go/slack/Makefile
+++ b/backend/vendor/github.com/slack-go/slack/Makefile
@@ -7,7 +7,7 @@ help:
@echo ""
@echo " make deps : Fetch all dependencies"
@echo " make fmt : Run go fmt to fix any formatting issues"
- @echo " make lint : Use go vet to check for linting issues"
+ @echo " make lint : Run golangci-lint for linting issues"
@echo " make test : Run all short tests"
@echo " make test-race : Run all tests with race condition checking"
@echo " make test-integration : Run all tests without limiting to short"
@@ -22,7 +22,7 @@ fmt:
@go fmt .
lint:
- @go vet .
+ @command -v golangci-lint >/dev/null 2>&1 && golangci-lint run ./... || go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 run ./...
test:
@go test -v -count=1 -timeout 300s -short ./...
diff --git a/backend/vendor/github.com/slack-go/slack/README.md b/backend/vendor/github.com/slack-go/slack/README.md
index 127ae253..53dd912d 100644
--- a/backend/vendor/github.com/slack-go/slack/README.md
+++ b/backend/vendor/github.com/slack-go/slack/README.md
@@ -1,9 +1,7 @@
Slack API in Go [](https://pkg.go.dev/github.com/slack-go/slack) [](https://github.com/slack-go/slack/actions/workflows/test.yml)
===============
-This is the original Slack library for Go created by Norberto Lopes, transferred to a GitHub organization.
-
-You can also chat with us on the #slack-go, #slack-go-ja Slack channel on the Gophers Slack.
+You can chat with us on the [#slack-go](https://gophers.slack.com/archives/C02JQ98JHNC), [#slack-go-ja](https://gophers.slack.com/archives/C02HNL8EN3H) Slack channel on the [Gophers Slack](https://gophers.slack.com).

@@ -17,6 +15,11 @@ Therefore, minor version releases may include backward incompatible changes.
See [Releases](https://github.com/slack-go/slack/releases) for more information about the changes.
+## Go Versions supported
+
+We support the same versions of Go as the officially supported Go versions (see [Go
+Release Policy](https://go.dev/doc/devel/release#policy)).
+
## Installing
### *go get*
@@ -29,24 +32,24 @@ See [Releases](https://github.com/slack-go/slack/releases) for more information
```golang
import (
- "fmt"
+ "fmt"
- "github.com/slack-go/slack"
+ "github.com/slack-go/slack"
)
func main() {
- api := slack.New("YOUR_TOKEN_HERE")
- // If you set debugging, it will log all requests to the console
- // Useful when encountering issues
- // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true))
- groups, err := api.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(false))
- if err != nil {
- fmt.Printf("%s\n", err)
- return
- }
- for _, group := range groups {
- fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name)
- }
+ api := slack.New("YOUR_TOKEN_HERE")
+ // If you set debugging, it will log all requests to the console
+ // Useful when encountering issues
+ // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true))
+ groups, err := api.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(false))
+ if err != nil {
+ fmt.Printf("%s\n", err)
+ return
+ }
+ for _, group := range groups {
+ fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name)
+ }
}
```
@@ -63,13 +66,21 @@ func main() {
api := slack.New("YOUR_TOKEN_HERE")
user, err := api.GetUserInfo("U023BECGF")
if err != nil {
- fmt.Printf("%s\n", err)
- return
+ fmt.Printf("%s\n", err)
+ return
}
fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email)
}
```
+### HTTP retries
+
+Retries are off by default. Use **OptionRetry(n)** for 429-only retries, or **OptionRetryConfig(cfg)** for full control (connection, 429, opt-in 5xx). With a custom client, pass retry options after `OptionHTTPClient`. See package `slack` doc for handler details.
+
+```golang
+api := slack.New("YOUR_TOKEN_HERE", slack.OptionRetry(3))
+```
+
## Minimal Socket Mode usage:
See https://github.com/slack-go/slack/blob/master/examples/socketmode/socketmode.go
diff --git a/backend/vendor/github.com/slack-go/slack/admin.go b/backend/vendor/github.com/slack-go/slack/admin.go
index d51426b5..1b0d2178 100644
--- a/backend/vendor/github.com/slack-go/slack/admin.go
+++ b/backend/vendor/github.com/slack-go/slack/admin.go
@@ -59,7 +59,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
- return fmt.Errorf("Failed to invite single-channel guest: %s", err)
+ return fmt.Errorf("failed to invite single-channel guest: %s", err)
}
return nil
@@ -86,7 +86,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
- return fmt.Errorf("Failed to restricted account: %s", err)
+ return fmt.Errorf("failed to restricted account: %s", err)
}
return nil
@@ -110,7 +110,7 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName,
err := api.adminRequest(ctx, "invite", teamName, values)
if err != nil {
- return fmt.Errorf("Failed to invite to team: %s", err)
+ return fmt.Errorf("failed to invite to team: %s", err)
}
return nil
@@ -132,7 +132,7 @@ func (api *Client) SetRegularContext(ctx context.Context, teamName, user string)
err := api.adminRequest(ctx, "setRegular", teamName, values)
if err != nil {
- return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err)
+ return fmt.Errorf("failed to change the user (%s) to a regular user: %s", user, err)
}
return nil
@@ -154,7 +154,7 @@ func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, use
err := api.adminRequest(ctx, "sendSSOBind", teamName, values)
if err != nil {
- return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err)
+ return fmt.Errorf("failed to send SSO binding email for user (%s): %s", user, err)
}
return nil
@@ -177,7 +177,7 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid,
err := api.adminRequest(ctx, "setUltraRestricted", teamName, values)
if err != nil {
- return fmt.Errorf("Failed to ultra-restrict account: %s", err)
+ return fmt.Errorf("failed to ultra-restrict account: %s", err)
}
return nil
diff --git a/backend/vendor/github.com/slack-go/slack/admin_conversations.go b/backend/vendor/github.com/slack-go/slack/admin_conversations.go
index 6f76568b..eba5d98e 100644
--- a/backend/vendor/github.com/slack-go/slack/admin_conversations.go
+++ b/backend/vendor/github.com/slack-go/slack/admin_conversations.go
@@ -2,11 +2,733 @@ package slack
import (
"context"
+ "encoding/json"
"net/url"
"strconv"
"strings"
)
+// AdminConversationsInviteParams contains arguments for AdminConversationsInvite method call.
+type AdminConversationsInviteParams struct {
+ ChannelID string
+ UserIDs []string
+}
+
+// AdminConversationsInvite invites users to a channel.
+// For more information see the admin.conversations.invite docs:
+// https://api.slack.com/methods/admin.conversations.invite
+func (api *Client) AdminConversationsInvite(ctx context.Context, params AdminConversationsInviteParams) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {params.ChannelID},
+ "user_ids": {strings.Join(params.UserIDs, ",")},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.invite", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsArchive archives a public or private channel.
+// For more information see the admin.conversations.archive docs:
+// https://api.slack.com/methods/admin.conversations.archive
+func (api *Client) AdminConversationsArchive(ctx context.Context, channelID string) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.archive", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsUnarchive unarchives a public or private channel.
+// For more information see the admin.conversations.unarchive docs:
+// https://api.slack.com/methods/admin.conversations.unarchive
+func (api *Client) AdminConversationsUnarchive(ctx context.Context, channelID string) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.unarchive", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsRename renames a public or private channel.
+// For more information see the admin.conversations.rename docs:
+// https://api.slack.com/methods/admin.conversations.rename
+func (api *Client) AdminConversationsRename(ctx context.Context, channelID, name string) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ "name": {name},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.rename", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsDelete deletes a public or private channel.
+// For more information see the admin.conversations.delete docs:
+// https://api.slack.com/methods/admin.conversations.delete
+func (api *Client) AdminConversationsDelete(ctx context.Context, channelID string) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.delete", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+type adminConversationsDisconnectSharedParams struct {
+ leavingTeamIDs []string
+}
+
+// AdminConversationsDisconnectSharedOption is an option for AdminConversationsDisconnectShared.
+type AdminConversationsDisconnectSharedOption func(*adminConversationsDisconnectSharedParams)
+
+// AdminConversationsDisconnectSharedOptionLeavingTeamIDs sets the team IDs of the workspaces to disconnect.
+func AdminConversationsDisconnectSharedOptionLeavingTeamIDs(teamIDs []string) AdminConversationsDisconnectSharedOption {
+ return func(params *adminConversationsDisconnectSharedParams) {
+ params.leavingTeamIDs = teamIDs
+ }
+}
+
+// AdminConversationsDisconnectShared disconnects a connected channel from one or more workspaces.
+// For more information see the admin.conversations.disconnectShared docs:
+// https://api.slack.com/methods/admin.conversations.disconnectShared
+func (api *Client) AdminConversationsDisconnectShared(ctx context.Context, channelID string, options ...AdminConversationsDisconnectSharedOption) error {
+ params := adminConversationsDisconnectSharedParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ }
+
+ if len(params.leavingTeamIDs) > 0 {
+ values.Add("leaving_team_ids", strings.Join(params.leavingTeamIDs, ","))
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.disconnectShared", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+type adminConversationsCreateParams struct {
+ description string
+ orgWide bool
+ teamID string
+}
+
+// AdminConversationsCreateOption is an option for AdminConversationsCreate.
+type AdminConversationsCreateOption func(*adminConversationsCreateParams)
+
+// AdminConversationsCreateOptionDescription sets the description of the channel.
+func AdminConversationsCreateOptionDescription(description string) AdminConversationsCreateOption {
+ return func(params *adminConversationsCreateParams) {
+ params.description = description
+ }
+}
+
+// AdminConversationsCreateOptionOrgWide sets whether the channel should be org-wide.
+func AdminConversationsCreateOptionOrgWide(orgWide bool) AdminConversationsCreateOption {
+ return func(params *adminConversationsCreateParams) {
+ params.orgWide = orgWide
+ }
+}
+
+// AdminConversationsCreateOptionTeamID sets the team ID where the channel should be created.
+func AdminConversationsCreateOptionTeamID(teamID string) AdminConversationsCreateOption {
+ return func(params *adminConversationsCreateParams) {
+ params.teamID = teamID
+ }
+}
+
+// AdminConversationsCreateResponse represents the response from admin.conversations.create.
+type AdminConversationsCreateResponse struct {
+ SlackResponse
+ ChannelID string `json:"channel_id"`
+}
+
+// AdminConversationsCreate creates a public or private channel-based conversation.
+// For more information see the admin.conversations.create docs:
+// https://api.slack.com/methods/admin.conversations.create
+func (api *Client) AdminConversationsCreate(ctx context.Context, name string, isPrivate bool, options ...AdminConversationsCreateOption) (string, error) {
+ params := adminConversationsCreateParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ "is_private": {strconv.FormatBool(isPrivate)},
+ "name": {name},
+ }
+
+ if params.description != "" {
+ values.Add("description", params.description)
+ }
+
+ if params.orgWide {
+ values.Add("org_wide", "true")
+ }
+
+ if params.teamID != "" {
+ values.Add("team_id", params.teamID)
+ }
+
+ response := &AdminConversationsCreateResponse{}
+ err := api.postMethod(ctx, "admin.conversations.create", values, response)
+ if err != nil {
+ return "", err
+ }
+
+ return response.ChannelID, response.Err()
+}
+
+// AdminConversationsGetTeamsParams contains arguments for AdminConversationsGetTeams method call.
+type AdminConversationsGetTeamsParams struct {
+ ChannelID string
+ Cursor string
+ Limit int
+}
+
+// AdminConversationsGetTeamsResponse represents the response from admin.conversations.getTeams.
+type AdminConversationsGetTeamsResponse struct {
+ SlackResponse
+ TeamIDs []string `json:"team_ids"`
+}
+
+// AdminConversationsGetTeams gets all the workspaces a given public or private channel is connected to within this Enterprise org.
+// For more information see the admin.conversations.getTeams docs:
+// https://api.slack.com/methods/admin.conversations.getTeams
+func (api *Client) AdminConversationsGetTeams(ctx context.Context, params AdminConversationsGetTeamsParams) ([]string, string, error) {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {params.ChannelID},
+ }
+
+ if params.Cursor != "" {
+ values.Add("cursor", params.Cursor)
+ }
+
+ if params.Limit > 0 {
+ values.Add("limit", strconv.Itoa(params.Limit))
+ }
+
+ response := &AdminConversationsGetTeamsResponse{}
+ err := api.postMethod(ctx, "admin.conversations.getTeams", values, response)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return response.TeamIDs, response.ResponseMetadata.Cursor, response.Err()
+}
+
+type adminConversationsSearchParams struct {
+ cursor string
+ limit int
+ query string
+ searchChannelType []string
+ sort string
+ sortDir string
+ teamIDs []string
+ connectedTeamIDs []string
+ totalCountOnly bool
+}
+
+// AdminConversationsSearchOption is an option for AdminConversationsSearch.
+type AdminConversationsSearchOption func(*adminConversationsSearchParams)
+
+// AdminConversationsSearchOptionCursor sets the cursor for pagination.
+func AdminConversationsSearchOptionCursor(cursor string) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.cursor = cursor
+ }
+}
+
+// AdminConversationsSearchOptionLimit sets the maximum number of results to return.
+func AdminConversationsSearchOptionLimit(limit int) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.limit = limit
+ }
+}
+
+// AdminConversationsSearchOptionQuery sets the search query.
+func AdminConversationsSearchOptionQuery(query string) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.query = query
+ }
+}
+
+// AdminConversationsSearchOptionSearchChannelTypes sets the channel types to search.
+// Valid values: "private", "public", "private_exclude", "multi_workspace", "org_wide", "external_shared_exclude", "external_shared"
+func AdminConversationsSearchOptionSearchChannelTypes(types []string) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.searchChannelType = types
+ }
+}
+
+// AdminConversationsSearchOptionSort sets the sort field.
+// Valid values: "name", "member_count", "created"
+func AdminConversationsSearchOptionSort(sort string) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.sort = sort
+ }
+}
+
+// AdminConversationsSearchOptionSortDir sets the sort direction.
+// Valid values: "asc", "desc"
+func AdminConversationsSearchOptionSortDir(sortDir string) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.sortDir = sortDir
+ }
+}
+
+// AdminConversationsSearchOptionTeamIDs filters results to channels in the specified teams.
+func AdminConversationsSearchOptionTeamIDs(teamIDs []string) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.teamIDs = teamIDs
+ }
+}
+
+// AdminConversationsSearchOptionConnectedTeamIDs filters results to channels connected to the specified teams.
+func AdminConversationsSearchOptionConnectedTeamIDs(teamIDs []string) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.connectedTeamIDs = teamIDs
+ }
+}
+
+// AdminConversationsSearchOptionTotalCountOnly when true, only returns the total count of matching channels.
+func AdminConversationsSearchOptionTotalCountOnly(totalCountOnly bool) AdminConversationsSearchOption {
+ return func(params *adminConversationsSearchParams) {
+ params.totalCountOnly = totalCountOnly
+ }
+}
+
+// ChannelEmailAddress represents an email address associated with a channel.
+type ChannelEmailAddress struct {
+ Address string `json:"address"`
+ CreatorID string `json:"creator_id"`
+ TeamID string `json:"team_id"`
+}
+
+// AdminConversationOwnershipDetail represents ownership details for lists/canvas.
+type AdminConversationOwnershipDetail struct {
+ Count int `json:"count,omitempty"`
+ TeamID string `json:"team_id,omitempty"`
+}
+
+// AdminConversationLists represents lists/canvas information in admin conversations.
+type AdminConversationLists struct {
+ OwnershipDetails []AdminConversationOwnershipDetail `json:"ownership_details,omitempty"`
+ TotalCount int `json:"total_count,omitempty"`
+}
+
+// AdminConversation represents a conversation in admin API responses.
+type AdminConversation struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ Purpose string `json:"purpose,omitempty"`
+ MemberCount int `json:"member_count,omitempty"`
+ Created int64 `json:"created,omitempty"`
+ CreatorID string `json:"creator_id,omitempty"`
+ IsPrivate bool `json:"is_private,omitempty"`
+ IsArchived bool `json:"is_archived,omitempty"`
+ IsGeneral bool `json:"is_general,omitempty"`
+ LastActivityTimestamp int64 `json:"last_activity_ts,omitempty"`
+ IsFrozen bool `json:"is_frozen,omitempty"`
+ IsOrgDefault bool `json:"is_org_default,omitempty"`
+ IsOrgMandatory bool `json:"is_org_mandatory,omitempty"`
+ IsOrgShared bool `json:"is_org_shared,omitempty"`
+ IsExtShared bool `json:"is_ext_shared,omitempty"`
+ IsGlobalShared bool `json:"is_global_shared,omitempty"`
+ IsPendingExtShared bool `json:"is_pending_ext_shared,omitempty"`
+ IsDisconnectInProgress bool `json:"is_disconnect_in_progress,omitempty"`
+ ConnectedTeamIDs []string `json:"connected_team_ids,omitempty"`
+ ConnectedLimitedTeamIDs []string `json:"connected_limited_team_ids,omitempty"`
+ PendingConnectedTeamIDs []string `json:"pending_connected_team_ids,omitempty"`
+ InternalTeamIDs []string `json:"internal_team_ids,omitempty"`
+ InternalTeamIDsCount int `json:"internal_team_ids_count,omitempty"`
+ InternalTeamIDsSampleTeam string `json:"internal_team_ids_sample_team,omitempty"`
+ ContextTeamID string `json:"context_team_id,omitempty"`
+ ConversationHostID string `json:"conversation_host_id,omitempty"`
+ ChannelEmailAddresses []ChannelEmailAddress `json:"channel_email_addresses,omitempty"`
+ ChannelManagerCount int `json:"channel_manager_count,omitempty"`
+ ExternalUserCount int `json:"external_user_count,omitempty"`
+ Canvas *AdminConversationLists `json:"canvas,omitempty"`
+ Lists *AdminConversationLists `json:"lists,omitempty"`
+ Properties *Properties `json:"properties,omitempty"`
+}
+
+// AdminConversationsSearchResponse represents the response from admin.conversations.search.
+type AdminConversationsSearchResponse struct {
+ SlackResponse
+ Conversations []AdminConversation `json:"conversations"`
+ TotalCount int `json:"total_count"`
+ NextCursor string `json:"next_cursor"`
+}
+
+// AdminConversationsSearch searches for public or private channels in an Enterprise organization.
+// For more information see the admin.conversations.search docs:
+// https://api.slack.com/methods/admin.conversations.search
+func (api *Client) AdminConversationsSearch(ctx context.Context, options ...AdminConversationsSearchOption) (*AdminConversationsSearchResponse, error) {
+ params := adminConversationsSearchParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ }
+
+ if params.cursor != "" {
+ values.Add("cursor", params.cursor)
+ }
+
+ if params.limit > 0 {
+ values.Add("limit", strconv.Itoa(params.limit))
+ }
+
+ if params.query != "" {
+ values.Add("query", params.query)
+ }
+
+ if len(params.searchChannelType) > 0 {
+ values.Add("search_channel_types", strings.Join(params.searchChannelType, ","))
+ }
+
+ if params.sort != "" {
+ values.Add("sort", params.sort)
+ }
+
+ if params.sortDir != "" {
+ values.Add("sort_dir", params.sortDir)
+ }
+
+ if len(params.teamIDs) > 0 {
+ values.Add("team_ids", strings.Join(params.teamIDs, ","))
+ }
+
+ if len(params.connectedTeamIDs) > 0 {
+ values.Add("connected_team_ids", strings.Join(params.connectedTeamIDs, ","))
+ }
+
+ if params.totalCountOnly {
+ values.Add("total_count_only", "true")
+ }
+
+ response := &AdminConversationsSearchResponse{}
+ err := api.postMethod(ctx, "admin.conversations.search", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, response.Err()
+}
+
+type adminConversationsLookupParams struct {
+ cursor string
+ limit int
+ maxMemberCount int
+}
+
+// AdminConversationsLookupOption is an option for AdminConversationsLookup.
+type AdminConversationsLookupOption func(*adminConversationsLookupParams)
+
+// AdminConversationsLookupOptionCursor sets the cursor for pagination.
+func AdminConversationsLookupOptionCursor(cursor string) AdminConversationsLookupOption {
+ return func(params *adminConversationsLookupParams) {
+ params.cursor = cursor
+ }
+}
+
+// AdminConversationsLookupOptionLimit sets the maximum number of results to return.
+func AdminConversationsLookupOptionLimit(limit int) AdminConversationsLookupOption {
+ return func(params *adminConversationsLookupParams) {
+ params.limit = limit
+ }
+}
+
+// AdminConversationsLookupOptionMaxMemberCount filters to channels with at most this many members.
+func AdminConversationsLookupOptionMaxMemberCount(maxMemberCount int) AdminConversationsLookupOption {
+ return func(params *adminConversationsLookupParams) {
+ params.maxMemberCount = maxMemberCount
+ }
+}
+
+// AdminConversationsLookupResponse represents the response from admin.conversations.lookup.
+type AdminConversationsLookupResponse struct {
+ SlackResponse
+ Channels []string `json:"channels"`
+}
+
+// AdminConversationsLookup returns channels on the given team matching the specified filters.
+// For more information see the admin.conversations.lookup docs:
+// https://api.slack.com/methods/admin.conversations.lookup
+func (api *Client) AdminConversationsLookup(ctx context.Context, teamIDs []string, lastMessageActivityBefore int64, options ...AdminConversationsLookupOption) ([]string, string, error) {
+ params := adminConversationsLookupParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ "last_message_activity_before": {strconv.FormatInt(lastMessageActivityBefore, 10)},
+ "team_ids": {strings.Join(teamIDs, ",")},
+ }
+
+ if params.cursor != "" {
+ values.Add("cursor", params.cursor)
+ }
+
+ if params.limit > 0 {
+ values.Add("limit", strconv.Itoa(params.limit))
+ }
+
+ if params.maxMemberCount > 0 {
+ values.Add("max_member_count", strconv.Itoa(params.maxMemberCount))
+ }
+
+ response := &AdminConversationsLookupResponse{}
+ err := api.postMethod(ctx, "admin.conversations.lookup", values, response)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return response.Channels, response.ResponseMetadata.Cursor, response.Err()
+}
+
+// AdminConversationsBulkArchive archives public or private channels in bulk.
+// For more information see the admin.conversations.bulkArchive docs:
+// https://api.slack.com/methods/admin.conversations.bulkArchive
+func (api *Client) AdminConversationsBulkArchive(ctx context.Context, channelIDs []string) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_ids": {strings.Join(channelIDs, ",")},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.bulkArchive", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsBulkDelete deletes public or private channels in bulk.
+// For more information see the admin.conversations.bulkDelete docs:
+// https://api.slack.com/methods/admin.conversations.bulkDelete
+func (api *Client) AdminConversationsBulkDelete(ctx context.Context, channelIDs []string) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_ids": {strings.Join(channelIDs, ",")},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.bulkDelete", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsBulkMoveParams contains arguments for AdminConversationsBulkMove method call.
+type AdminConversationsBulkMoveParams struct {
+ ChannelIDs []string
+ TargetTeamID string
+}
+
+// AdminConversationsBulkMove moves public or private channels in bulk.
+// For more information see the admin.conversations.bulkMove docs:
+// https://api.slack.com/methods/admin.conversations.bulkMove
+func (api *Client) AdminConversationsBulkMove(ctx context.Context, params AdminConversationsBulkMoveParams) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_ids": {strings.Join(params.ChannelIDs, ",")},
+ "target_team_id": {params.TargetTeamID},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.bulkMove", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationPrefs represents conversation preferences.
+type AdminConversationPrefs struct {
+ WhoCanPost *AdminConversationPref `json:"who_can_post,omitempty"`
+ CanThread *AdminConversationPref `json:"can_thread,omitempty"`
+ CanHuddle *AdminConversationPref `json:"can_huddle,omitempty"`
+ EnableAtHere *AdminConversationPrefEnabled `json:"enable_at_here,omitempty"`
+ EnableAtChannel *AdminConversationPrefEnabled `json:"enable_at_channel,omitempty"`
+}
+
+// AdminConversationPrefEnabled represents an enabled/disabled preference.
+type AdminConversationPrefEnabled struct {
+ Enabled bool `json:"enabled"`
+}
+
+// AdminConversationPref represents a single conversation preference.
+type AdminConversationPref struct {
+ Type []string `json:"type,omitempty"`
+ User []string `json:"user,omitempty"`
+}
+
+// AdminConversationsGetConversationPrefsResponse represents the response from admin.conversations.getConversationPrefs.
+type AdminConversationsGetConversationPrefsResponse struct {
+ SlackResponse
+ Prefs AdminConversationPrefs `json:"prefs"`
+}
+
+// AdminConversationsGetConversationPrefs gets conversation preferences for a public or private channel.
+// For more information see the admin.conversations.getConversationPrefs docs:
+// https://api.slack.com/methods/admin.conversations.getConversationPrefs
+func (api *Client) AdminConversationsGetConversationPrefs(ctx context.Context, channelID string) (*AdminConversationPrefs, error) {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ }
+
+ response := &AdminConversationsGetConversationPrefsResponse{}
+ err := api.postMethod(ctx, "admin.conversations.getConversationPrefs", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return &response.Prefs, response.Err()
+}
+
+// AdminConversationsSetConversationPrefsParams contains arguments for AdminConversationsSetConversationPrefs method call.
+type AdminConversationsSetConversationPrefsParams struct {
+ ChannelID string
+ Prefs AdminConversationPrefs
+}
+
+// AdminConversationsSetConversationPrefs sets conversation preferences for a public or private channel.
+// For more information see the admin.conversations.setConversationPrefs docs:
+// https://api.slack.com/methods/admin.conversations.setConversationPrefs
+func (api *Client) AdminConversationsSetConversationPrefs(ctx context.Context, params AdminConversationsSetConversationPrefsParams) error {
+ prefsJSON, err := json.Marshal(params.Prefs)
+ if err != nil {
+ return err
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {params.ChannelID},
+ "prefs": {string(prefsJSON)},
+ }
+
+ response := &SlackResponse{}
+ err = api.postMethod(ctx, "admin.conversations.setConversationPrefs", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsGetCustomRetentionResponse represents the response from admin.conversations.getCustomRetention.
+type AdminConversationsGetCustomRetentionResponse struct {
+ SlackResponse
+ DurationDays int `json:"duration_days"`
+ IsPolicyEnabled bool `json:"is_policy_enabled"`
+}
+
+// AdminConversationsGetCustomRetention gets a conversation's custom retention policy.
+// For more information see the admin.conversations.getCustomRetention docs:
+// https://api.slack.com/methods/admin.conversations.getCustomRetention
+func (api *Client) AdminConversationsGetCustomRetention(ctx context.Context, channelID string) (*AdminConversationsGetCustomRetentionResponse, error) {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ }
+
+ response := &AdminConversationsGetCustomRetentionResponse{}
+ err := api.postMethod(ctx, "admin.conversations.getCustomRetention", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, response.Err()
+}
+
+// AdminConversationsSetCustomRetention sets a conversation's custom retention policy.
+// For more information see the admin.conversations.setCustomRetention docs:
+// https://api.slack.com/methods/admin.conversations.setCustomRetention
+func (api *Client) AdminConversationsSetCustomRetention(ctx context.Context, channelID string, durationDays int) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ "duration_days": {strconv.Itoa(durationDays)},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.setCustomRetention", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsRemoveCustomRetention removes a conversation's custom retention policy.
+// For more information see the admin.conversations.removeCustomRetention docs:
+// https://api.slack.com/methods/admin.conversations.removeCustomRetention
+func (api *Client) AdminConversationsRemoveCustomRetention(ctx context.Context, channelID string) error {
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.removeCustomRetention", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
// AdminConversationsSetTeamsParams contains arguments for AdminConversationsSetTeams
// method calls.
type AdminConversationsSetTeamsParams struct {
diff --git a/backend/vendor/github.com/slack-go/slack/admin_conversations_ekm.go b/backend/vendor/github.com/slack-go/slack/admin_conversations_ekm.go
new file mode 100644
index 00000000..f4b4e14e
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/admin_conversations_ekm.go
@@ -0,0 +1,101 @@
+package slack
+
+import (
+ "context"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+type adminConversationsEKMListOriginalConnectedChannelInfoParams struct {
+ channelIDs []string
+ teamIDs []string
+ cursor string
+ limit int
+}
+
+// AdminConversationsEKMListOriginalConnectedChannelInfoOption is an option for
+// AdminConversationsEKMListOriginalConnectedChannelInfo.
+type AdminConversationsEKMListOriginalConnectedChannelInfoOption func(*adminConversationsEKMListOriginalConnectedChannelInfoParams)
+
+// AdminConversationsEKMListOriginalConnectedChannelInfoOptionChannelIDs filters results to specific channels.
+func AdminConversationsEKMListOriginalConnectedChannelInfoOptionChannelIDs(channelIDs []string) AdminConversationsEKMListOriginalConnectedChannelInfoOption {
+ return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) {
+ params.channelIDs = channelIDs
+ }
+}
+
+// AdminConversationsEKMListOriginalConnectedChannelInfoOptionTeamIDs filters results to specific teams.
+func AdminConversationsEKMListOriginalConnectedChannelInfoOptionTeamIDs(teamIDs []string) AdminConversationsEKMListOriginalConnectedChannelInfoOption {
+ return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) {
+ params.teamIDs = teamIDs
+ }
+}
+
+// AdminConversationsEKMListOriginalConnectedChannelInfoOptionCursor sets the cursor for pagination.
+func AdminConversationsEKMListOriginalConnectedChannelInfoOptionCursor(cursor string) AdminConversationsEKMListOriginalConnectedChannelInfoOption {
+ return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) {
+ params.cursor = cursor
+ }
+}
+
+// AdminConversationsEKMListOriginalConnectedChannelInfoOptionLimit sets the maximum number of results to return.
+func AdminConversationsEKMListOriginalConnectedChannelInfoOptionLimit(limit int) AdminConversationsEKMListOriginalConnectedChannelInfoOption {
+ return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) {
+ params.limit = limit
+ }
+}
+
+// AdminConversationsEKMOriginalConnectedChannelInfo represents channel info for EKM response.
+type AdminConversationsEKMOriginalConnectedChannelInfo struct {
+ ID string `json:"id"`
+ OriginalConnectedHostID string `json:"original_connected_host_id"`
+ OriginalConnectedChannelID string `json:"original_connected_channel_id"`
+ InternalTeamIDs []string `json:"internal_team_ids_count"`
+}
+
+// AdminConversationsEKMListOriginalConnectedChannelInfoResponse represents the response from
+// admin.conversations.ekm.listOriginalConnectedChannelInfo.
+type AdminConversationsEKMListOriginalConnectedChannelInfoResponse struct {
+ SlackResponse
+ Channels []AdminConversationsEKMOriginalConnectedChannelInfo `json:"channels"`
+}
+
+// AdminConversationsEKMListOriginalConnectedChannelInfo lists the original connected channel
+// information for Slack Connect channels.
+// For more information see the admin.conversations.ekm.listOriginalConnectedChannelInfo docs:
+// https://api.slack.com/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+func (api *Client) AdminConversationsEKMListOriginalConnectedChannelInfo(ctx context.Context, options ...AdminConversationsEKMListOriginalConnectedChannelInfoOption) (*AdminConversationsEKMListOriginalConnectedChannelInfoResponse, error) {
+ params := adminConversationsEKMListOriginalConnectedChannelInfoParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ }
+
+ if len(params.channelIDs) > 0 {
+ values.Add("channel_ids", strings.Join(params.channelIDs, ","))
+ }
+
+ if len(params.teamIDs) > 0 {
+ values.Add("team_ids", strings.Join(params.teamIDs, ","))
+ }
+
+ if params.cursor != "" {
+ values.Add("cursor", params.cursor)
+ }
+
+ if params.limit > 0 {
+ values.Add("limit", strconv.Itoa(params.limit))
+ }
+
+ response := &AdminConversationsEKMListOriginalConnectedChannelInfoResponse{}
+ err := api.postMethod(ctx, "admin.conversations.ekm.listOriginalConnectedChannelInfo", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, response.Err()
+}
diff --git a/backend/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go b/backend/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go
new file mode 100644
index 00000000..34457997
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go
@@ -0,0 +1,150 @@
+package slack
+
+import (
+ "context"
+ "net/url"
+)
+
+// AdminConversationsRestrictAccessAddGroup
+
+type adminConversationsRestrictAccessAddGroupParams struct {
+ teamID string
+}
+
+// AdminConversationsRestrictAccessAddGroupOption is an option for AdminConversationsRestrictAccessAddGroup.
+type AdminConversationsRestrictAccessAddGroupOption func(*adminConversationsRestrictAccessAddGroupParams)
+
+// AdminConversationsRestrictAccessAddGroupOptionTeamID sets the workspace where the channel exists.
+// Required if using an org token.
+func AdminConversationsRestrictAccessAddGroupOptionTeamID(teamID string) AdminConversationsRestrictAccessAddGroupOption {
+ return func(params *adminConversationsRestrictAccessAddGroupParams) {
+ params.teamID = teamID
+ }
+}
+
+// AdminConversationsRestrictAccessAddGroup adds an allowlist of IDP groups
+// for accessing a channel.
+// For more information see the admin.conversations.restrictAccess.addGroup docs:
+// https://api.slack.com/methods/admin.conversations.restrictAccess.addGroup
+func (api *Client) AdminConversationsRestrictAccessAddGroup(ctx context.Context, channelID, groupID string, options ...AdminConversationsRestrictAccessAddGroupOption) error {
+ params := adminConversationsRestrictAccessAddGroupParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ "group_id": {groupID},
+ }
+
+ if params.teamID != "" {
+ values.Add("team_id", params.teamID)
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.restrictAccess.addGroup", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// AdminConversationsRestrictAccessListGroups
+
+type adminConversationsRestrictAccessListGroupsParams struct {
+ teamID string
+}
+
+// AdminConversationsRestrictAccessListGroupsOption is an option for AdminConversationsRestrictAccessListGroups.
+type AdminConversationsRestrictAccessListGroupsOption func(*adminConversationsRestrictAccessListGroupsParams)
+
+// AdminConversationsRestrictAccessListGroupsOptionTeamID sets the workspace where the channel exists.
+// Required if using an org token.
+func AdminConversationsRestrictAccessListGroupsOptionTeamID(teamID string) AdminConversationsRestrictAccessListGroupsOption {
+ return func(params *adminConversationsRestrictAccessListGroupsParams) {
+ params.teamID = teamID
+ }
+}
+
+// AdminConversationsRestrictAccessListGroupsResponse represents the response from
+// admin.conversations.restrictAccess.listGroups.
+type AdminConversationsRestrictAccessListGroupsResponse struct {
+ SlackResponse
+ GroupIDs []string `json:"group_ids"`
+}
+
+// AdminConversationsRestrictAccessListGroups lists the allowlist of IDP groups
+// for a private channel.
+// For more information see the admin.conversations.restrictAccess.listGroups docs:
+// https://api.slack.com/methods/admin.conversations.restrictAccess.listGroups
+func (api *Client) AdminConversationsRestrictAccessListGroups(ctx context.Context, channelID string, options ...AdminConversationsRestrictAccessListGroupsOption) ([]string, error) {
+ params := adminConversationsRestrictAccessListGroupsParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ }
+
+ if params.teamID != "" {
+ values.Add("team_id", params.teamID)
+ }
+
+ response := &AdminConversationsRestrictAccessListGroupsResponse{}
+ err := api.postMethod(ctx, "admin.conversations.restrictAccess.listGroups", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response.GroupIDs, response.Err()
+}
+
+// AdminConversationsRestrictAccessRemoveGroup
+
+type adminConversationsRestrictAccessRemoveGroupParams struct {
+ teamID string
+}
+
+// AdminConversationsRestrictAccessRemoveGroupOption is an option for AdminConversationsRestrictAccessRemoveGroup.
+type AdminConversationsRestrictAccessRemoveGroupOption func(*adminConversationsRestrictAccessRemoveGroupParams)
+
+// AdminConversationsRestrictAccessRemoveGroupOptionTeamID sets the workspace where the channel exists.
+// Required if using an org token.
+func AdminConversationsRestrictAccessRemoveGroupOptionTeamID(teamID string) AdminConversationsRestrictAccessRemoveGroupOption {
+ return func(params *adminConversationsRestrictAccessRemoveGroupParams) {
+ params.teamID = teamID
+ }
+}
+
+// AdminConversationsRestrictAccessRemoveGroup removes an IDP group from the
+// allowlist of a private channel.
+// For more information see the admin.conversations.restrictAccess.removeGroup docs:
+// https://api.slack.com/methods/admin.conversations.restrictAccess.removeGroup
+func (api *Client) AdminConversationsRestrictAccessRemoveGroup(ctx context.Context, channelID, groupID string, options ...AdminConversationsRestrictAccessRemoveGroupOption) error {
+ params := adminConversationsRestrictAccessRemoveGroupParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ "channel_id": {channelID},
+ "group_id": {groupID},
+ }
+
+ if params.teamID != "" {
+ values.Add("team_id", params.teamID)
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.conversations.restrictAccess.removeGroup", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
diff --git a/backend/vendor/github.com/slack-go/slack/admin_roles.go b/backend/vendor/github.com/slack-go/slack/admin_roles.go
new file mode 100644
index 00000000..676acf0b
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/admin_roles.go
@@ -0,0 +1,204 @@
+package slack
+
+import (
+ "context"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+// AdminRolesAddAssignmentsParams contains arguments for AdminRolesAddAssignments method call.
+type AdminRolesAddAssignmentsParams struct {
+ RoleID string
+ EntityIDs []string
+ UserIDs []string
+}
+
+// AdminRolesRejectedUser represents a user that could not be assigned a role.
+type AdminRolesRejectedUser struct {
+ ID string `json:"id"`
+ Error string `json:"error"`
+}
+
+// AdminRolesRejectedEntity represents an entity that could not be assigned a role.
+type AdminRolesRejectedEntity struct {
+ ID string `json:"id"`
+ Error string `json:"error"`
+}
+
+// AdminRolesAddAssignmentsResponse represents the response from admin.roles.addAssignments.
+type AdminRolesAddAssignmentsResponse struct {
+ SlackResponse
+ RejectedUsers []AdminRolesRejectedUser `json:"rejected_users"`
+ RejectedEntities []AdminRolesRejectedEntity `json:"rejected_entities"`
+}
+
+// AdminRolesAddAssignments adds members to a specified role.
+// For more information see the admin.roles.addAssignments docs:
+// https://api.slack.com/methods/admin.roles.addAssignments
+func (api *Client) AdminRolesAddAssignments(ctx context.Context, params AdminRolesAddAssignmentsParams) (*AdminRolesAddAssignmentsResponse, error) {
+ values := url.Values{
+ "token": {api.token},
+ "role_id": {params.RoleID},
+ }
+
+ if len(params.EntityIDs) > 0 {
+ values.Add("entity_ids", strings.Join(params.EntityIDs, ","))
+ }
+
+ if len(params.UserIDs) > 0 {
+ values.Add("user_ids", strings.Join(params.UserIDs, ","))
+ }
+
+ response := &AdminRolesAddAssignmentsResponse{}
+ err := api.postMethod(ctx, "admin.roles.addAssignments", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, response.Err()
+}
+
+type adminRolesListAssignmentsParams struct {
+ roleIDs []string
+ entityIDs []string
+ limit int
+ cursor string
+ sortDirection string
+}
+
+// AdminRolesListAssignmentsOption is an option for AdminRolesListAssignments.
+type AdminRolesListAssignmentsOption func(*adminRolesListAssignmentsParams)
+
+// AdminRolesListAssignmentsOptionRoleIDs filters results to the specified role IDs.
+func AdminRolesListAssignmentsOptionRoleIDs(roleIDs []string) AdminRolesListAssignmentsOption {
+ return func(params *adminRolesListAssignmentsParams) {
+ params.roleIDs = roleIDs
+ }
+}
+
+// AdminRolesListAssignmentsOptionEntityIDs filters results to the specified entity IDs.
+func AdminRolesListAssignmentsOptionEntityIDs(entityIDs []string) AdminRolesListAssignmentsOption {
+ return func(params *adminRolesListAssignmentsParams) {
+ params.entityIDs = entityIDs
+ }
+}
+
+// AdminRolesListAssignmentsOptionLimit sets the maximum number of results to return.
+func AdminRolesListAssignmentsOptionLimit(limit int) AdminRolesListAssignmentsOption {
+ return func(params *adminRolesListAssignmentsParams) {
+ params.limit = limit
+ }
+}
+
+// AdminRolesListAssignmentsOptionCursor sets the cursor for pagination.
+func AdminRolesListAssignmentsOptionCursor(cursor string) AdminRolesListAssignmentsOption {
+ return func(params *adminRolesListAssignmentsParams) {
+ params.cursor = cursor
+ }
+}
+
+// AdminRolesListAssignmentsOptionSortDir sets the sort direction.
+// Valid values: "asc", "desc".
+func AdminRolesListAssignmentsOptionSortDir(sortDir string) AdminRolesListAssignmentsOption {
+ return func(params *adminRolesListAssignmentsParams) {
+ params.sortDirection = sortDir
+ }
+}
+
+// RoleAssignment represents a single role assignment.
+type RoleAssignment struct {
+ RoleID string `json:"role_id"`
+ EntityID string `json:"entity_id,omitempty"`
+ UserID string `json:"user_id,omitempty"`
+ DateCreate int64 `json:"date_create,omitempty"`
+}
+
+// AdminRolesListAssignmentsResponse represents the response from admin.roles.listAssignments.
+type AdminRolesListAssignmentsResponse struct {
+ SlackResponse
+ RoleAssignments []RoleAssignment `json:"role_assignments"`
+ ResponseMetadata ResponseMetadata `json:"response_metadata"`
+}
+
+// AdminRolesListAssignments lists assignments for roles.
+// For more information see the admin.roles.listAssignments docs:
+// https://api.slack.com/methods/admin.roles.listAssignments
+func (api *Client) AdminRolesListAssignments(ctx context.Context, options ...AdminRolesListAssignmentsOption) (*AdminRolesListAssignmentsResponse, error) {
+ params := adminRolesListAssignmentsParams{}
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ }
+
+ if len(params.roleIDs) > 0 {
+ values.Add("role_ids", strings.Join(params.roleIDs, ","))
+ }
+
+ if len(params.entityIDs) > 0 {
+ values.Add("entity_ids", strings.Join(params.entityIDs, ","))
+ }
+
+ if params.limit > 0 {
+ values.Add("limit", strconv.Itoa(params.limit))
+ }
+
+ if params.cursor != "" {
+ values.Add("cursor", params.cursor)
+ }
+
+ if params.sortDirection != "" {
+ values.Add("sort_dir", params.sortDirection)
+ }
+
+ response := &AdminRolesListAssignmentsResponse{}
+ err := api.postMethod(ctx, "admin.roles.listAssignments", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, response.Err()
+}
+
+// AdminRolesRemoveAssignmentsParams contains arguments for AdminRolesRemoveAssignments method call.
+type AdminRolesRemoveAssignmentsParams struct {
+ RoleID string
+ EntityIDs []string
+ UserIDs []string
+}
+
+// AdminRolesRemoveAssignmentsResponse represents the response from admin.roles.removeAssignments.
+type AdminRolesRemoveAssignmentsResponse struct {
+ SlackResponse
+ RejectedUsers []AdminRolesRejectedUser `json:"rejected_users"`
+ RejectedEntities []AdminRolesRejectedEntity `json:"rejected_entities"`
+}
+
+// AdminRolesRemoveAssignments removes members from a specified role.
+// For more information see the admin.roles.removeAssignments docs:
+// https://api.slack.com/methods/admin.roles.removeAssignments
+func (api *Client) AdminRolesRemoveAssignments(ctx context.Context, params AdminRolesRemoveAssignmentsParams) (*AdminRolesRemoveAssignmentsResponse, error) {
+ values := url.Values{
+ "token": {api.token},
+ "role_id": {params.RoleID},
+ }
+
+ if len(params.EntityIDs) > 0 {
+ values.Add("entity_ids", strings.Join(params.EntityIDs, ","))
+ }
+
+ if len(params.UserIDs) > 0 {
+ values.Add("user_ids", strings.Join(params.UserIDs, ","))
+ }
+
+ response := &AdminRolesRemoveAssignmentsResponse{}
+ err := api.postMethod(ctx, "admin.roles.removeAssignments", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, response.Err()
+}
diff --git a/backend/vendor/github.com/slack-go/slack/admin_teams.go b/backend/vendor/github.com/slack-go/slack/admin_teams.go
new file mode 100644
index 00000000..17ae3df3
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/admin_teams.go
@@ -0,0 +1,153 @@
+package slack
+
+import (
+ "context"
+ "net/url"
+ "strings"
+)
+
+// AdminTeamSettings contains workspace settings returned by admin.teams.settings.info.
+type AdminTeamSettings struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ URL string `json:"url"`
+ Domain string `json:"domain"`
+ EmailDomain string `json:"email_domain"`
+ AvatarBaseURL string `json:"avatar_base_url"`
+ IsVerified bool `json:"is_verified"`
+ Icon TeamSettingsIcon `json:"icon"`
+ EnterpriseID string `json:"enterprise_id"`
+ EnterpriseName string `json:"enterprise_name"`
+ EnterpriseDomain string `json:"enterprise_domain"`
+ DefaultChannels []string `json:"default_channels"`
+}
+
+// TeamSettingsIcon contains team icon URLs and a default flag.
+type TeamSettingsIcon struct {
+ ImageDefault bool `json:"image_default"`
+ Image34 string `json:"image_34"`
+ Image44 string `json:"image_44"`
+ Image68 string `json:"image_68"`
+ Image88 string `json:"image_88"`
+ Image102 string `json:"image_102"`
+ Image132 string `json:"image_132"`
+ Image230 string `json:"image_230"`
+}
+
+// TeamDiscoverability represents the discoverability setting for a workspace.
+type TeamDiscoverability string
+
+const (
+ TeamDiscoverabilityOpen TeamDiscoverability = "open"
+ TeamDiscoverabilityInviteOnly TeamDiscoverability = "invite_only"
+ TeamDiscoverabilityClosed TeamDiscoverability = "closed"
+ TeamDiscoverabilityUnlisted TeamDiscoverability = "unlisted"
+)
+
+type adminTeamSettingsInfoResponse struct {
+ Team AdminTeamSettings `json:"team"`
+ SlackResponse
+}
+
+// AdminTeamsSettingsInfo returns workspace settings.
+// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.info
+func (api *Client) AdminTeamsSettingsInfo(ctx context.Context, teamID string) (*AdminTeamSettings, error) {
+ values := url.Values{
+ "token": {api.token},
+ "team_id": {teamID},
+ }
+
+ response := &adminTeamSettingsInfoResponse{}
+ err := api.postMethod(ctx, "admin.teams.settings.info", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return &response.Team, response.Err()
+}
+
+// AdminTeamsSettingsSetDefaultChannels sets the default channels for a workspace.
+// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+func (api *Client) AdminTeamsSettingsSetDefaultChannels(ctx context.Context, teamID string, channelIDs ...string) error {
+ values := url.Values{
+ "token": {api.token},
+ "team_id": {teamID},
+ "channel_ids": {strings.Join(channelIDs, ",")},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.teams.settings.setDefaultChannels", values, response)
+ if err != nil {
+ return err
+ }
+ return response.Err()
+}
+
+// AdminTeamsSettingsSetDescription sets the description for a workspace.
+// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+func (api *Client) AdminTeamsSettingsSetDescription(ctx context.Context, teamID, description string) error {
+ values := url.Values{
+ "token": {api.token},
+ "team_id": {teamID},
+ "description": {description},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.teams.settings.setDescription", values, response)
+ if err != nil {
+ return err
+ }
+ return response.Err()
+}
+
+// AdminTeamsSettingsSetDiscoverability sets the discoverability for a workspace.
+// The discoverability parameter must be one of: open, invite_only, closed, or unlisted.
+// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+func (api *Client) AdminTeamsSettingsSetDiscoverability(ctx context.Context, teamID string, discoverability TeamDiscoverability) error {
+ values := url.Values{
+ "token": {api.token},
+ "team_id": {teamID},
+ "discoverability": {string(discoverability)},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.teams.settings.setDiscoverability", values, response)
+ if err != nil {
+ return err
+ }
+ return response.Err()
+}
+
+// AdminTeamsSettingsSetIcon sets the icon for a workspace.
+// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+func (api *Client) AdminTeamsSettingsSetIcon(ctx context.Context, teamID, imageURL string) error {
+ values := url.Values{
+ "token": {api.token},
+ "team_id": {teamID},
+ "image_url": {imageURL},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.teams.settings.setIcon", values, response)
+ if err != nil {
+ return err
+ }
+ return response.Err()
+}
+
+// AdminTeamsSettingsSetName sets the name for a workspace.
+// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+func (api *Client) AdminTeamsSettingsSetName(ctx context.Context, teamID, name string) error {
+ values := url.Values{
+ "token": {api.token},
+ "team_id": {teamID},
+ "name": {name},
+ }
+
+ response := &SlackResponse{}
+ err := api.postMethod(ctx, "admin.teams.settings.setName", values, response)
+ if err != nil {
+ return err
+ }
+ return response.Err()
+}
diff --git a/backend/vendor/github.com/slack-go/slack/apps.go b/backend/vendor/github.com/slack-go/slack/apps.go
index 7322b15b..c75569fb 100644
--- a/backend/vendor/github.com/slack-go/slack/apps.go
+++ b/backend/vendor/github.com/slack-go/slack/apps.go
@@ -35,7 +35,7 @@ func (api *Client) ListEventAuthorizationsContext(ctx context.Context, eventCont
"event_context": eventContext,
})
- err := postJSON(ctx, api.httpclient, api.endpoint+"apps.event.authorizations.list", api.appLevelToken, request, &resp, api)
+ err := api.postJSONMethod(ctx, "apps.event.authorizations.list", api.appLevelToken, request, &resp)
if err != nil {
return nil, err
diff --git a/backend/vendor/github.com/slack-go/slack/assistant.go b/backend/vendor/github.com/slack-go/slack/assistant.go
index 8432f89b..d95e68a5 100644
--- a/backend/vendor/github.com/slack-go/slack/assistant.go
+++ b/backend/vendor/github.com/slack-go/slack/assistant.go
@@ -4,13 +4,16 @@ import (
"context"
"encoding/json"
"net/url"
+ "strconv"
+ "strings"
)
// AssistantThreadSetStatusParameters are the parameters for AssistantThreadSetStatus
type AssistantThreadsSetStatusParameters struct {
- ChannelID string `json:"channel_id"`
- Status string `json:"status"`
- ThreadTS string `json:"thread_ts"`
+ ChannelID string `json:"channel_id"`
+ Status string `json:"status"`
+ ThreadTS string `json:"thread_ts"`
+ LoadingMessages []string `json:"loading_messages,omitempty"`
}
// AssistantThreadSetTitleParameters are the parameters for AssistantThreadSetTitle
@@ -34,6 +37,43 @@ type AssistantThreadsPrompt struct {
Message string `json:"message"`
}
+// AssistantSearchContextParameters are the parameters for AssistantSearchContext
+type AssistantSearchContextParameters struct {
+ Query string `json:"query"`
+ ActionToken string `json:"action_token,omitempty"`
+ ChannelTypes []string `json:"channel_types,omitempty"`
+ ContentTypes []string `json:"content_types,omitempty"`
+ ContextChannelID string `json:"context_channel_id,omitempty"`
+ Cursor string `json:"cursor,omitempty"`
+ IncludeBots bool `json:"include_bots,omitempty"`
+ Limit int `json:"limit,omitempty"`
+}
+
+// AssistantSearchContextMessage represents a search result message
+type AssistantSearchContextMessage struct {
+ AuthorUserID string `json:"author_user_id"`
+ TeamID string `json:"team_id"`
+ ChannelID string `json:"channel_id"`
+ MessageTS string `json:"message_ts"`
+ Content string `json:"content"`
+ IsAuthorBot bool `json:"is_author_bot"`
+ Permalink string `json:"permalink"`
+}
+
+// AssistantSearchContextResults contains the search results
+type AssistantSearchContextResults struct {
+ Messages []AssistantSearchContextMessage `json:"messages"`
+}
+
+// AssistantSearchContextResponse is the response from assistant.search.context
+type AssistantSearchContextResponse struct {
+ SlackResponse
+ Results AssistantSearchContextResults `json:"results"`
+ ResponseMetadata struct {
+ NextCursor string `json:"next_cursor"`
+ } `json:"response_metadata"`
+}
+
// AssistantThreadSetSuggestedPrompts sets the suggested prompts for a thread
func (p *AssistantThreadsSetSuggestedPromptsParameters) AddPrompt(title, message string) {
p.Prompts = append(p.Prompts, AssistantThreadsPrompt{
@@ -62,6 +102,10 @@ func (api *Client) SetAssistantThreadsSuggestedPromptsContext(ctx context.Contex
values.Add("channel_id", params.ChannelID)
+ if params.Title != "" {
+ values.Add("title", params.Title)
+ }
+
// Send Prompts as JSON
prompts, err := json.Marshal(params.Prompts)
if err != nil {
@@ -105,6 +149,10 @@ func (api *Client) SetAssistantThreadsStatusContext(ctx context.Context, params
// Always send the status parameter, if empty, it will clear any existing status
values.Add("status", params.Status)
+ if len(params.LoadingMessages) > 0 {
+ values.Add("loading_messages", strings.Join(params.LoadingMessages, ","))
+ }
+
response := struct {
SlackResponse
}{}
@@ -155,3 +203,60 @@ func (api *Client) SetAssistantThreadsTitleContext(ctx context.Context, params A
return response.Err()
}
+
+// SearchAssistantContext searches messages across the Slack organization
+// @see https://api.slack.com/methods/assistant.search.context
+func (api *Client) SearchAssistantContext(params AssistantSearchContextParameters) (*AssistantSearchContextResponse, error) {
+ return api.SearchAssistantContextContext(context.Background(), params)
+}
+
+// SearchAssistantContextContext searches messages across the Slack organization with a custom context
+// @see https://api.slack.com/methods/assistant.search.context
+func (api *Client) SearchAssistantContextContext(ctx context.Context, params AssistantSearchContextParameters) (*AssistantSearchContextResponse, error) {
+ values := url.Values{
+ "token": {api.token},
+ }
+
+ values.Add("query", params.Query)
+
+ if params.ActionToken != "" {
+ values.Add("action_token", params.ActionToken)
+ }
+
+ if len(params.ChannelTypes) > 0 {
+ for _, channelType := range params.ChannelTypes {
+ values.Add("channel_types", channelType)
+ }
+ }
+
+ if len(params.ContentTypes) > 0 {
+ for _, contentType := range params.ContentTypes {
+ values.Add("content_types", contentType)
+ }
+ }
+
+ if params.ContextChannelID != "" {
+ values.Add("context_channel_id", params.ContextChannelID)
+ }
+
+ if params.Cursor != "" {
+ values.Add("cursor", params.Cursor)
+ }
+
+ if params.IncludeBots {
+ values.Add("include_bots", "true")
+ }
+
+ if params.Limit > 0 {
+ values.Add("limit", strconv.Itoa(params.Limit))
+ }
+
+ response := &AssistantSearchContextResponse{}
+
+ err := api.postMethod(ctx, "assistant.search.context", values, response)
+ if err != nil {
+ return nil, err
+ }
+
+ return response, response.Err()
+}
diff --git a/backend/vendor/github.com/slack-go/slack/attachments.go b/backend/vendor/github.com/slack-go/slack/attachments.go
index f4eb9b93..be04e904 100644
--- a/backend/vendor/github.com/slack-go/slack/attachments.go
+++ b/backend/vendor/github.com/slack-go/slack/attachments.go
@@ -77,8 +77,11 @@ type Attachment struct {
Pretext string `json:"pretext,omitempty"`
Text string `json:"text,omitempty"`
- ImageURL string `json:"image_url,omitempty"`
- ThumbURL string `json:"thumb_url,omitempty"`
+ ImageURL string `json:"image_url,omitempty"`
+ ImageBytes int `json:"image_bytes,omitempty"`
+ ImageHeight int `json:"image_height,omitempty"`
+ ImageWidth int `json:"image_width,omitempty"`
+ ThumbURL string `json:"thumb_url,omitempty"`
ServiceName string `json:"service_name,omitempty"`
ServiceIcon string `json:"service_icon,omitempty"`
diff --git a/backend/vendor/github.com/slack-go/slack/audit.go b/backend/vendor/github.com/slack-go/slack/audit.go
index a3ea7ebd..135a68d7 100644
--- a/backend/vendor/github.com/slack-go/slack/audit.go
+++ b/backend/vendor/github.com/slack-go/slack/audit.go
@@ -107,7 +107,8 @@ type AuditLogParameters struct {
func (api *Client) auditLogsRequest(ctx context.Context, path string, values url.Values) (*AuditLogResponse, error) {
response := &AuditLogResponse{}
- err := api.getMethod(ctx, path, api.token, values, response)
+ // The Audit Logs API uses a different base URL (api.slack.com instead of slack.com/api)
+ _, err := getResource(ctx, api.httpclient, api.auditEndpoint+path, api.token, values, response, api)
if err != nil {
return nil, err
}
diff --git a/backend/vendor/github.com/slack-go/slack/block.go b/backend/vendor/github.com/slack-go/slack/block.go
index 7c4f9930..8963c7af 100644
--- a/backend/vendor/github.com/slack-go/slack/block.go
+++ b/backend/vendor/github.com/slack-go/slack/block.go
@@ -5,18 +5,22 @@ package slack
type MessageBlockType string
const (
- MBTSection MessageBlockType = "section"
- MBTDivider MessageBlockType = "divider"
- MBTImage MessageBlockType = "image"
- MBTAction MessageBlockType = "actions"
- MBTContext MessageBlockType = "context"
- MBTFile MessageBlockType = "file"
- MBTInput MessageBlockType = "input"
- MBTHeader MessageBlockType = "header"
- MBTRichText MessageBlockType = "rich_text"
- MBTCall MessageBlockType = "call"
- MBTVideo MessageBlockType = "video"
- MBTMarkdown MessageBlockType = "markdown"
+ MBTSection MessageBlockType = "section"
+ MBTDivider MessageBlockType = "divider"
+ MBTImage MessageBlockType = "image"
+ MBTAction MessageBlockType = "actions"
+ MBTContext MessageBlockType = "context"
+ MBTContextActions MessageBlockType = "context_actions"
+ MBTFile MessageBlockType = "file"
+ MBTInput MessageBlockType = "input"
+ MBTHeader MessageBlockType = "header"
+ MBTRichText MessageBlockType = "rich_text"
+ MBTCall MessageBlockType = "call"
+ MBTVideo MessageBlockType = "video"
+ MBTMarkdown MessageBlockType = "markdown"
+ MBTTable MessageBlockType = "table"
+ MBTTaskCard MessageBlockType = "task_card"
+ MBTPlan MessageBlockType = "plan"
)
// Block defines an interface all block types should implement
diff --git a/backend/vendor/github.com/slack-go/slack/block_call.go b/backend/vendor/github.com/slack-go/slack/block_call.go
index c81dcb8b..621a8b64 100644
--- a/backend/vendor/github.com/slack-go/slack/block_call.go
+++ b/backend/vendor/github.com/slack-go/slack/block_call.go
@@ -7,6 +7,55 @@ type CallBlock struct {
Type MessageBlockType `json:"type"`
BlockID string `json:"block_id,omitempty"`
CallID string `json:"call_id"`
+ // Call is populated by Slack when retrieving messages containing a call block.
+ // When creating a call block to post, only CallID is required.
+ // Note: The structure differs from the Call type used in API responses.
+ Call *CallBlockData `json:"call,omitempty"`
+ APIDecorationAvailable bool `json:"api_decoration_available,omitempty"`
+}
+
+// CallBlockData represents the call data structure as it appears in CallBlocks.
+// This differs from the Call type used in API responses - CallBlock data is nested under V1.
+type CallBlockData struct {
+ V1 *CallBlockDataV1 `json:"v1,omitempty"`
+ MediaBackendType string `json:"media_backend_type,omitempty"`
+}
+
+// CallBlockDataV1 contains the actual call information within a CallBlock.
+type CallBlockDataV1 struct {
+ ID string `json:"id"`
+ AppID string `json:"app_id,omitempty"`
+ AppIconURLs *CallBlockIconURLs `json:"app_icon_urls,omitempty"`
+ DateStart int64 `json:"date_start"`
+ DateEnd int64 `json:"date_end"`
+ ActiveParticipants []CallParticipant `json:"active_participants,omitempty"`
+ AllParticipants []CallParticipant `json:"all_participants,omitempty"`
+ DisplayID string `json:"display_id,omitempty"`
+ JoinURL string `json:"join_url,omitempty"`
+ DesktopAppJoinURL string `json:"desktop_app_join_url,omitempty"`
+ Name string `json:"name,omitempty"`
+ CreatedBy string `json:"created_by,omitempty"`
+ Channels []string `json:"channels,omitempty"`
+ IsDMCall bool `json:"is_dm_call"`
+ WasRejected bool `json:"was_rejected"`
+ WasMissed bool `json:"was_missed"`
+ WasAccepted bool `json:"was_accepted"`
+ HasEnded bool `json:"has_ended"`
+}
+
+// CallBlockIconURLs contains app icon URLs at various sizes for a call integration.
+type CallBlockIconURLs struct {
+ Image32 string `json:"image_32,omitempty"`
+ Image36 string `json:"image_36,omitempty"`
+ Image48 string `json:"image_48,omitempty"`
+ Image64 string `json:"image_64,omitempty"`
+ Image72 string `json:"image_72,omitempty"`
+ Image96 string `json:"image_96,omitempty"`
+ Image128 string `json:"image_128,omitempty"`
+ Image192 string `json:"image_192,omitempty"`
+ Image512 string `json:"image_512,omitempty"`
+ Image1024 string `json:"image_1024,omitempty"`
+ ImageOriginal string `json:"image_original,omitempty"`
}
// BlockType returns the type of the block
@@ -19,10 +68,26 @@ func (s CallBlock) ID() string {
return s.BlockID
}
+// CallBlockOption allows configuration of options for a new call block
+type CallBlockOption func(*CallBlock)
+
+// CallBlockOptionBlockID sets the block_id for the call block
+func CallBlockOptionBlockID(blockID string) CallBlockOption {
+ return func(block *CallBlock) {
+ block.BlockID = blockID
+ }
+}
+
// NewCallBlock returns a new instance of a call block
-func NewCallBlock(callID string) *CallBlock {
- return &CallBlock{
+func NewCallBlock(callID string, options ...CallBlockOption) *CallBlock {
+ block := &CallBlock{
Type: MBTCall,
CallID: callID,
}
+
+ for _, option := range options {
+ option(block)
+ }
+
+ return block
}
diff --git a/backend/vendor/github.com/slack-go/slack/block_context_actions.go b/backend/vendor/github.com/slack-go/slack/block_context_actions.go
new file mode 100644
index 00000000..d1cf532c
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/block_context_actions.go
@@ -0,0 +1,31 @@
+package slack
+
+// ContextActionsBlock defines data that is used to hold interactive action elements.
+//
+// More Information: https://docs.slack.dev/reference/block-kit/blocks/context-actions-block/
+type ContextActionsBlock struct {
+ Type MessageBlockType `json:"type"`
+ BlockID string `json:"block_id,omitempty"`
+ Elements *BlockElements `json:"elements"`
+}
+
+// BlockType returns the type of the block
+func (s ContextActionsBlock) BlockType() MessageBlockType {
+ return s.Type
+}
+
+// ID returns the ID of the block
+func (s ContextActionsBlock) ID() string {
+ return s.BlockID
+}
+
+// NewContextActionsBlock returns a new instance of a Context Actions Block
+func NewContextActionsBlock(blockID string, elements ...BlockElement) *ContextActionsBlock {
+ return &ContextActionsBlock{
+ Type: MBTContextActions,
+ BlockID: blockID,
+ Elements: &BlockElements{
+ ElementSet: elements,
+ },
+ }
+}
diff --git a/backend/vendor/github.com/slack-go/slack/block_conv.go b/backend/vendor/github.com/slack-go/slack/block_conv.go
index 7df9b107..a792f26e 100644
--- a/backend/vendor/github.com/slack-go/slack/block_conv.go
+++ b/backend/vendor/github.com/slack-go/slack/block_conv.go
@@ -53,6 +53,8 @@ func (b *Blocks) UnmarshalJSON(data []byte) error {
block = &ActionBlock{}
case "context":
block = &ContextBlock{}
+ case "context_actions":
+ block = &ContextActionsBlock{}
case "divider":
block = &DividerBlock{}
case "file":
@@ -75,8 +77,19 @@ func (b *Blocks) UnmarshalJSON(data []byte) error {
block = &CallBlock{}
case "video":
block = &VideoBlock{}
+ case "table":
+ block = &TableBlock{}
+ case "task_card":
+ block = &TaskCardBlock{}
+ case "plan":
+ block = &PlanBlock{}
default:
- block = &UnknownBlock{}
+ b := &UnknownBlock{raw: r}
+ if err = json.Unmarshal(r, b); err != nil {
+ return err
+ }
+ blocks.BlockSet = append(blocks.BlockSet, b)
+ continue
}
err = json.Unmarshal(r, block)
@@ -141,6 +154,12 @@ func (b *InputBlock) UnmarshalJSON(data []byte) error {
e = &NumberInputBlockElement{}
case "file_input":
e = &FileInputBlockElement{}
+ case "feedback_buttons":
+ e = &FeedbackButtonsBlockElement{}
+ case "icon_button":
+ e = &IconButtonBlockElement{}
+ case "workflow_button":
+ e = &WorkflowButtonBlockElement{}
default:
return fmt.Errorf("unsupported block element type %v", s.TypeVal)
}
@@ -219,8 +238,18 @@ func (b *BlockElements) UnmarshalJSON(data []byte) error {
blockElement = &RadioButtonsBlockElement{}
case "static_select", "external_select", "users_select", "conversations_select", "channels_select":
blockElement = &SelectBlockElement{}
+ case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select":
+ blockElement = &MultiSelectBlockElement{}
case "number_input":
blockElement = &NumberInputBlockElement{}
+ case "file_input":
+ blockElement = &FileInputBlockElement{}
+ case "feedback_buttons":
+ blockElement = &FeedbackButtonsBlockElement{}
+ case "icon_button":
+ blockElement = &IconButtonBlockElement{}
+ case "workflow_button":
+ blockElement = &WorkflowButtonBlockElement{}
default:
return fmt.Errorf("unsupported block element type %v", blockElementType)
}
@@ -341,6 +370,12 @@ func (a *Accessory) UnmarshalJSON(data []byte) error {
return err
}
a.CheckboxGroupsBlockElement = element.(*CheckboxGroupsBlockElement)
+ case "workflow_button":
+ element, err := unmarshalBlockElement(r, &WorkflowButtonBlockElement{})
+ if err != nil {
+ return err
+ }
+ a.WorkflowButtonElement = element.(*WorkflowButtonBlockElement)
default:
element, err := unmarshalBlockElement(r, &UnknownBlockElement{})
if err != nil {
@@ -391,6 +426,12 @@ func toBlockElement(element *Accessory) BlockElement {
if element.MultiSelectElement != nil {
return element.MultiSelectElement
}
+ if element.RichTextInputElement != nil {
+ return element.RichTextInputElement
+ }
+ if element.WorkflowButtonElement != nil {
+ return element.WorkflowButtonElement
+ }
return nil
}
diff --git a/backend/vendor/github.com/slack-go/slack/block_element.go b/backend/vendor/github.com/slack-go/slack/block_element.go
index 2b32d331..12849340 100644
--- a/backend/vendor/github.com/slack-go/slack/block_element.go
+++ b/backend/vendor/github.com/slack-go/slack/block_element.go
@@ -3,20 +3,23 @@ package slack
// https://api.slack.com/reference/messaging/block-elements
const (
- METCheckboxGroups MessageElementType = "checkboxes"
- METImage MessageElementType = "image"
- METButton MessageElementType = "button"
- METOverflow MessageElementType = "overflow"
- METDatepicker MessageElementType = "datepicker"
- METTimepicker MessageElementType = "timepicker"
- METDatetimepicker MessageElementType = "datetimepicker"
- METPlainTextInput MessageElementType = "plain_text_input"
- METRadioButtons MessageElementType = "radio_buttons"
- METRichTextInput MessageElementType = "rich_text_input"
- METEmailTextInput MessageElementType = "email_text_input"
- METURLTextInput MessageElementType = "url_text_input"
- METNumber MessageElementType = "number_input"
- METFileInput MessageElementType = "file_input"
+ METCheckboxGroups MessageElementType = "checkboxes"
+ METImage MessageElementType = "image"
+ METButton MessageElementType = "button"
+ METOverflow MessageElementType = "overflow"
+ METDatepicker MessageElementType = "datepicker"
+ METTimepicker MessageElementType = "timepicker"
+ METDatetimepicker MessageElementType = "datetimepicker"
+ METPlainTextInput MessageElementType = "plain_text_input"
+ METRadioButtons MessageElementType = "radio_buttons"
+ METRichTextInput MessageElementType = "rich_text_input"
+ METEmailTextInput MessageElementType = "email_text_input"
+ METURLTextInput MessageElementType = "url_text_input"
+ METNumber MessageElementType = "number_input"
+ METFileInput MessageElementType = "file_input"
+ METFeedbackButtons MessageElementType = "feedback_buttons"
+ METIconButton MessageElementType = "icon_button"
+ METWorkflowButton MessageElementType = "workflow_button"
MixedElementImage MixedElementType = "mixed_image"
MixedElementText MixedElementType = "mixed_text"
@@ -58,34 +61,37 @@ type Accessory struct {
SelectElement *SelectBlockElement
MultiSelectElement *MultiSelectBlockElement
CheckboxGroupsBlockElement *CheckboxGroupsBlockElement
+ WorkflowButtonElement *WorkflowButtonBlockElement
UnknownElement *UnknownBlockElement
}
// NewAccessory returns a new Accessory for a given block element
func NewAccessory(element BlockElement) *Accessory {
- switch element.(type) {
+ switch element := element.(type) {
case *ImageBlockElement:
- return &Accessory{ImageElement: element.(*ImageBlockElement)}
+ return &Accessory{ImageElement: element}
case *ButtonBlockElement:
- return &Accessory{ButtonElement: element.(*ButtonBlockElement)}
+ return &Accessory{ButtonElement: element}
case *OverflowBlockElement:
- return &Accessory{OverflowElement: element.(*OverflowBlockElement)}
+ return &Accessory{OverflowElement: element}
case *DatePickerBlockElement:
- return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)}
+ return &Accessory{DatePickerElement: element}
case *TimePickerBlockElement:
- return &Accessory{TimePickerElement: element.(*TimePickerBlockElement)}
+ return &Accessory{TimePickerElement: element}
case *PlainTextInputBlockElement:
- return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)}
+ return &Accessory{PlainTextInputElement: element}
case *RichTextInputBlockElement:
- return &Accessory{RichTextInputElement: element.(*RichTextInputBlockElement)}
+ return &Accessory{RichTextInputElement: element}
case *RadioButtonsBlockElement:
- return &Accessory{RadioButtonsElement: element.(*RadioButtonsBlockElement)}
+ return &Accessory{RadioButtonsElement: element}
case *SelectBlockElement:
- return &Accessory{SelectElement: element.(*SelectBlockElement)}
+ return &Accessory{SelectElement: element}
case *MultiSelectBlockElement:
- return &Accessory{MultiSelectElement: element.(*MultiSelectBlockElement)}
+ return &Accessory{MultiSelectElement: element}
case *CheckboxGroupsBlockElement:
- return &Accessory{CheckboxGroupsBlockElement: element.(*CheckboxGroupsBlockElement)}
+ return &Accessory{CheckboxGroupsBlockElement: element}
+ case *WorkflowButtonBlockElement:
+ return &Accessory{WorkflowButtonElement: element}
default:
return &Accessory{UnknownElement: element.(*UnknownBlockElement)}
}
@@ -118,7 +124,7 @@ func (s UnknownBlockElement) ElementType() MessageElementType {
// More Information: https://api.slack.com/reference/messaging/block-elements#image
type ImageBlockElement struct {
Type MessageElementType `json:"type"`
- ImageURL string `json:"image_url"`
+ ImageURL *string `json:"image_url,omitempty"`
AltText string `json:"alt_text"`
SlackFile *SlackFileObject `json:"slack_file,omitempty"`
}
@@ -136,7 +142,7 @@ func (s ImageBlockElement) MixedElementType() MixedElementType {
func NewImageBlockElement(imageURL, altText string) *ImageBlockElement {
return &ImageBlockElement{
Type: METImage,
- ImageURL: imageURL,
+ ImageURL: &imageURL,
AltText: altText,
}
}
@@ -242,6 +248,7 @@ type SelectBlockElement struct {
Filter *SelectBlockElementFilter `json:"filter,omitempty"`
MinQueryLength *int `json:"min_query_length,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
+ FocusOnLoad bool `json:"focus_on_load,omitempty"`
}
// SelectBlockElementFilter allows to filter select element conversation options by type.
@@ -333,6 +340,7 @@ type MultiSelectBlockElement struct {
MinQueryLength *int `json:"min_query_length,omitempty"`
MaxSelectedItems *int `json:"max_selected_items,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
+ FocusOnLoad bool `json:"focus_on_load,omitempty"`
}
// ElementType returns the type of the Element
@@ -453,6 +461,7 @@ type DatePickerBlockElement struct {
Placeholder *TextBlockObject `json:"placeholder,omitempty"`
InitialDate string `json:"initial_date,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
+ FocusOnLoad bool `json:"focus_on_load,omitempty"`
}
// ElementType returns the type of the Element
@@ -480,6 +489,7 @@ type TimePickerBlockElement struct {
InitialTime string `json:"initial_time,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
Timezone string `json:"timezone,omitempty"`
+ FocusOnLoad bool `json:"focus_on_load,omitempty"`
}
// ElementType returns the type of the Element
@@ -591,6 +601,7 @@ type PlainTextInputBlockElement struct {
MinLength int `json:"min_length,omitempty"`
MaxLength int `json:"max_length,omitempty"`
DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"`
+ FocusOnLoad bool `json:"focus_on_load,omitempty"`
}
type DispatchActionConfig struct {
@@ -678,6 +689,7 @@ type CheckboxGroupsBlockElement struct {
Options []*OptionBlockObject `json:"options"`
InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
+ FocusOnLoad bool `json:"focus_on_load,omitempty"`
}
// ElementType returns the type of the Element
@@ -704,6 +716,7 @@ type RadioButtonsBlockElement struct {
Options []*OptionBlockObject `json:"options"`
InitialOption *OptionBlockObject `json:"initial_option,omitempty"`
Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
+ FocusOnLoad bool `json:"focus_on_load,omitempty"`
}
// ElementType returns the type of the Element
@@ -734,6 +747,7 @@ type NumberInputBlockElement struct {
MinValue string `json:"min_value,omitempty"`
MaxValue string `json:"max_value,omitempty"`
DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"`
+ FocusOnLoad bool `json:"focus_on_load,omitempty"`
}
// ElementType returns the type of the Element
@@ -811,3 +825,170 @@ func (s *FileInputBlockElement) WithMaxFiles(maxFiles int) *FileInputBlockElemen
s.MaxFiles = maxFiles
return s
}
+
+// FeedbackButton defines a button within a feedback buttons element
+type FeedbackButton struct {
+ Text *TextBlockObject `json:"text"`
+ Value string `json:"value"`
+ AccessibilityLabel string `json:"accessibility_label,omitempty"`
+}
+
+// NewFeedbackButton returns a new instance of a feedback button
+func NewFeedbackButton(text *TextBlockObject, value string) *FeedbackButton {
+ return &FeedbackButton{
+ Text: text,
+ Value: value,
+ }
+}
+
+// WithAccessibilityLabel sets the accessibility label for the feedback button
+func (fb *FeedbackButton) WithAccessibilityLabel(label string) *FeedbackButton {
+ fb.AccessibilityLabel = label
+ return fb
+}
+
+// FeedbackButtonsBlockElement defines an element that provides positive/negative feedback options
+//
+// More Information: https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element
+type FeedbackButtonsBlockElement struct {
+ Type MessageElementType `json:"type"`
+ ActionID string `json:"action_id,omitempty"`
+ PositiveButton *FeedbackButton `json:"positive_button"`
+ NegativeButton *FeedbackButton `json:"negative_button"`
+}
+
+// ElementType returns the type of the element
+func (s FeedbackButtonsBlockElement) ElementType() MessageElementType {
+ return s.Type
+}
+
+// NewFeedbackButtonsBlockElement returns a new instance of a feedback buttons element
+func NewFeedbackButtonsBlockElement(actionID string, positiveButton, negativeButton *FeedbackButton) *FeedbackButtonsBlockElement {
+ return &FeedbackButtonsBlockElement{
+ Type: METFeedbackButtons,
+ ActionID: actionID,
+ PositiveButton: positiveButton,
+ NegativeButton: negativeButton,
+ }
+}
+
+// WithPositiveButton sets the positive button for the feedback buttons element
+func (s *FeedbackButtonsBlockElement) WithPositiveButton(button *FeedbackButton) *FeedbackButtonsBlockElement {
+ s.PositiveButton = button
+ return s
+}
+
+// WithNegativeButton sets the negative button for the feedback buttons element
+func (s *FeedbackButtonsBlockElement) WithNegativeButton(button *FeedbackButton) *FeedbackButtonsBlockElement {
+ s.NegativeButton = button
+ return s
+}
+
+// IconButtonBlockElement defines an element that displays icon-based interactive buttons
+//
+// More Information: https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element
+type IconButtonBlockElement struct {
+ Type MessageElementType `json:"type"`
+ Icon string `json:"icon"`
+ Text *TextBlockObject `json:"text"`
+ ActionID string `json:"action_id,omitempty"`
+ Value string `json:"value,omitempty"`
+ Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
+ AccessibilityLabel string `json:"accessibility_label,omitempty"`
+ VisibleToUserIDs []string `json:"visible_to_user_ids,omitempty"`
+}
+
+// ElementType returns the type of the element
+func (s IconButtonBlockElement) ElementType() MessageElementType {
+ return s.Type
+}
+
+// NewIconButtonBlockElement returns a new instance of an icon button element
+func NewIconButtonBlockElement(icon string, text *TextBlockObject, actionID string) *IconButtonBlockElement {
+ return &IconButtonBlockElement{
+ Type: METIconButton,
+ Icon: icon,
+ Text: text,
+ ActionID: actionID,
+ }
+}
+
+// WithValue sets the value for the icon button element
+func (s *IconButtonBlockElement) WithValue(value string) *IconButtonBlockElement {
+ s.Value = value
+ return s
+}
+
+// WithConfirm sets the confirmation dialog for the icon button element
+func (s *IconButtonBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *IconButtonBlockElement {
+ s.Confirm = confirm
+ return s
+}
+
+// WithAccessibilityLabel sets the accessibility label for the icon button element
+func (s *IconButtonBlockElement) WithAccessibilityLabel(label string) *IconButtonBlockElement {
+ s.AccessibilityLabel = label
+ return s
+}
+
+// WithVisibleToUserIDs sets the user IDs who can see the icon button element
+func (s *IconButtonBlockElement) WithVisibleToUserIDs(userIDs []string) *IconButtonBlockElement {
+ s.VisibleToUserIDs = userIDs
+ return s
+}
+
+// WorkflowTrigger defines the workflow to be executed when a workflow button is clicked
+type WorkflowTrigger struct {
+ URL string `json:"url"`
+ CustomizableInputParameters []CustomizableInputParameter `json:"customizable_input_parameters,omitempty"`
+}
+
+// CustomizableInputParameter defines a parameter that can be passed to a workflow
+type CustomizableInputParameter struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+}
+
+// Workflow contains the trigger details for a workflow button
+type Workflow struct {
+ Trigger *WorkflowTrigger `json:"trigger"`
+}
+
+// WorkflowButtonBlockElement defines an element that triggers a workflow when clicked
+//
+// More Information: https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element
+type WorkflowButtonBlockElement struct {
+ Type MessageElementType `json:"type"`
+ Text *TextBlockObject `json:"text"`
+ Workflow *Workflow `json:"workflow"`
+ ActionID string `json:"action_id"`
+ Style Style `json:"style,omitempty"`
+ AccessibilityLabel string `json:"accessibility_label,omitempty"`
+}
+
+// ElementType returns the type of the element
+func (s WorkflowButtonBlockElement) ElementType() MessageElementType {
+ return s.Type
+}
+
+// NewWorkflowButtonBlockElement returns a new instance of a workflow button element
+func NewWorkflowButtonBlockElement(text *TextBlockObject, workflow *Workflow, actionID string) *WorkflowButtonBlockElement {
+ return &WorkflowButtonBlockElement{
+ Type: METWorkflowButton,
+ Text: text,
+ Workflow: workflow,
+ ActionID: actionID,
+ }
+}
+
+// WithStyle sets the style for the workflow button element
+func (s *WorkflowButtonBlockElement) WithStyle(style Style) *WorkflowButtonBlockElement {
+ s.Style = style
+ return s
+}
+
+// WithAccessibilityLabel sets the accessibility label for the workflow button element
+func (s *WorkflowButtonBlockElement) WithAccessibilityLabel(label string) *WorkflowButtonBlockElement {
+ s.AccessibilityLabel = label
+ return s
+}
diff --git a/backend/vendor/github.com/slack-go/slack/block_header.go b/backend/vendor/github.com/slack-go/slack/block_header.go
index 3afb4c95..4277057f 100644
--- a/backend/vendor/github.com/slack-go/slack/block_header.go
+++ b/backend/vendor/github.com/slack-go/slack/block_header.go
@@ -36,7 +36,9 @@ func NewHeaderBlock(textObj *TextBlockObject, options ...HeaderBlockOption) *Hea
}
for _, option := range options {
- option(&block)
+ if option != nil {
+ option(&block)
+ }
}
return &block
diff --git a/backend/vendor/github.com/slack-go/slack/block_json.go b/backend/vendor/github.com/slack-go/slack/block_json.go
new file mode 100644
index 00000000..43798f51
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/block_json.go
@@ -0,0 +1,110 @@
+package slack
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// RawJSONBlock represents a block created from raw JSON that preserves
+// the original JSON structure. This is useful for testing new Slack block types
+// before the library has full support, or for using blocks copied from Block Kit Builder.
+//
+// The block stores the original JSON and outputs it unchanged during marshalling,
+// ensuring no data is lost through the unmarshal/marshal cycle.
+type RawJSONBlock struct {
+ Type MessageBlockType `json:"-"`
+ BlockID string `json:"-"`
+ raw json.RawMessage
+}
+
+// BlockType returns the type of the block
+func (r RawJSONBlock) BlockType() MessageBlockType {
+ return r.Type
+}
+
+// ID returns the block_id of the block
+func (r RawJSONBlock) ID() string {
+ return r.BlockID
+}
+
+// MarshalJSON outputs the original JSON unchanged
+func (r RawJSONBlock) MarshalJSON() ([]byte, error) {
+ return r.raw, nil
+}
+
+// BlockFromJSON creates a RawJSONBlock from a JSON string that preserves
+// the original JSON. This is useful for quickly testing blocks from Slack's
+// Block Kit Builder or for incorporating new block types before the library
+// has full support.
+//
+// The JSON can be either a single block object or an array of blocks.
+// If an array is provided, only the first block is returned.
+//
+// The returned block stores the original JSON and outputs it unchanged during
+// marshalling, ensuring no data is lost.
+//
+// Returns an error if the JSON is invalid, empty, or missing required fields.
+//
+// Example:
+//
+// block, err := slack.BlockFromJSON(`{"type": "section", "text": {"type": "mrkdwn", "text": "Hello"}}`)
+// if err != nil {
+// return err
+// }
+// blocks = append(blocks, block)
+func BlockFromJSON(jsonStr string) (Block, error) {
+ var rawJSON json.RawMessage
+ var isArray bool
+
+ // Try to unmarshal as an array first
+ var arrayTest []json.RawMessage
+ if err := json.Unmarshal([]byte(jsonStr), &arrayTest); err == nil && len(arrayTest) > 0 {
+ rawJSON = arrayTest[0]
+ isArray = true
+ } else {
+ // Try as a single block object
+ if err := json.Unmarshal([]byte(jsonStr), &rawJSON); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal block JSON: %w", err)
+ }
+ isArray = false
+ }
+
+ if !isArray && len(rawJSON) == 0 {
+ return nil, fmt.Errorf("no blocks found in JSON")
+ }
+
+ // Extract minimal fields for Block interface
+ var minimal struct {
+ Type string `json:"type"`
+ BlockID string `json:"block_id"`
+ }
+ if err := json.Unmarshal(rawJSON, &minimal); err != nil {
+ return nil, fmt.Errorf("failed to extract block type: %w", err)
+ }
+
+ if minimal.Type == "" {
+ return nil, fmt.Errorf("block missing required 'type' field")
+ }
+
+ return RawJSONBlock{
+ Type: MessageBlockType(minimal.Type),
+ BlockID: minimal.BlockID,
+ raw: rawJSON,
+ }, nil
+}
+
+// MustBlockFromJSON creates a Block from a JSON string and panics if there's an error.
+// This is primarily intended for use in tests or examples where the JSON is known to be valid.
+// For production code, use BlockFromJSON which returns an error instead.
+//
+// Example:
+//
+// block := slack.MustBlockFromJSON(`{"type": "divider"}`)
+// msg := slack.NewBlockMessage(block)
+func MustBlockFromJSON(jsonStr string) Block {
+ block, err := BlockFromJSON(jsonStr)
+ if err != nil {
+ panic(fmt.Sprintf("MustBlockFromJSON: %v", err))
+ }
+ return block
+}
diff --git a/backend/vendor/github.com/slack-go/slack/block_object.go b/backend/vendor/github.com/slack-go/slack/block_object.go
index fd73b6c4..c1b64cc4 100644
--- a/backend/vendor/github.com/slack-go/slack/block_object.go
+++ b/backend/vendor/github.com/slack-go/slack/block_object.go
@@ -11,7 +11,6 @@ import (
// BlockObject defines an interface that all block object types should
// implement.
-// @TODO: Is this interface needed?
// blockObject object types
const (
diff --git a/backend/vendor/github.com/slack-go/slack/block_plan.go b/backend/vendor/github.com/slack-go/slack/block_plan.go
new file mode 100644
index 00000000..c6e41749
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/block_plan.go
@@ -0,0 +1,55 @@
+package slack
+
+// PlanBlock defines a block of type plan used by AI agents
+// to group multiple task cards under a shared title.
+//
+// More Information: https://docs.slack.dev/reference/block-kit/blocks/plan-block/
+type PlanBlock struct {
+ Type MessageBlockType `json:"type"`
+ BlockID string `json:"block_id,omitempty"`
+ Title string `json:"title"`
+ Tasks []TaskCardBlock `json:"tasks,omitempty"`
+}
+
+// BlockType returns the type of the block
+func (s PlanBlock) BlockType() MessageBlockType {
+ return s.Type
+}
+
+// ID returns the ID of the block
+func (s PlanBlock) ID() string {
+ return s.BlockID
+}
+
+// PlanBlockOption allows configuration of options for a new plan block
+type PlanBlockOption func(*PlanBlock)
+
+// PlanBlockOptionBlockID sets the block ID for the plan block
+func PlanBlockOptionBlockID(blockID string) PlanBlockOption {
+ return func(block *PlanBlock) {
+ block.BlockID = blockID
+ }
+}
+
+// NewPlanBlock returns a new instance of a plan block
+func NewPlanBlock(title string, options ...PlanBlockOption) *PlanBlock {
+ block := PlanBlock{
+ Type: MBTPlan,
+ Title: title,
+ }
+
+ for _, option := range options {
+ option(&block)
+ }
+
+ return &block
+}
+
+// WithTasks sets the tasks for the PlanBlock
+func (s *PlanBlock) WithTasks(tasks ...*TaskCardBlock) *PlanBlock {
+ s.Tasks = make([]TaskCardBlock, len(tasks))
+ for i, t := range tasks {
+ s.Tasks[i] = *t
+ }
+ return s
+}
diff --git a/backend/vendor/github.com/slack-go/slack/block_rich_text.go b/backend/vendor/github.com/slack-go/slack/block_rich_text.go
index 73233d23..b0bc87b9 100644
--- a/backend/vendor/github.com/slack-go/slack/block_rich_text.go
+++ b/backend/vendor/github.com/slack-go/slack/block_rich_text.go
@@ -103,6 +103,10 @@ func (u RichTextUnknown) RichTextElementType() RichTextElementType {
return u.Type
}
+func (u RichTextUnknown) MarshalJSON() ([]byte, error) {
+ return []byte(u.Raw), nil
+}
+
type RichTextListElementType string
const (
@@ -129,7 +133,7 @@ func NewRichTextList(style RichTextListElementType, indent int, elements ...Rich
}
}
-// ElementType returns the type of the Element
+// RichTextElementType returns the type of the Element
func (s RichTextList) RichTextElementType() RichTextElementType {
return s.Type
}
@@ -262,7 +266,9 @@ func (e *RichTextSection) UnmarshalJSON(b []byte) error {
return nil
}
-// NewRichTextSectionBlockElement .
+// NewRichTextSection creates a new rich text section from the provided elements. The
+// section type will default to "rich_text_section", as it's the only currently supported
+// section type.
func NewRichTextSection(elements ...RichTextSectionElement) *RichTextSection {
return &RichTextSection{
Type: RTESection,
@@ -328,7 +334,7 @@ func (r RichTextSectionChannelElement) RichTextSectionElementType() RichTextSect
func NewRichTextSectionChannelElement(channelID string, style *RichTextSectionTextStyle) *RichTextSectionChannelElement {
return &RichTextSectionChannelElement{
- Type: RTSEText,
+ Type: RTSEChannel,
ChannelID: channelID,
Style: style,
}
@@ -490,8 +496,16 @@ func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSect
return r.Type
}
+func (r RichTextSectionUnknownElement) MarshalJSON() ([]byte, error) {
+ return []byte(r.Raw), nil
+}
+
// RichTextQuote represents rich_text_quote element type.
-type RichTextQuote RichTextSection
+type RichTextQuote struct {
+ Type RichTextElementType `json:"type"`
+ Elements []RichTextSectionElement `json:"elements"`
+ Border int `json:"border,omitempty"`
+}
// RichTextElementType returns the type of the Element
func (s *RichTextQuote) RichTextElementType() RichTextElementType {
@@ -504,15 +518,26 @@ func (s *RichTextQuote) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(b, &rts); err != nil {
return err
}
- *s = RichTextQuote(rts)
- s.Type = RTEQuote
+ var standalone struct {
+ Border int `json:"border"`
+ }
+ if err := json.Unmarshal(b, &standalone); err != nil {
+ return err
+ }
+ *s = RichTextQuote{
+ Type: RTEQuote,
+ Elements: rts.Elements,
+ Border: standalone.Border,
+ }
return nil
}
// RichTextPreformatted represents rich_text_quote element type.
type RichTextPreformatted struct {
- RichTextSection
- Border int `json:"border"`
+ Type RichTextElementType `json:"type"`
+ Elements []RichTextSectionElement `json:"elements"`
+ Border int `json:"border"`
+ Language string `json:"language,omitempty"`
}
// RichTextElementType returns the type of the Element
@@ -536,15 +561,17 @@ func (s *RichTextPreformatted) UnmarshalJSON(b []byte) error {
// original struct, which may become a maintenance burden (i.e. update the
// fields in two places, should it ever change).
var standalone struct {
- Border int `json:"border"`
+ Border int `json:"border"`
+ Language string `json:"language"`
}
if err := json.Unmarshal(b, &standalone); err != nil {
return err
}
*s = RichTextPreformatted{
- RichTextSection: rts,
- Border: standalone.Border,
+ Type: RTEPreformatted,
+ Elements: rts.Elements,
+ Border: standalone.Border,
+ Language: standalone.Language,
}
- s.Type = RTEPreformatted
return nil
}
diff --git a/backend/vendor/github.com/slack-go/slack/block_table.go b/backend/vendor/github.com/slack-go/slack/block_table.go
new file mode 100644
index 00000000..5c7b0f13
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/block_table.go
@@ -0,0 +1,55 @@
+package slack
+
+// TableBlock defines a block that lets you use a table to display your data.
+//
+// More Information: https://docs.slack.dev/reference/block-kit/blocks/table-block/
+type TableBlock struct {
+ Type MessageBlockType `json:"type"`
+ BlockID string `json:"block_id,omitempty"`
+ Rows [][]*RichTextBlock `json:"rows"`
+ ColumnSettings []ColumnSetting `json:"column_settings,omitempty"`
+}
+
+type ColumnAlignment string
+
+const (
+ ColumnAlignmentLeft ColumnAlignment = "left"
+ ColumnAlignmentCenter ColumnAlignment = "center"
+ ColumnAlignmentRight ColumnAlignment = "right"
+)
+
+type ColumnSetting struct {
+ Align ColumnAlignment `json:"align"`
+ IsWrapped bool `json:"is_wrapped"`
+}
+
+// BlockType returns the type of the block
+func (s TableBlock) BlockType() MessageBlockType {
+ return s.Type
+}
+
+// ID returns the ID of the block
+func (s TableBlock) ID() string {
+ return s.BlockID
+}
+
+// WithColumnSettings sets the column settings for the Table Block
+func (s *TableBlock) WithColumnSettings(columnSettings ...ColumnSetting) *TableBlock {
+ s.ColumnSettings = columnSettings
+ return s
+}
+
+// AddRow adds a new row of cells to the Table Block
+func (s *TableBlock) AddRow(cells ...*RichTextBlock) *TableBlock {
+ s.Rows = append(s.Rows, append([]*RichTextBlock{}, cells...))
+ return s
+}
+
+// NewTableBlock returns an instance of a Table Block type
+func NewTableBlock(blockID string) *TableBlock {
+ return &TableBlock{
+ Type: MBTTable,
+ BlockID: blockID,
+ Rows: make([][]*RichTextBlock, 0),
+ }
+}
diff --git a/backend/vendor/github.com/slack-go/slack/block_task_card.go b/backend/vendor/github.com/slack-go/slack/block_task_card.go
new file mode 100644
index 00000000..2053a82d
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/block_task_card.go
@@ -0,0 +1,101 @@
+package slack
+
+// TaskCardStatus defines the status of a task card block.
+type TaskCardStatus string
+
+const (
+ TaskCardStatusPending TaskCardStatus = "pending"
+ TaskCardStatusInProgress TaskCardStatus = "in_progress"
+ TaskCardStatusComplete TaskCardStatus = "complete"
+ TaskCardStatusError TaskCardStatus = "error"
+)
+
+// TaskCardSource represents a URL reference in a task card block.
+type TaskCardSource struct {
+ Type string `json:"type"`
+ URL string `json:"url"`
+ Text string `json:"text"`
+}
+
+// NewTaskCardSource creates a new TaskCardSource with type "url".
+func NewTaskCardSource(url, text string) TaskCardSource {
+ return TaskCardSource{
+ Type: "url",
+ URL: url,
+ Text: text,
+ }
+}
+
+// TaskCardBlock defines a block of type task_card used by AI agents
+// to display thinking steps and task execution.
+//
+// More Information: https://docs.slack.dev/reference/block-kit/blocks/task-card-block/
+type TaskCardBlock struct {
+ Type MessageBlockType `json:"type"`
+ BlockID string `json:"block_id,omitempty"`
+ TaskID string `json:"task_id"`
+ Title string `json:"title"`
+ Status TaskCardStatus `json:"status,omitempty"`
+ Details *RichTextBlock `json:"details,omitempty"`
+ Output *RichTextBlock `json:"output,omitempty"`
+ Sources []TaskCardSource `json:"sources,omitempty"`
+}
+
+// BlockType returns the type of the block
+func (s TaskCardBlock) BlockType() MessageBlockType {
+ return s.Type
+}
+
+// ID returns the ID of the block
+func (s TaskCardBlock) ID() string {
+ return s.BlockID
+}
+
+// TaskCardBlockOption allows configuration of options for a new task card block
+type TaskCardBlockOption func(*TaskCardBlock)
+
+// TaskCardBlockOptionBlockID sets the block ID for the task card block
+func TaskCardBlockOptionBlockID(blockID string) TaskCardBlockOption {
+ return func(block *TaskCardBlock) {
+ block.BlockID = blockID
+ }
+}
+
+// NewTaskCardBlock returns a new instance of a task card block
+func NewTaskCardBlock(taskID, title string, options ...TaskCardBlockOption) *TaskCardBlock {
+ block := TaskCardBlock{
+ Type: MBTTaskCard,
+ TaskID: taskID,
+ Title: title,
+ }
+
+ for _, option := range options {
+ option(&block)
+ }
+
+ return &block
+}
+
+// WithStatus sets the status for the TaskCardBlock
+func (s *TaskCardBlock) WithStatus(status TaskCardStatus) *TaskCardBlock {
+ s.Status = status
+ return s
+}
+
+// WithDetails sets the details rich text block for the TaskCardBlock
+func (s *TaskCardBlock) WithDetails(details *RichTextBlock) *TaskCardBlock {
+ s.Details = details
+ return s
+}
+
+// WithOutput sets the output rich text block for the TaskCardBlock
+func (s *TaskCardBlock) WithOutput(output *RichTextBlock) *TaskCardBlock {
+ s.Output = output
+ return s
+}
+
+// WithSources sets the sources for the TaskCardBlock
+func (s *TaskCardBlock) WithSources(sources ...TaskCardSource) *TaskCardBlock {
+ s.Sources = sources
+ return s
+}
diff --git a/backend/vendor/github.com/slack-go/slack/block_unknown.go b/backend/vendor/github.com/slack-go/slack/block_unknown.go
index 7a49a2c8..71b2a90c 100644
--- a/backend/vendor/github.com/slack-go/slack/block_unknown.go
+++ b/backend/vendor/github.com/slack-go/slack/block_unknown.go
@@ -1,10 +1,19 @@
package slack
-// UnknownBlock represents a block type that is not yet known. This block type exists to prevent Slack from introducing
-// new and unknown block types that break this library.
+import "encoding/json"
+
+// UnknownBlock represents a block type that is not yet known. This block type
+// exists to prevent Slack from introducing new and unknown block types that
+// break this library. It preserves the raw JSON so that unrecognized blocks
+// survive round-trip marshaling.
+//
+// If you encounter an UnknownBlock for a block type that Slack documents,
+// please open an issue at https://github.com/slack-go/slack/issues so we can
+// add first-class support for it.
type UnknownBlock struct {
Type MessageBlockType `json:"type"`
BlockID string `json:"block_id,omitempty"`
+ raw json.RawMessage
}
// BlockType returns the type of the block
@@ -16,3 +25,12 @@ func (b UnknownBlock) BlockType() MessageBlockType {
func (s UnknownBlock) ID() string {
return s.BlockID
}
+
+// MarshalJSON returns the original raw JSON if available, preserving all fields
+func (b UnknownBlock) MarshalJSON() ([]byte, error) {
+ if b.raw != nil {
+ return b.raw, nil
+ }
+ type alias UnknownBlock
+ return json.Marshal(alias(b))
+}
diff --git a/backend/vendor/github.com/slack-go/slack/channels.go b/backend/vendor/github.com/slack-go/slack/channels.go
index 88d567bf..d01ce823 100644
--- a/backend/vendor/github.com/slack-go/slack/channels.go
+++ b/backend/vendor/github.com/slack-go/slack/channels.go
@@ -28,7 +28,7 @@ type Channel struct {
func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) {
response := &channelResponseFull{}
- err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api)
+ _, err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api)
if err != nil {
return nil, err
}
diff --git a/backend/vendor/github.com/slack-go/slack/chat.go b/backend/vendor/github.com/slack-go/slack/chat.go
index 85c4848b..8d4066cf 100644
--- a/backend/vendor/github.com/slack-go/slack/chat.go
+++ b/backend/vendor/github.com/slack-go/slack/chat.go
@@ -119,13 +119,13 @@ func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOptio
// ScheduleMessageContext sends a message to a channel with a custom context.
// Slack API docs: https://api.slack.com/methods/chat.scheduleMessage
func (api *Client) ScheduleMessageContext(ctx context.Context, channelID, postAt string, options ...MsgOption) (string, string, error) {
- respChannel, scheduledMessageId, _, err := api.SendMessageContext(
+ respChannel, scheduledMessageID, _, err := api.SendMessageContext(
ctx,
channelID,
MsgOptionSchedule(postAt),
MsgOptionCompose(options...),
)
- return respChannel, scheduledMessageId, err
+ return respChannel, scheduledMessageID, err
}
// PostMessage sends a message to a channel.
@@ -209,6 +209,33 @@ func (api *Client) UnfurlMessageWithAuthURLContext(ctx context.Context, channelI
return api.SendMessageContext(ctx, channelID, MsgOptionUnfurlAuthURL(timestamp, userAuthURL), MsgOptionCompose(options...))
}
+// UnfurlMessageWorkObject unfurls a message with Work Objects metadata.
+// For more details, see UnfurlMessageWorkObjectContext documentation.
+func (api *Client) UnfurlMessageWorkObject(channelID, timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata, options ...MsgOption) (string, string, string, error) {
+ return api.UnfurlMessageWorkObjectContext(context.Background(), channelID, timestamp, unfurls, metadata, options...)
+}
+
+// UnfurlMessageWorkObjectContext unfurls a message with Work Objects metadata with a custom context.
+// This enables rich Work Object previews as described in https://docs.slack.dev/messaging/work-objects/
+// unfurls may be nil to send only Work Object metadata (no legacy attachment unfurls).
+func (api *Client) UnfurlMessageWorkObjectContext(ctx context.Context, channelID, timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata, options ...MsgOption) (string, string, string, error) {
+ return api.SendMessageContext(ctx, channelID, MsgOptionUnfurlWorkObject(timestamp, unfurls, metadata), MsgOptionCompose(options...))
+}
+
+// UnfurlMessageByID unfurls a link in the message composer using unfurl_id and source.
+// Use this when Slack sends link_shared with unfurl_id (e.g. before the message is posted).
+// For more details, see UnfurlMessageByIDContext documentation.
+func (api *Client) UnfurlMessageByID(unfurlID, source string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) {
+ return api.UnfurlMessageByIDContext(context.Background(), unfurlID, source, unfurls, options...)
+}
+
+// UnfurlMessageByIDContext unfurls by unfurl_id and source with a custom context.
+// Both unfurl_id and source must be provided together (alternative to channel + ts).
+// Slack API docs: https://api.slack.com/methods/chat.unfurl
+func (api *Client) UnfurlMessageByIDContext(ctx context.Context, unfurlID, source string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) {
+ return api.SendMessageContext(ctx, "", MsgOptionUnfurlByID(unfurlID, source, unfurls), MsgOptionCompose(options...))
+}
+
// SendMessage more flexible method for configuring messages.
// For more details, see SendMessageContext documentation.
func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) {
@@ -217,7 +244,7 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st
// SendMessageContext more flexible method for configuring messages with a custom context.
// Slack API docs: https://api.slack.com/methods/chat.postMessage
-func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestampOrScheduledMessageId string, _text string, err error) {
+func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestampOrScheduledMessageID string, _text string, err error) {
var (
req *http.Request
parser func(*chatResponseFull) responseParser
@@ -237,7 +264,7 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt
api.Debugf("Sending request: %s", redactToken(reqBody))
}
- if err = doPost(api.httpclient, req, parser(&response), api); err != nil {
+ if _, err = doPost(api.httpclient, req, parser(&response), api); err != nil {
return "", "", "", err
}
@@ -307,6 +334,9 @@ const (
chatResponse sendMode = "chat.responseURL"
chatMeMessage sendMode = "chat.meMessage"
chatUnfurl sendMode = "chat.unfurl"
+ chatStartStream sendMode = "chat.startStream"
+ chatAppendStream sendMode = "chat.appendStream"
+ chatStopStream sendMode = "chat.stopStream"
)
type sendConfig struct {
@@ -345,13 +375,15 @@ func (t sendConfig) BuildRequestContext(ctx context.Context, token, channelID st
deleteOriginal: t.deleteOriginal,
}.BuildRequestContext(ctx)
default:
- return formSender{endpoint: t.endpoint, values: t.values}.BuildRequestContext(ctx)
+ return formSender{endpoint: t.endpoint, values: t.values, attachments: t.attachments, blocks: t.blocks}.BuildRequestContext(ctx)
}
}
type formSender struct {
- endpoint string
- values url.Values
+ endpoint string
+ values url.Values
+ attachments []Attachment
+ blocks Blocks
}
func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) {
@@ -359,6 +391,22 @@ func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) respo
}
func (t formSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) {
+ if t.attachments != nil {
+ attachmentBytes, err := json.Marshal(t.attachments)
+ if err != nil {
+ return nil, nil, err
+ }
+ t.values.Set("attachments", string(attachmentBytes))
+ }
+
+ if t.blocks.BlockSet != nil {
+ blockBytes, err := json.Marshal(t.blocks.BlockSet)
+ if err != nil {
+ return nil, nil, err
+ }
+ t.values.Set("blocks", string(blockBytes))
+ }
+
req, err := formReq(ctx, t.endpoint, t.values)
return req, func(resp *chatResponseFull) responseParser {
return newJSONParser(resp)
@@ -468,6 +516,51 @@ func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption
}
}
+// MsgOptionUnfurlByID unfurls using unfurl_id and source (e.g. when link is in the composer).
+// Use instead of channel+ts when Slack provides unfurl_id in the link_shared event.
+// unfurls may be nil; the API expects a JSON object so nil is sent as {}.
+func MsgOptionUnfurlByID(unfurlID, source string, unfurls map[string]Attachment) MsgOption {
+ return func(config *sendConfig) error {
+ config.endpoint = config.apiurl + string(chatUnfurl)
+ config.values.Del("channel")
+ config.values.Del("ts")
+ config.values.Set("unfurl_id", unfurlID)
+ config.values.Set("source", source)
+ if unfurls == nil {
+ unfurls = make(map[string]Attachment)
+ }
+ unfurlsStr, err := json.Marshal(unfurls)
+ if err == nil {
+ config.values.Set("unfurls", string(unfurlsStr))
+ }
+ return err
+ }
+}
+
+// MsgOptionUnfurlMetadataOnly sets chat.unfurl endpoint with only Work Object metadata (no unfurls).
+func MsgOptionUnfurlMetadataOnly(timestamp string, metadata WorkObjectMetadata) MsgOption {
+ return MsgOptionCompose(
+ func(config *sendConfig) error {
+ config.endpoint = config.apiurl + string(chatUnfurl)
+ config.values.Add("ts", timestamp)
+ return nil
+ },
+ MsgOptionWorkObjectMetadata(metadata),
+ )
+}
+
+// MsgOptionUnfurlWorkObject unfurls a message with Work Objects metadata.
+// When unfurls is nil, only metadata is sent (no legacy attachment unfurls).
+func MsgOptionUnfurlWorkObject(timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata) MsgOption {
+ if len(unfurls) > 0 {
+ return MsgOptionCompose(
+ MsgOptionUnfurl(timestamp, unfurls),
+ MsgOptionWorkObjectMetadata(metadata),
+ )
+ }
+ return MsgOptionUnfurlMetadataOnly(timestamp, metadata)
+}
+
// MsgOptionUnfurlAuthURL unfurls a message using an auth url based on the timestamp.
func MsgOptionUnfurlAuthURL(timestamp string, userAuthURL string) MsgOption {
return func(config *sendConfig) error {
@@ -500,6 +593,23 @@ func MsgOptionUnfurlAuthMessage(timestamp string, msg string) MsgOption {
}
}
+// MsgOptionUnfurlAuthBlocks sets Block Kit blocks for the auth prompt (overrides default buttons).
+// See https://docs.slack.com/methods/chat.unfurl for user_auth_blocks.
+func MsgOptionUnfurlAuthBlocks(timestamp string, blocks ...Block) MsgOption {
+ return func(config *sendConfig) error {
+ config.endpoint = config.apiurl + string(chatUnfurl)
+ config.values.Add("ts", timestamp)
+ if len(blocks) == 0 {
+ return nil
+ }
+ blocksStr, err := json.Marshal(blocks)
+ if err == nil {
+ config.values.Set("user_auth_blocks", string(blocksStr))
+ }
+ return err
+ }
+}
+
// MsgOptionResponseURL supplies a url to use as the endpoint.
func MsgOptionResponseURL(url string, responseType string) MsgOption {
return func(config *sendConfig) error {
@@ -578,33 +688,24 @@ func MsgOptionAttachments(attachments ...Attachment) MsgOption {
config.attachments = attachments
- // FIXME: We are setting the attachments on the message twice: above for
- // the json version, and below for the html version. The marshalled bytes
- // we put into config.values below don't work directly in the Msg version.
-
- attachmentBytes, err := json.Marshal(attachments)
- if err == nil {
- config.values.Set("attachments", string(attachmentBytes))
- }
-
- return err
+ return nil
}
}
-// MsgOptionBlocks sets blocks for the message
+// MsgOptionBlocks sets blocks for the message.
+// Calling with no arguments or an empty slice sends "blocks=[]" to clear blocks.
+// To skip setting blocks entirely, do not include this option.
func MsgOptionBlocks(blocks ...Block) MsgOption {
return func(config *sendConfig) error {
- if blocks == nil {
- return nil
+ if len(blocks) == 0 {
+ // Explicitly set to empty slice (not nil) so the sender
+ // knows to marshal "[]" and clear blocks on the message.
+ config.blocks.BlockSet = []Block{}
+ } else {
+ config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...)
}
- config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...)
-
- blocks, err := json.Marshal(blocks)
- if err == nil {
- config.values.Set("blocks", string(blocks))
- }
- return err
+ return nil
}
}
@@ -710,6 +811,31 @@ func MsgOptionMetadata(metadata SlackMetadata) MsgOption {
}
}
+// MsgOptionWorkObjectMetadata sets Work Objects metadata for unfurls and messages
+// This enables Work Objects support as described in https://docs.slack.dev/messaging/work-objects/
+// If metadata.Entities is nil, it is marshaled as [] so the API receives a valid entities array.
+func MsgOptionWorkObjectMetadata(metadata WorkObjectMetadata) MsgOption {
+ return func(config *sendConfig) error {
+ metaToMarshal := metadata
+ if metaToMarshal.Entities == nil {
+ metaToMarshal.Entities = []WorkObjectEntity{}
+ }
+ meta, err := json.Marshal(metaToMarshal)
+ if err == nil {
+ config.values.Set("metadata", string(meta))
+ }
+ return err
+ }
+}
+
+// MsgOptionWorkObjectEntity creates Work Objects metadata with a single entity
+// This is a convenience function for the common case of unfurling a single Work Object
+func MsgOptionWorkObjectEntity(entity WorkObjectEntity) MsgOption {
+ return MsgOptionWorkObjectMetadata(WorkObjectMetadata{
+ Entities: []WorkObjectEntity{entity},
+ })
+}
+
// MsgOptionLinkNames finds and links user groups. Does not support linking individual users
func MsgOptionLinkNames(linkName bool) MsgOption {
return func(config *sendConfig) error {
@@ -735,6 +861,56 @@ func MsgOptionFileIDs(fileIDs []string) MsgOption {
}
}
+// MsgOptionStartStream starts a streaming message.
+func MsgOptionStartStream() MsgOption {
+ return func(config *sendConfig) error {
+ config.endpoint = config.apiurl + string(chatStartStream)
+ return nil
+ }
+}
+
+// MsgOptionAppendStream appends to a streaming message.
+func MsgOptionAppendStream(timestamp string) MsgOption {
+ return func(config *sendConfig) error {
+ config.endpoint = config.apiurl + string(chatAppendStream)
+ config.values.Add("ts", timestamp)
+ return nil
+ }
+}
+
+// MsgOptionStopStream stops a streaming message.
+func MsgOptionStopStream(timestamp string) MsgOption {
+ return func(config *sendConfig) error {
+ config.endpoint = config.apiurl + string(chatStopStream)
+ config.values.Add("ts", timestamp)
+ return nil
+ }
+}
+
+// MsgOptionRecipientTeamID sets the recipient team ID for streaming messages.
+func MsgOptionRecipientTeamID(teamID string) MsgOption {
+ return func(config *sendConfig) error {
+ config.values.Set("recipient_team_id", teamID)
+ return nil
+ }
+}
+
+// MsgOptionRecipientUserID sets the recipient user ID for streaming messages.
+func MsgOptionRecipientUserID(userID string) MsgOption {
+ return func(config *sendConfig) error {
+ config.values.Set("recipient_user_id", userID)
+ return nil
+ }
+}
+
+// MsgOptionMarkdownText sets the markdown text for streaming messages.
+func MsgOptionMarkdownText(text string) MsgOption {
+ return func(config *sendConfig) error {
+ config.values.Set("markdown_text", text)
+ return nil
+ }
+}
+
// UnsafeMsgOptionEndpoint deliver the message to the specified endpoint.
// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option
// will be supported by the library, it is subject to change without notice that
@@ -798,6 +974,12 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
config.values.Set("reply_broadcast", "true")
}
+ if params.MetaData.EventType != "" {
+ if err := MsgOptionMetadata(params.MetaData)(config); err != nil {
+ return err
+ }
+ }
+
if len(params.FileIDs) > 0 {
return MsgOptionFileIDs(params.FileIDs)(config)
}
@@ -924,3 +1106,57 @@ func (api *Client) DeleteScheduledMessageContext(ctx context.Context, params *De
return response.Ok, response.Err()
}
+
+// StartStream starts a streaming message in a channel.
+// For more details, see StartStreamContext documentation.
+func (api *Client) StartStream(channelID string, options ...MsgOption) (string, string, error) {
+ return api.StartStreamContext(context.Background(), channelID, options...)
+}
+
+// StartStreamContext starts a streaming message in a channel with a custom context.
+// Slack API docs: https://api.slack.com/methods/chat.startStream
+func (api *Client) StartStreamContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) {
+ respChannel, respTimestamp, _, err := api.SendMessageContext(
+ ctx,
+ channelID,
+ MsgOptionStartStream(),
+ MsgOptionCompose(options...),
+ )
+ return respChannel, respTimestamp, err
+}
+
+// AppendStream appends text to a streaming message.
+// For more details, see AppendStreamContext documentation.
+func (api *Client) AppendStream(channelID, timestamp string, options ...MsgOption) (string, string, error) {
+ return api.AppendStreamContext(context.Background(), channelID, timestamp, options...)
+}
+
+// AppendStreamContext appends text to a streaming message with a custom context.
+// Slack API docs: https://api.slack.com/methods/chat.appendStream
+func (api *Client) AppendStreamContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, error) {
+ respChannel, respTimestamp, _, err := api.SendMessageContext(
+ ctx,
+ channelID,
+ MsgOptionAppendStream(timestamp),
+ MsgOptionCompose(options...),
+ )
+ return respChannel, respTimestamp, err
+}
+
+// StopStream stops a streaming message.
+// For more details, see StopStreamContext documentation.
+func (api *Client) StopStream(channelID, timestamp string, options ...MsgOption) (string, string, error) {
+ return api.StopStreamContext(context.Background(), channelID, timestamp, options...)
+}
+
+// StopStreamContext stops a streaming message with a custom context.
+// Slack API docs: https://api.slack.com/methods/chat.stopStream
+func (api *Client) StopStreamContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, error) {
+ respChannel, respTimestamp, _, err := api.SendMessageContext(
+ ctx,
+ channelID,
+ MsgOptionStopStream(timestamp),
+ MsgOptionCompose(options...),
+ )
+ return respChannel, respTimestamp, err
+}
diff --git a/backend/vendor/github.com/slack-go/slack/conversation.go b/backend/vendor/github.com/slack-go/slack/conversation.go
index 33eb0ff9..a0ce707b 100644
--- a/backend/vendor/github.com/slack-go/slack/conversation.go
+++ b/backend/vendor/github.com/slack-go/slack/conversation.go
@@ -7,6 +7,7 @@ import (
"net/url"
"strconv"
"strings"
+ "time"
)
// Conversation is the foundation for IM and BaseGroupConversation
@@ -28,6 +29,7 @@ type Conversation struct {
IsPrivate bool `json:"is_private"`
IsReadOnly bool `json:"is_read_only"`
IsMpIM bool `json:"is_mpim"`
+ IsUserDeleted bool `json:"is_user_deleted"`
Unlinked int `json:"unlinked"`
NameNormalized string `json:"name_normalized"`
NumMembers int `json:"num_members"`
@@ -67,12 +69,13 @@ type Purpose struct {
LastSet JSONTime `json:"last_set"`
}
-// Properties contains the Canvas associated to the channel.
+// Properties contains additional fields that appear based on the context of the conversation
type Properties struct {
- Canvas Canvas `json:"canvas"`
- PostingRestrictedTo RestrictedTo `json:"posting_restricted_to"`
- Tabs []Tab `json:"tabs"`
- ThreadsRestrictedTo RestrictedTo `json:"threads_restricted_to"`
+ Canvas Canvas `json:"canvas"`
+ PostingRestrictedTo RestrictedTo `json:"posting_restricted_to"`
+ Tabs []Tab `json:"tabs"`
+ ThreadsRestrictedTo RestrictedTo `json:"threads_restricted_to"`
+ RecordChannel RecordChannel `json:"record_channel"`
}
type RestrictedTo struct {
@@ -92,6 +95,13 @@ type Canvas struct {
QuipThreadId string `json:"quip_thread_id"`
}
+type RecordChannel struct {
+ RecordID string `json:"record_id"`
+ RecordType string `json:"record_type"`
+ RecordLabel string `json:"record_label"`
+ RecordLabelPlural string `json:"record_label_plural"`
+}
+
type GetUsersInConversationParameters struct {
ChannelID string
Cursor string
@@ -344,12 +354,14 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel
return response.Channel, response.Err()
}
-// The following functions are for inviting users to a channel but setting the `force`
-// parameter to true. We have added this so that we don't break the existing API.
-//
-// IMPORTANT: If we ever get here for _another_ parameter, we should consider refactoring
-// this to be more flexible.
-//
+/**********************************************************************************
+The following functions are for inviting users to a channel but setting the `force`
+parameter to true. We have added this so that we don't break the existing API.
+
+IMPORTANT: If we ever get here for _another_ parameter, we should consider refactoring
+this to be more flexible.
+*/
+
// ForceInviteUsersToConversation invites users to a channel but sets the `force`
// parameter to true.
//
@@ -478,7 +490,7 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI
"user": {user},
}
- response := SlackResponse{}
+ response := KickUserFromConversationSlackResponse{}
err := api.postMethod(ctx, "conversations.kick", values, &response)
if err != nil {
return err
@@ -677,6 +689,150 @@ type GetConversationsParameters struct {
TeamID string
}
+// GetConversationsOption options for the GetAllConversationsContext method call.
+type GetConversationsOption func(*ConversationPagination)
+
+// GetConversationsOptionLimit limit the number of conversations returned
+func GetConversationsOptionLimit(n int) GetConversationsOption {
+ return func(p *ConversationPagination) {
+ p.limit = n
+ }
+}
+
+// GetConversationsOptionExcludeArchived exclude archived conversations
+func GetConversationsOptionExcludeArchived(exclude bool) GetConversationsOption {
+ return func(p *ConversationPagination) {
+ p.excludeArchived = exclude
+ }
+}
+
+// GetConversationsOptionTypes filter conversations by type
+func GetConversationsOptionTypes(types []string) GetConversationsOption {
+ return func(p *ConversationPagination) {
+ p.types = types
+ }
+}
+
+// GetConversationsOptionTeamID include team Id
+func GetConversationsOptionTeamID(teamId string) GetConversationsOption {
+ return func(p *ConversationPagination) {
+ p.teamId = teamId
+ }
+}
+
+func newConversationPagination(c *Client, options ...GetConversationsOption) (cp ConversationPagination) {
+ cp = ConversationPagination{
+ c: c,
+ limit: 200, // per slack api documentation.
+ }
+
+ for _, opt := range options {
+ opt(&cp)
+ }
+
+ return cp
+}
+
+// ConversationPagination allows for paginating over the conversations
+type ConversationPagination struct {
+ Conversations []Channel
+ limit int
+ excludeArchived bool
+ types []string
+ teamId string
+ previousResp *ResponseMetadata
+ c *Client
+}
+
+// Done checks if the pagination has completed
+func (ConversationPagination) Done(err error) bool {
+ return errors.Is(err, errPaginationComplete)
+}
+
+// Failure checks if pagination failed.
+func (t ConversationPagination) Failure(err error) error {
+ if t.Done(err) {
+ return nil
+ }
+
+ return err
+}
+
+func (t ConversationPagination) Next(ctx context.Context) (_ ConversationPagination, err error) {
+ if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") {
+ return t, errPaginationComplete
+ }
+
+ t.previousResp = t.previousResp.initialize()
+
+ values := url.Values{
+ "token": {t.c.token},
+ "limit": {strconv.Itoa(t.limit)},
+ "cursor": {t.previousResp.Cursor},
+ }
+ if t.excludeArchived {
+ values.Add("exclude_archived", strconv.FormatBool(t.excludeArchived))
+ }
+ if t.types != nil {
+ values.Add("types", strings.Join(t.types, ","))
+ }
+ if t.teamId != "" {
+ values.Add("team_id", t.teamId)
+ }
+
+ response := struct {
+ Channels []Channel `json:"channels"`
+ ResponseMetaData responseMetaData `json:"response_metadata"`
+ SlackResponse
+ }{}
+
+ err = t.c.postMethod(ctx, "conversations.list", values, &response)
+ if err != nil {
+ return t, err
+ }
+
+ if err := response.Err(); err != nil {
+ return t, err
+ }
+
+ t.c.Debugf("GetAllConversationsContext: got %d conversations; cursor %s", len(response.Channels), response.ResponseMetaData.NextCursor)
+ t.Conversations = response.Channels
+ t.previousResp = &ResponseMetadata{Cursor: response.ResponseMetaData.NextCursor}
+
+ return t, nil
+}
+
+// GetConversationsPaginated fetches conversations in a paginated fashion, see GetAllConversationsContext for usage.
+func (api *Client) GetConversationsPaginated(options ...GetConversationsOption) ConversationPagination {
+ return newConversationPagination(api, options...)
+}
+
+// GetAllConversations returns the list of all conversations, handling pagination and rate limiting
+func (api *Client) GetAllConversations(options ...GetConversationsOption) (results []Channel, err error) {
+ return api.GetAllConversationsContext(context.Background(), options...)
+}
+
+// GetAllConversationsContext returns the list of all conversations with a custom context, handling pagination and rate limiting
+func (api *Client) GetAllConversationsContext(ctx context.Context, options ...GetConversationsOption) (results []Channel, err error) {
+ results = []Channel{}
+ p := api.GetConversationsPaginated(options...)
+ for err == nil {
+ p, err = p.Next(ctx)
+ if err == nil {
+ results = append(results, p.Conversations...)
+ } else if rateLimitedError, ok := err.(*RateLimitedError); ok {
+ select {
+ case <-ctx.Done():
+ err = ctx.Err()
+ case <-time.After(rateLimitedError.RetryAfter):
+ err = nil
+ }
+ }
+ }
+
+ return results, p.Failure(err)
+}
+
// GetConversations returns the list of channels in a Slack team.
// For more details, see GetConversationsContext documentation.
func (api *Client) GetConversations(params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) {
@@ -813,13 +969,13 @@ type GetConversationHistoryResponse struct {
Messages []Message `json:"messages"`
}
-// GetConversationHistory joins an existing conversation.
+// GetConversationHistory retrieves the message history from the specified conversation.
// For more details, see GetConversationHistoryContext documentation.
func (api *Client) GetConversationHistory(params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) {
return api.GetConversationHistoryContext(context.Background(), params)
}
-// GetConversationHistoryContext joins an existing conversation with a custom context.
+// GetConversationHistoryContext retrieves the message history from the specified conversation with a custom context.
// Slack API docs: https://api.slack.com/methods/conversations.history
func (api *Client) GetConversationHistoryContext(ctx context.Context, params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) {
values := url.Values{"token": {api.token}, "channel": {params.ChannelID}}
@@ -880,21 +1036,55 @@ func (api *Client) MarkConversationContext(ctx context.Context, channel, ts stri
return response.Err()
}
+// createChannelCanvasParams contains arguments for CreateChannelCanvas method call.
+type createChannelCanvasParams struct {
+ title string
+ documentContent *DocumentContent
+}
+
+// CreateChannelCanvasOption options for the CreateChannelCanvas method call.
+type CreateChannelCanvasOption func(*createChannelCanvasParams)
+
+// CreateChannelCanvasOptionTitle sets the title of the canvas.
+func CreateChannelCanvasOptionTitle(title string) CreateChannelCanvasOption {
+ return func(params *createChannelCanvasParams) {
+ params.title = title
+ }
+}
+
+// CreateChannelCanvasOptionDocumentContent sets the document content of the canvas.
+func CreateChannelCanvasOptionDocumentContent(documentContent DocumentContent) CreateChannelCanvasOption {
+ return func(params *createChannelCanvasParams) {
+ params.documentContent = &documentContent
+ }
+}
+
// CreateChannelCanvas creates a new canvas in a channel.
// For more details, see CreateChannelCanvasContext documentation.
-func (api *Client) CreateChannelCanvas(channel string, documentContent DocumentContent) (string, error) {
- return api.CreateChannelCanvasContext(context.Background(), channel, documentContent)
+func (api *Client) CreateChannelCanvas(channel string, documentContent DocumentContent, options ...CreateChannelCanvasOption) (string, error) {
+ return api.CreateChannelCanvasContext(context.Background(), channel, documentContent, options...)
}
// CreateChannelCanvasContext creates a new canvas in a channel with a custom context.
// Slack API docs: https://api.slack.com/methods/conversations.canvases.create
-func (api *Client) CreateChannelCanvasContext(ctx context.Context, channel string, documentContent DocumentContent) (string, error) {
+func (api *Client) CreateChannelCanvasContext(ctx context.Context, channel string, documentContent DocumentContent, options ...CreateChannelCanvasOption) (string, error) {
+ params := createChannelCanvasParams{
+ documentContent: &documentContent,
+ }
+
+ for _, opt := range options {
+ opt(¶ms)
+ }
+
values := url.Values{
"token": {api.token},
"channel_id": {channel},
}
- if documentContent.Type != "" {
- documentContentJSON, err := json.Marshal(documentContent)
+ if params.title != "" {
+ values.Add("title", params.title)
+ }
+ if params.documentContent != nil && params.documentContent.Type != "" {
+ documentContentJSON, err := json.Marshal(params.documentContent)
if err != nil {
return "", err
}
diff --git a/backend/vendor/github.com/slack-go/slack/dialog.go b/backend/vendor/github.com/slack-go/slack/dialog.go
index f94113f4..4c507fcd 100644
--- a/backend/vendor/github.com/slack-go/slack/dialog.go
+++ b/backend/vendor/github.com/slack-go/slack/dialog.go
@@ -106,8 +106,7 @@ func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dial
}
response := &DialogOpenResponse{}
- endpoint := api.endpoint + "dialog.open"
- if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil {
+ if err := api.postJSONMethod(ctx, "dialog.open", api.token, encoded, response); err != nil {
return err
}
diff --git a/backend/vendor/github.com/slack-go/slack/dnd.go b/backend/vendor/github.com/slack-go/slack/dnd.go
index 81eaf502..4f6b35a5 100644
--- a/backend/vendor/github.com/slack-go/slack/dnd.go
+++ b/backend/vendor/github.com/slack-go/slack/dnd.go
@@ -7,6 +7,14 @@ import (
"strings"
)
+// DNDOptionTeamID sets the team_id parameter for DND methods. Required after
+// workspace migration when the API returns missing_argument: team_id.
+func DNDOptionTeamID(teamID string) ParamOption {
+ return func(v *url.Values) {
+ v.Set("team_id", teamID)
+ }
+}
+
type SnoozeDebug struct {
SnoozeEndDate string `json:"snooze_end_date"`
}
@@ -52,7 +60,7 @@ func (api *Client) EndDND() error {
}
// EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context.
-// Slack API docs: https://api.slack.com/methods/dnd.endDnd
+// Slack API docs: https://docs.slack.dev/reference/methods/dnd.endDnd
func (api *Client) EndDNDContext(ctx context.Context) error {
values := url.Values{
"token": {api.token},
@@ -74,7 +82,7 @@ func (api *Client) EndSnooze() (*DNDStatus, error) {
}
// EndSnoozeContext ends the current user's snooze mode with a custom context.
-// Slack API docs: https://api.slack.com/methods/dnd.endSnooze
+// Slack API docs: https://docs.slack.dev/reference/methods/dnd.endSnooze
func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) {
values := url.Values{
"token": {api.token},
@@ -89,19 +97,22 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) {
// GetDNDInfo provides information about a user's current Do Not Disturb settings.
// For more information see the GetDNDInfoContext documentation.
-func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) {
- return api.GetDNDInfoContext(context.Background(), user)
+func (api *Client) GetDNDInfo(user *string, options ...ParamOption) (*DNDStatus, error) {
+ return api.GetDNDInfoContext(context.Background(), user, options...)
}
// GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context.
-// Slack API docs: https://api.slack.com/methods/dnd.info
-func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) {
+// Slack API docs: https://docs.slack.dev/reference/methods/dnd.info/
+func (api *Client) GetDNDInfoContext(ctx context.Context, user *string, options ...ParamOption) (*DNDStatus, error) {
values := url.Values{
"token": {api.token},
}
if user != nil {
values.Set("user", *user)
}
+ for _, opt := range options {
+ opt(&values)
+ }
response, err := api.dndRequest(ctx, "dnd.info", values)
if err != nil {
@@ -112,17 +123,20 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta
// GetDNDTeamInfo provides information about a user's current Do Not Disturb settings.
// For more information see the GetDNDTeamInfoContext documentation.
-func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) {
- return api.GetDNDTeamInfoContext(context.Background(), users)
+func (api *Client) GetDNDTeamInfo(users []string, options ...ParamOption) (map[string]DNDStatus, error) {
+ return api.GetDNDTeamInfoContext(context.Background(), users, options...)
}
// GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context.
-// Slack API docs: https://api.slack.com/methods/dnd.teamInfo
-func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) {
+// Slack API docs: https://docs.slack.dev/reference/methods/dnd.teamInfo
+func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string, options ...ParamOption) (map[string]DNDStatus, error) {
values := url.Values{
"token": {api.token},
"users": {strings.Join(users, ",")},
}
+ for _, opt := range options {
+ opt(&values)
+ }
response := &dndTeamInfoResponse{}
if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil {
@@ -145,7 +159,7 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) {
// SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings.
// If a snooze session is not already active for the user, invoking this method will
// begin one for the specified duration.
-// Slack API docs: https://api.slack.com/methods/dnd.setSnooze
+// Slack API docs: https://docs.slack.dev/reference/methods/dnd.setSnooze
func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) {
values := url.Values{
"token": {api.token},
diff --git a/backend/vendor/github.com/slack-go/slack/entity.go b/backend/vendor/github.com/slack-go/slack/entity.go
new file mode 100644
index 00000000..815de4d7
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/entity.go
@@ -0,0 +1,132 @@
+package slack
+
+import (
+ "context"
+ "encoding/json"
+ "net/url"
+)
+
+// EntityPresentDetailsParameters contains the parameters for entity.presentDetails API method
+type EntityPresentDetailsParameters struct {
+ TriggerID string `json:"trigger_id"`
+ Metadata *EntityDetailsMetadata `json:"metadata,omitempty"`
+ Error *EntityDetailsError `json:"error,omitempty"`
+ UserAuthRequired bool `json:"user_auth_required,omitempty"`
+ UserAuthURL string `json:"user_auth_url,omitempty"`
+ UserAuthMessage string `json:"user_auth_message,omitempty"`
+}
+
+// EntityDetailsMetadata represents the metadata for entity details
+type EntityDetailsMetadata struct {
+ EntityType string `json:"entity_type"`
+ URL string `json:"url,omitempty"`
+ ExternalRef WorkObjectExternalRef `json:"external_ref,omitempty"`
+ EntityPayload map[string]interface{} `json:"entity_payload"`
+}
+
+// EntityDetailsError represents an error response for entity details
+type EntityDetailsError struct {
+ Status string `json:"status"`
+ CustomTitle string `json:"custom_title,omitempty"`
+ CustomMessage string `json:"custom_message,omitempty"`
+ MessageFormat string `json:"message_format,omitempty"`
+ Actions []EntityDetailsAction `json:"actions,omitempty"`
+}
+
+// EntityDetailsAction represents an action button in entity details error
+type EntityDetailsAction struct {
+ Text string `json:"text"`
+ ActionID string `json:"action_id"`
+ Value string `json:"value,omitempty"`
+ Style string `json:"style,omitempty"`
+ URL string `json:"url,omitempty"`
+ ProcessingState *EntityDetailsProcessingState `json:"processing_state,omitempty"`
+}
+
+// EntityDetailsProcessingState represents the processing state of an action
+type EntityDetailsProcessingState struct {
+ Enabled bool `json:"enabled"`
+}
+
+// EntityPresentDetailsResponse represents the response from entity.presentDetails
+type EntityPresentDetailsResponse struct {
+ SlackResponse
+}
+
+// EntityPresentDetails presents entity details in the flexpane
+// For more details, see EntityPresentDetailsContext documentation.
+func (api *Client) EntityPresentDetails(params EntityPresentDetailsParameters) error {
+ return api.EntityPresentDetailsContext(context.Background(), params)
+}
+
+// EntityPresentDetailsContext presents entity details in the flexpane with a custom context.
+// Slack API docs: https://docs.slack.dev/reference/methods/entity.presentDetails
+func (api *Client) EntityPresentDetailsContext(ctx context.Context, params EntityPresentDetailsParameters) error {
+ values := url.Values{
+ "token": {api.token},
+ "trigger_id": {params.TriggerID},
+ }
+
+ // Add metadata if provided
+ if params.Metadata != nil {
+ metadataJSON, err := json.Marshal(params.Metadata)
+ if err != nil {
+ return err
+ }
+ values.Set("metadata", string(metadataJSON))
+ }
+
+ // Add error if provided
+ if params.Error != nil {
+ errorJSON, err := json.Marshal(params.Error)
+ if err != nil {
+ return err
+ }
+ values.Set("error", string(errorJSON))
+ }
+
+ // Add user auth parameters if provided
+ if params.UserAuthRequired {
+ values.Set("user_auth_required", "true")
+ }
+ if params.UserAuthURL != "" {
+ values.Set("user_auth_url", params.UserAuthURL)
+ }
+ if params.UserAuthMessage != "" {
+ values.Set("user_auth_message", params.UserAuthMessage)
+ }
+
+ response := &EntityPresentDetailsResponse{}
+ err := api.postMethod(ctx, "entity.presentDetails", values, response)
+ if err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
+// EntityPresentDetailsWithMetadata is a convenience method for presenting entity details with metadata
+func (api *Client) EntityPresentDetailsWithMetadata(triggerID string, metadata EntityDetailsMetadata) error {
+ return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{
+ TriggerID: triggerID,
+ Metadata: &metadata,
+ })
+}
+
+// EntityPresentDetailsWithError is a convenience method for presenting entity details with an error
+func (api *Client) EntityPresentDetailsWithError(triggerID string, errPayload EntityDetailsError) error {
+ return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{
+ TriggerID: triggerID,
+ Error: &errPayload,
+ })
+}
+
+// EntityPresentDetailsWithAuth is a convenience method for presenting entity details with authentication required
+func (api *Client) EntityPresentDetailsWithAuth(triggerID, authURL, authMessage string) error {
+ return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{
+ TriggerID: triggerID,
+ UserAuthRequired: true,
+ UserAuthURL: authURL,
+ UserAuthMessage: authMessage,
+ })
+}
diff --git a/backend/vendor/github.com/slack-go/slack/files.go b/backend/vendor/github.com/slack-go/slack/files.go
index 810c476b..729ab4ff 100644
--- a/backend/vendor/github.com/slack-go/slack/files.go
+++ b/backend/vendor/github.com/slack-go/slack/files.go
@@ -95,6 +95,9 @@ type File struct {
From []EmailFileUserInfo `json:"from"`
Cc []EmailFileUserInfo `json:"cc"`
Headers EmailHeaders `json:"headers"`
+
+ PlainText string `json:"plain_text"`
+ PreviewPlainText string `json:"preview_plain_text"`
}
type EmailFileUserInfo struct {
@@ -126,24 +129,6 @@ type ShareFileInfo struct {
TeamID string `json:"team_id"`
}
-// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request.
-//
-// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large,
-// or provide a local file path in File to upload it from your filesystem.
-//
-// Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy.
-type FileUploadParameters struct {
- File string
- Content string
- Reader io.Reader
- Filetype string
- Filename string
- Title string
- InitialComment string
- Channels []string
- ThreadTimestamp string
-}
-
// GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request
type GetFilesParameters struct {
User string
@@ -167,7 +152,7 @@ type ListFilesParameters struct {
Cursor string
}
-type UploadFileV2Parameters struct {
+type UploadFileParameters struct {
File string
FileSize int
Content string
@@ -375,73 +360,6 @@ func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParamet
return response.Files, ¶ms, nil
}
-// UploadFile uploads a file.
-//
-// Deprecated: Use [Client.UploadFileV2] instead.
-//
-// Per Slack Changelog, specifically [https://api.slack.com/changelog#entry-march_2025_1](this entry),
-// this will stop functioning on November 12, 2025.
-//
-// For more details, see: https://api.slack.com/methods/files.upload#markdown
-func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) {
- return api.UploadFileContext(context.Background(), params)
-}
-
-// UploadFileContext uploads a file and setting a custom context.
-//
-// Deprecated: Use [Client.UploadFileV2Context] instead.
-//
-// Per Slack Changelog, specifically [https://api.slack.com/changelog#entry-march_2025_1](this entry),
-// this will stop functioning on November 12, 2025.
-//
-// For more details, see: https://api.slack.com/methods/files.upload#markdown
-func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) {
- // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More
- // investigation needed, but for now this will do.
- _, err = api.AuthTestContext(ctx)
- if err != nil {
- return nil, err
- }
- response := &fileResponseFull{}
- values := url.Values{}
- if params.Filetype != "" {
- values.Add("filetype", params.Filetype)
- }
- if params.Filename != "" {
- values.Add("filename", params.Filename)
- }
- if params.Title != "" {
- values.Add("title", params.Title)
- }
- if params.InitialComment != "" {
- values.Add("initial_comment", params.InitialComment)
- }
- if params.ThreadTimestamp != "" {
- values.Add("thread_ts", params.ThreadTimestamp)
- }
- if len(params.Channels) != 0 {
- values.Add("channels", strings.Join(params.Channels, ","))
- }
- if params.Content != "" {
- values.Add("content", params.Content)
- values.Add("token", api.token)
- err = api.postMethod(ctx, "files.upload", values, response)
- } else if params.File != "" {
- err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", api.token, values, response, api)
- } else if params.Reader != nil {
- if params.Filename == "" {
- return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader")
- }
- err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", api.token, values, params.Reader, response, api)
- }
-
- if err != nil {
- return nil, err
- }
-
- return &response.File, response.Err()
-}
-
// DeleteFileComment deletes a file's comment.
// For more details, see DeleteFileCommentContext documentation.
func (api *Client) DeleteFileComment(commentID, fileID string) error {
@@ -609,19 +527,19 @@ func (api *Client) CompleteUploadExternalContext(ctx context.Context, params Com
return response, nil
}
-// UploadFileV2 uploads file to a given slack channel using 3 steps.
-// For more details, see UploadFileV2Context documentation.
-func (api *Client) UploadFileV2(params UploadFileV2Parameters) (*FileSummary, error) {
- return api.UploadFileV2Context(context.Background(), params)
+// UploadFile uploads file to a given slack channel using 3 steps.
+// For more details, see UploadFileContext documentation.
+func (api *Client) UploadFile(params UploadFileParameters) (*FileSummary, error) {
+ return api.UploadFileContext(context.Background(), params)
}
-// UploadFileV2Context uploads file to a given slack channel using 3 steps -
+// UploadFileContext uploads file to a given slack channel using 3 steps -
// 1. Get an upload URL using files.getUploadURLExternal API
// 2. Send the file as a post to the URL provided by slack
// 3. Complete the upload and share it to the specified channel using files.completeUploadExternal
//
// Slack Docs: https://api.slack.com/messaging/files#uploading_files
-func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2Parameters) (file *FileSummary, err error) {
+func (api *Client) UploadFileContext(ctx context.Context, params UploadFileParameters) (file *FileSummary, err error) {
if params.Filename == "" {
return nil, fmt.Errorf("file.upload.v2: filename cannot be empty")
}
@@ -636,7 +554,7 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P
SnippetType: params.SnippetType,
})
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("GetUploadURLExternal: %w", err)
}
err = api.UploadToURL(ctx, UploadToURLParameters{
@@ -647,7 +565,7 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P
Filename: params.Filename,
})
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("UploadToURL: %w", err)
}
c, err := api.CompleteUploadExternalContext(ctx, CompleteUploadExternalParameters{
@@ -661,7 +579,7 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P
Blocks: params.Blocks,
})
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("CompleteUploadExternal: %w", err)
}
if len(c.Files) != 1 {
return nil, fmt.Errorf("file.upload.v2: something went wrong; received %d files instead of 1", len(c.Files))
diff --git a/backend/vendor/github.com/slack-go/slack/function_execute.go b/backend/vendor/github.com/slack-go/slack/function_execute.go
index 4ec8f9f4..97bc7e15 100644
--- a/backend/vendor/github.com/slack-go/slack/function_execute.go
+++ b/backend/vendor/github.com/slack-go/slack/function_execute.go
@@ -43,14 +43,13 @@ func (api *Client) FunctionCompleteSuccessContext(ctx context.Context, functionE
option(r)
}
- endpoint := api.endpoint + "functions.completeSuccess"
jsonData, err := json.Marshal(r)
if err != nil {
return err
}
response := &SlackResponse{}
- if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil {
+ if err := api.postJSONMethod(ctx, "functions.completeSuccess", api.token, jsonData, response); err != nil {
return err
}
@@ -74,14 +73,13 @@ func (api *Client) FunctionCompleteErrorContext(ctx context.Context, functionExe
}
r.Error = errorMessage
- endpoint := api.endpoint + "functions.completeError"
jsonData, err := json.Marshal(r)
if err != nil {
return err
}
response := &SlackResponse{}
- if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil {
+ if err := api.postJSONMethod(ctx, "functions.completeError", api.token, jsonData, response); err != nil {
return err
}
diff --git a/backend/vendor/github.com/slack-go/slack/huddle.go b/backend/vendor/github.com/slack-go/slack/huddle.go
new file mode 100644
index 00000000..70123647
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/huddle.go
@@ -0,0 +1,64 @@
+package slack
+
+// HuddleRoom represents a Slack huddle room as it appears in message events
+// with subtype "huddle_thread". This is different from CallBlock which is used
+// for external call integrations (Zoom, etc.).
+type HuddleRoom struct {
+ ID string `json:"id"`
+ Name string `json:"name,omitempty"`
+ MediaServer string `json:"media_server,omitempty"`
+ CreatedBy string `json:"created_by,omitempty"`
+ DateStart int64 `json:"date_start"`
+ DateEnd int64 `json:"date_end"`
+ Participants []string `json:"participants,omitempty"`
+ ParticipantHistory []string `json:"participant_history,omitempty"`
+ ParticipantsEvents map[string]HuddleParticipantEvent `json:"participants_events,omitempty"`
+ ParticipantsCameraOn []string `json:"participants_camera_on,omitempty"`
+ ParticipantsCameraOff []string `json:"participants_camera_off,omitempty"`
+ ParticipantsScreenshareOn []string `json:"participants_screenshare_on,omitempty"`
+ ParticipantsScreenshareOff []string `json:"participants_screenshare_off,omitempty"`
+ CanvasThreadTs string `json:"canvas_thread_ts,omitempty"`
+ ThreadRootTs string `json:"thread_root_ts,omitempty"`
+ Channels []string `json:"channels,omitempty"`
+ IsDMCall bool `json:"is_dm_call"`
+ WasRejected bool `json:"was_rejected"`
+ WasMissed bool `json:"was_missed"`
+ WasAccepted bool `json:"was_accepted"`
+ HasEnded bool `json:"has_ended"`
+ BackgroundID string `json:"background_id,omitempty"`
+ CanvasBackground string `json:"canvas_background,omitempty"`
+ IsPrewarmed bool `json:"is_prewarmed"`
+ IsScheduled bool `json:"is_scheduled"`
+ Recording *HuddleRecording `json:"recording,omitempty"`
+ Locale string `json:"locale,omitempty"`
+ AttachedFileIDs []string `json:"attached_file_ids,omitempty"`
+ MediaBackendType string `json:"media_backend_type,omitempty"`
+ DisplayID string `json:"display_id,omitempty"`
+ ExternalUniqueID string `json:"external_unique_id,omitempty"`
+ AppID string `json:"app_id,omitempty"`
+ CallFamily string `json:"call_family,omitempty"`
+ PendingInvitees map[string]any `json:"pending_invitees,omitempty"`
+ LastInviteStatusByUser map[string]any `json:"last_invite_status_by_user,omitempty"`
+ Knocks map[string]any `json:"knocks,omitempty"`
+ HuddleLink string `json:"huddle_link,omitempty"`
+}
+
+// HuddleParticipantEvent tracks a participant's activity in a huddle.
+type HuddleParticipantEvent struct {
+ UserTeam map[string]any `json:"user_team,omitempty"`
+ Joined bool `json:"joined"`
+ CameraOn bool `json:"camera_on"`
+ CameraOff bool `json:"camera_off"`
+ ScreenshareOn bool `json:"screenshare_on"`
+ ScreenshareOff bool `json:"screenshare_off"`
+}
+
+// HuddleRecording contains recording status for a huddle.
+type HuddleRecording struct {
+ CanRecordSummary string `json:"can_record_summary,omitempty"`
+ NoteTaking bool `json:"note_taking,omitempty"`
+ Summary bool `json:"summary,omitempty"`
+ SummaryStatus string `json:"summary_status,omitempty"`
+ Transcript bool `json:"transcript,omitempty"`
+ RecordingUser string `json:"recording_user,omitempty"`
+}
diff --git a/backend/vendor/github.com/slack-go/slack/im.go b/backend/vendor/github.com/slack-go/slack/im.go
deleted file mode 100644
index 7c4bc257..00000000
--- a/backend/vendor/github.com/slack-go/slack/im.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package slack
-
-type imChannel struct {
- ID string `json:"id"`
-}
-
-type imResponseFull struct {
- NoOp bool `json:"no_op"`
- AlreadyClosed bool `json:"already_closed"`
- AlreadyOpen bool `json:"already_open"`
- Channel imChannel `json:"channel"`
- IMs []IM `json:"ims"`
- History
- SlackResponse
-}
-
-// IM contains information related to the Direct Message channel
-type IM struct {
- Conversation
- IsUserDeleted bool `json:"is_user_deleted"`
-}
diff --git a/backend/vendor/github.com/slack-go/slack/info.go b/backend/vendor/github.com/slack-go/slack/info.go
index a026ab49..d5276f75 100644
--- a/backend/vendor/github.com/slack-go/slack/info.go
+++ b/backend/vendor/github.com/slack-go/slack/info.go
@@ -431,7 +431,7 @@ type Team struct {
Icons *Icons `json:"icon,omitempty"`
}
-// Icons XXX: needs further investigation
+// Icons contains the image URLs for the team icons in various sizes
type Icons struct {
Image36 string `json:"image_36,omitempty"`
Image48 string `json:"image_48,omitempty"`
@@ -452,28 +452,3 @@ type infoResponseFull struct {
Info
SlackResponse
}
-
-// GetBotByID is deprecated and returns nil
-func (info Info) GetBotByID(botID string) *Bot {
- return nil
-}
-
-// GetUserByID is deprecated and returns nil
-func (info Info) GetUserByID(userID string) *User {
- return nil
-}
-
-// GetChannelByID is deprecated and returns nil
-func (info Info) GetChannelByID(channelID string) *Channel {
- return nil
-}
-
-// GetGroupByID is deprecated and returns nil
-func (info Info) GetGroupByID(groupID string) *Group {
- return nil
-}
-
-// GetIMByID is deprecated and returns nil
-func (info Info) GetIMByID(imID string) *IM {
- return nil
-}
diff --git a/backend/vendor/github.com/slack-go/slack/manifests.go b/backend/vendor/github.com/slack-go/slack/manifests.go
index 0a972a25..ab6b1f2f 100644
--- a/backend/vendor/github.com/slack-go/slack/manifests.go
+++ b/backend/vendor/github.com/slack-go/slack/manifests.go
@@ -187,11 +187,11 @@ type Display struct {
// Settings is a group of settings corresponding to the Settings section of the app config pages.
type Settings struct {
- AllowedIPAddressRanges []string `json:"allowed_ip_address_ranges,omitempty" yaml:"allowed_ip_address_ranges,omitempty"`
- EventSubscriptions EventSubscriptions `json:"event_subscriptions,omitempty" yaml:"event_subscriptions,omitempty"`
- Interactivity Interactivity `json:"interactivity,omitempty" yaml:"interactivity,omitempty"`
- OrgDeployEnabled bool `json:"org_deploy_enabled,omitempty" yaml:"org_deploy_enabled,omitempty"`
- SocketModeEnabled bool `json:"socket_mode_enabled,omitempty" yaml:"socket_mode_enabled,omitempty"`
+ AllowedIPAddressRanges []string `json:"allowed_ip_address_ranges,omitempty" yaml:"allowed_ip_address_ranges,omitempty"`
+ EventSubscriptions *EventSubscriptions `json:"event_subscriptions,omitempty" yaml:"event_subscriptions,omitempty"`
+ Interactivity *Interactivity `json:"interactivity,omitempty" yaml:"interactivity,omitempty"`
+ OrgDeployEnabled bool `json:"org_deploy_enabled,omitempty" yaml:"org_deploy_enabled,omitempty"`
+ SocketModeEnabled bool `json:"socket_mode_enabled,omitempty" yaml:"socket_mode_enabled,omitempty"`
}
// EventSubscriptions is a group of settings that describe the Events API configuration
diff --git a/backend/vendor/github.com/slack-go/slack/messages.go b/backend/vendor/github.com/slack-go/slack/messages.go
index c53809fd..332e15e9 100644
--- a/backend/vendor/github.com/slack-go/slack/messages.go
+++ b/backend/vendor/github.com/slack-go/slack/messages.go
@@ -88,6 +88,16 @@ type Msg struct {
Icons *Icon `json:"icons,omitempty"`
BotProfile *BotProfile `json:"bot_profile,omitempty"`
+ // These tend to be present in some of the messages, especially when triggered through
+ // a workflow. The API documentation is not clear about which ones are present in
+ // which messages, so we make them all optional.
+ //
+ // I'm adding them here for completeness but none of the Slack official libraries seem
+ // to support these fields. Be warned that they may be removed in future versions of
+ // the API, and that they may not be present in all messages.
+ TriggerID string `json:"trigger_id,omitempty"`
+ WorkflowID string `json:"workflow_id,omitempty"`
+
// channel_join, group_join
Inviter string `json:"inviter,omitempty"`
diff --git a/backend/vendor/github.com/slack-go/slack/metadata.go b/backend/vendor/github.com/slack-go/slack/metadata.go
index a8c06504..a1e34e99 100644
--- a/backend/vendor/github.com/slack-go/slack/metadata.go
+++ b/backend/vendor/github.com/slack-go/slack/metadata.go
@@ -2,6 +2,37 @@ package slack
// SlackMetadata https://api.slack.com/reference/metadata
type SlackMetadata struct {
- EventType string `json:"event_type"`
- EventPayload map[string]interface{} `json:"event_payload"`
+ EventType string `json:"event_type"`
+ EventPayload map[string]any `json:"event_payload"`
+}
+
+// Work Object entity type constants.
+// See https://docs.slack.dev/messaging/work-objects/
+const (
+ EntityTypeTask = "slack#/entities/task"
+ EntityTypeFile = "slack#/entities/file"
+ EntityTypeItem = "slack#/entities/item"
+ EntityTypeIncident = "slack#/entities/incident"
+ EntityTypeContentItem = "slack#/entities/content_item"
+)
+
+// WorkObjectExternalRef represents an external reference for a Work Object
+type WorkObjectExternalRef struct {
+ ID string `json:"id"`
+ Type string `json:"type,omitempty"`
+}
+
+// WorkObjectEntity represents a single Work Object entity
+type WorkObjectEntity struct {
+ AppUnfurlURL string `json:"app_unfurl_url,omitempty"`
+ URL string `json:"url"`
+ ExternalRef WorkObjectExternalRef `json:"external_ref"`
+ EntityType string `json:"entity_type"`
+ EntityPayload map[string]interface{} `json:"entity_payload"`
+}
+
+// WorkObjectMetadata represents the metadata for Work Objects
+// Used in chat.unfurl and chat.postMessage for Work Objects support
+type WorkObjectMetadata struct {
+ Entities []WorkObjectEntity `json:"entities"`
}
diff --git a/backend/vendor/github.com/slack-go/slack/misc.go b/backend/vendor/github.com/slack-go/slack/misc.go
index 48411ee1..46060a72 100644
--- a/backend/vendor/github.com/slack-go/slack/misc.go
+++ b/backend/vendor/github.com/slack-go/slack/misc.go
@@ -19,7 +19,7 @@ import (
"time"
)
-// Apps Manifest Create Response Errors ("/apps.manifest.create")
+// AppsManifestCreateResponseError ("/apps.manifest.create")
type AppsManifestCreateResponseError struct {
Code string `json:"code,omitempty"`
Message string `json:"message"`
@@ -27,7 +27,7 @@ type AppsManifestCreateResponseError struct {
RelatedComponent string `json:"related_component,omitempty"`
}
-// Conversations Invite Response Errors ("/conversations.invite")
+// ConversationsInviteResponseError ("/conversations.invite")
type ConversationsInviteResponseError struct {
Error string `json:"error"`
Ok bool `json:"ok"`
@@ -69,7 +69,7 @@ func (e *SlackResponseErrors) UnmarshalJSON(data []byte) error {
}
// Try to determine the error type by checking for unique fields
- var raw map[string]interface{}
+ var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
// If we can't unmarshal as object, try as string (fallback case)
//
@@ -114,10 +114,127 @@ func (e *SlackResponseErrors) UnmarshalJSON(data []byte) error {
type SlackResponse struct {
Ok bool `json:"ok"`
Error string `json:"error"`
+ Warning string `json:"warning"`
Errors []SlackResponseErrors `json:"errors,omitempty"`
ResponseMetadata ResponseMetadata `json:"response_metadata"`
}
+// Warn returns warning information from the API response, or nil if there
+// are no warnings.
+func (t SlackResponse) Warn() *Warning {
+ if t.Warning == "" && len(t.ResponseMetadata.Warnings) == 0 {
+ return nil
+ }
+ return &Warning{
+ Codes: strings.Split(t.Warning, ","),
+ Warnings: t.ResponseMetadata.Warnings,
+ }
+}
+
+// warner is satisfied by any response type that can report warnings.
+type warner interface {
+ Warn() *Warning
+}
+
+// Warning provides warning information from the web API.
+// https://docs.slack.dev/apis/web-api/#responses
+type Warning struct {
+ Codes []string
+ Warnings []string
+}
+
+// httpHeaderSetter is satisfied by response types that can store HTTP
+// response headers. The response parser checks for this interface and
+// injects headers before JSON decoding.
+type httpHeaderSetter interface {
+ setHTTPResponseHeaders(http.Header)
+}
+
+// responseHeaders is a mix-in for internal response types that need to
+// capture HTTP response headers. Embedded in types like authTestResponseFull
+// so the parser can store headers that are then propagated to the public
+// response type (e.g. AuthTestResponse.Header).
+type responseHeaders struct {
+ header http.Header
+}
+
+func (r *responseHeaders) setHTTPResponseHeaders(h http.Header) { r.header = h }
+
+// KickUserFromConversationSlackResponse is a variant of SlackResponse that can handle the case where
+// "errors" can be either an empty object {} or an array of errors.
+// This addresses issue #1446 where conversations.kick endpoint returns {"ok":true,"errors":{}}
+type KickUserFromConversationSlackResponse struct {
+ Ok bool `json:"ok"`
+ Error string `json:"error"`
+ Warning string `json:"warning"`
+ Errors []SlackResponseErrors `json:"-"`
+ ResponseMetadata ResponseMetadata `json:"response_metadata"`
+}
+
+// UnmarshalJSON implements custom unmarshaling for KickUserFromConversationSlackResponse to handle
+// the case where "errors" can be either an empty object {} or an array of errors
+func (s *KickUserFromConversationSlackResponse) UnmarshalJSON(data []byte) error {
+ // First, unmarshal everything except errors
+ type Alias KickUserFromConversationSlackResponse
+ aux := &struct {
+ *Alias
+ ErrorsRaw json.RawMessage `json:"errors,omitempty"`
+ }{
+ Alias: (*Alias)(s),
+ }
+
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+
+ // Handle the errors field
+ if len(aux.ErrorsRaw) > 0 {
+ // Check if it's an empty object by looking for just "{}"
+ trimmed := bytes.TrimSpace(aux.ErrorsRaw)
+ if bytes.Equal(trimmed, []byte("{}")) {
+ // Empty object, leave errors as nil/empty slice
+ s.Errors = nil
+ } else {
+ // Try to unmarshal as array of errors
+ var errors []SlackResponseErrors
+ if err := json.Unmarshal(aux.ErrorsRaw, &errors); err != nil {
+ return err
+ }
+ s.Errors = errors
+ }
+ }
+
+ return nil
+}
+
+// Warn returns warning information from the API response, or nil if there
+// are no warnings.
+func (s KickUserFromConversationSlackResponse) Warn() *Warning {
+ if s.Warning == "" && len(s.ResponseMetadata.Warnings) == 0 {
+ return nil
+ }
+ return &Warning{
+ Codes: strings.Split(s.Warning, ","),
+ Warnings: s.ResponseMetadata.Warnings,
+ }
+}
+
+// Err returns any API error present in the response.
+func (s KickUserFromConversationSlackResponse) Err() error {
+ if s.Ok {
+ return nil
+ }
+
+ // handle pure text based responses like chat.post
+ // which while they have a slack response in their data structure
+ // it doesn't actually get set during parsing.
+ if strings.TrimSpace(s.Error) == "" {
+ return nil
+ }
+
+ return SlackErrorResponse{Err: s.Error, Errors: s.Errors, ResponseMetadata: s.ResponseMetadata}
+}
+
func (t SlackResponse) Err() error {
if t.Ok {
return nil
@@ -203,7 +320,7 @@ func formReq(ctx context.Context, endpoint string, values url.Values) (req *http
return req, nil
}
-func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http.Request, err error) {
+func jsonReq(ctx context.Context, endpoint string, body any) (req *http.Request, err error) {
buffer := bytes.NewBuffer([]byte{})
if err = json.NewEncoder(buffer).Encode(body); err != nil {
return nil, err
@@ -217,7 +334,7 @@ func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http.
return req, nil
}
-func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname, token string, values url.Values, intf interface{}, d Debug) error {
+func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname, token string, values url.Values, intf any, d Debug) error {
fullpath, err := filepath.Abs(fpath)
if err != nil {
return err
@@ -231,7 +348,7 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, meth
return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, token, values, file, intf, d)
}
-func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname, token string, values url.Values, r io.Reader, intf interface{}, d Debug) error {
+func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname, token string, values url.Values, r io.Reader, intf any, d Debug) error {
pipeReader, pipeWriter := io.Pipe()
wr := multipart.NewWriter(pipeWriter)
@@ -300,49 +417,54 @@ func createFormFields(mw *multipart.Writer, values url.Values) error {
return nil
}
-func doPost(client httpClient, req *http.Request, parser responseParser, d Debug) error {
+func doPost(client httpClient, req *http.Request, parser responseParser, d Debug) (http.Header, error) {
resp, err := client.Do(req)
if err != nil {
- return err
+ return nil, err
}
defer resp.Body.Close()
- err = checkStatusCode(resp, d)
- if err != nil {
- return err
+ if err = checkStatusCode(resp, d); err != nil {
+ return nil, err
}
- return parser(resp)
+ return resp.Header, parser(resp)
}
// post JSON.
-func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d Debug) error {
- reqBody := bytes.NewBuffer(json)
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody)
+func postJSON(ctx context.Context, client httpClient, endpoint, token string, jsonBody []byte, intf any, d Debug) (http.Header, error) {
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(jsonBody))
if err != nil {
- return err
+ return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
-
+ // allow retry client to re-send the request body on 429/5xx.
+ req.GetBody = func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(jsonBody)), nil
+ }
return doPost(client, req, newJSONParser(intf), d)
}
// post a url encoded form.
-func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d Debug) error {
- reqBody := strings.NewReader(values.Encode())
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody)
+func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf any, d Debug) (http.Header, error) {
+ body := values.Encode()
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(body))
if err != nil {
- return err
+ return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ // allow retry client to re-send the request body on 429/5xx.
+ req.GetBody = func() (io.ReadCloser, error) {
+ return io.NopCloser(strings.NewReader(body)), nil
+ }
return doPost(client, req, newJSONParser(intf), d)
}
-func getResource(ctx context.Context, client httpClient, endpoint, token string, values url.Values, intf interface{}, d Debug) error {
+func getResource(ctx context.Context, client httpClient, endpoint, token string, values url.Values, intf any, d Debug) (http.Header, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
- return err
+ return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
@@ -352,9 +474,10 @@ func getResource(ctx context.Context, client httpClient, endpoint, token string,
return doPost(client, req, newJSONParser(intf), d)
}
-func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d Debug) error {
+func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf any, d Debug) error {
endpoint := fmt.Sprintf(WEBAPIURLFormat, teamName, method, time.Now().Unix())
- return postForm(ctx, client, endpoint, values, intf, d)
+ _, err := postForm(ctx, client, endpoint, values, intf, d)
+ return err
}
func logResponse(resp *http.Response, d Debug) error {
@@ -397,16 +520,19 @@ func checkStatusCode(resp *http.Response, d Debug) error {
type responseParser func(*http.Response) error
-func newJSONParser(dst interface{}) responseParser {
+func newJSONParser(dst any) responseParser {
return func(resp *http.Response) error {
if dst == nil {
return nil
}
+ if hs, ok := dst.(httpHeaderSetter); ok {
+ hs.setHTTPResponseHeaders(resp.Header.Clone())
+ }
return json.NewDecoder(resp.Body).Decode(dst)
}
}
-func newTextParser(dst interface{}) responseParser {
+func newTextParser(dst any) responseParser {
return func(resp *http.Response) error {
if dst == nil {
return nil
@@ -425,7 +551,7 @@ func newTextParser(dst interface{}) responseParser {
}
}
-func newContentTypeParser(dst interface{}) responseParser {
+func newContentTypeParser(dst any) responseParser {
return func(req *http.Response) (err error) {
var (
ctype string
@@ -439,6 +565,10 @@ func newContentTypeParser(dst interface{}) responseParser {
case "application/json":
return newJSONParser(dst)(req)
default:
+ // newTextParser doesn't use dst, so capture headers here.
+ if hs, ok := dst.(httpHeaderSetter); ok {
+ hs.setHTTPResponseHeaders(req.Header.Clone())
+ }
return newTextParser(dst)(req)
}
}
diff --git a/backend/vendor/github.com/slack-go/slack/mise.toml b/backend/vendor/github.com/slack-go/slack/mise.toml
new file mode 100644
index 00000000..ce5d07a9
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/mise.toml
@@ -0,0 +1,3 @@
+[tools]
+go = "1.25"
+golangci-lint = "2.10.1"
diff --git a/backend/vendor/github.com/slack-go/slack/oauth.go b/backend/vendor/github.com/slack-go/slack/oauth.go
index 0c77eca4..1df1369c 100644
--- a/backend/vendor/github.com/slack-go/slack/oauth.go
+++ b/backend/vendor/github.com/slack-go/slack/oauth.go
@@ -79,16 +79,36 @@ type OpenIDConnectResponse struct {
SlackResponse
}
+type oauthConfig struct {
+ apiURL string
+}
+
+// OAuthOption configures package-level OAuth functions.
+type OAuthOption func(*oauthConfig)
+
+// OAuthOptionAPIURL overrides the default Slack API URL. Useful for testing.
+func OAuthOptionAPIURL(url string) OAuthOption {
+ return func(c *oauthConfig) { c.apiURL = url }
+}
+
+func resolveOAuthAPIURL(opts []OAuthOption) string {
+ c := oauthConfig{apiURL: APIURL}
+ for _, o := range opts {
+ o(&c)
+ }
+ return c.apiURL
+}
+
// GetOAuthToken retrieves an AccessToken.
// For more details, see GetOAuthTokenContext documentation.
-func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
- return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
+func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, err error) {
+ return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...)
}
// GetOAuthTokenContext retrieves an AccessToken with a custom context.
// For more details, see GetOAuthResponseContext documentation.
-func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
- response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI)
+func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, err error) {
+ response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI, opts...)
if err != nil {
return "", "", err
}
@@ -97,14 +117,14 @@ func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clie
// GetBotOAuthToken retrieves top-level and bot AccessToken - https://api.slack.com/legacy/oauth#bot_user_access_tokens
// For more details, see GetBotOAuthTokenContext documentation.
-func GetBotOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, bot OAuthResponseBot, err error) {
- return GetBotOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
+func GetBotOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, bot OAuthResponseBot, err error) {
+ return GetBotOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...)
}
// GetBotOAuthTokenContext retrieves top-level and bot AccessToken with a custom context.
// For more details, see GetOAuthResponseContext documentation.
-func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, bot OAuthResponseBot, err error) {
- response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI)
+func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, bot OAuthResponseBot, err error) {
+ response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI, opts...)
if err != nil {
return "", "", OAuthResponseBot{}, err
}
@@ -113,13 +133,13 @@ func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, c
// GetOAuthResponse retrieves OAuth response.
// For more details, see GetOAuthResponseContext documentation.
-func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) {
- return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
+func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthResponse, err error) {
+ return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...)
}
// GetOAuthResponseContext retrieves OAuth response with custom context.
// Slack API docs: https://api.slack.com/methods/oauth.access
-func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) {
+func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthResponse, err error) {
values := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
@@ -127,7 +147,7 @@ func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, c
"redirect_uri": {redirectURI},
}
response := &OAuthResponse{}
- if err = postForm(ctx, client, APIURL+"oauth.access", values, response, discard{}); err != nil {
+ if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"oauth.access", values, response, discard{}); err != nil {
return nil, err
}
return response, response.Err()
@@ -135,13 +155,13 @@ func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, c
// GetOAuthV2Response gets a V2 OAuth access token response.
// For more details, see GetOAuthV2ResponseContext documentation.
-func GetOAuthV2Response(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthV2Response, err error) {
- return GetOAuthV2ResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
+func GetOAuthV2Response(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthV2Response, err error) {
+ return GetOAuthV2ResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...)
}
// GetOAuthV2ResponseContext with a context, gets a V2 OAuth access token response.
// Slack API docs: https://api.slack.com/methods/oauth.v2.access
-func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthV2Response, err error) {
+func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthV2Response, err error) {
values := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
@@ -149,7 +169,7 @@ func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID,
"redirect_uri": {redirectURI},
}
response := &OAuthV2Response{}
- if err = postForm(ctx, client, APIURL+"oauth.v2.access", values, response, discard{}); err != nil {
+ if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"oauth.v2.access", values, response, discard{}); err != nil {
return nil, err
}
return response, response.Err()
@@ -157,13 +177,13 @@ func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID,
// RefreshOAuthV2Token with a context, gets a V2 OAuth access token response.
// For more details, see RefreshOAuthV2TokenContext documentation.
-func RefreshOAuthV2Token(client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) {
- return RefreshOAuthV2TokenContext(context.Background(), client, clientID, clientSecret, refreshToken)
+func RefreshOAuthV2Token(client httpClient, clientID, clientSecret, refreshToken string, opts ...OAuthOption) (resp *OAuthV2Response, err error) {
+ return RefreshOAuthV2TokenContext(context.Background(), client, clientID, clientSecret, refreshToken, opts...)
}
// RefreshOAuthV2TokenContext with a context, gets a V2 OAuth access token response.
// Slack API docs: https://api.slack.com/methods/oauth.v2.access
-func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) {
+func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID, clientSecret, refreshToken string, opts ...OAuthOption) (resp *OAuthV2Response, err error) {
values := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
@@ -171,7 +191,75 @@ func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID
"grant_type": {"refresh_token"},
}
response := &OAuthV2Response{}
- if err = postForm(ctx, client, APIURL+"oauth.v2.access", values, response, discard{}); err != nil {
+ if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"oauth.v2.access", values, response, discard{}); err != nil {
+ return nil, err
+ }
+ return response, response.Err()
+}
+
+// OpenIDConnectUserInfoResponse contains the response from openid.connect.userInfo.
+//
+// Some of the fields in the response to this method are preceded with https://slack.com/.
+// These fields are Slack-specific, and they're from the perspective of Slack.
+type OpenIDConnectUserInfoResponse struct {
+ Ok bool `json:"ok"`
+
+ Sub string `json:"sub"`
+
+ UserID string `json:"https://slack.com/user_id"`
+ TeamID string `json:"https://slack.com/team_id"`
+
+ Email string `json:"email"`
+ EmailVerified bool `json:"email_verified"`
+ DateEmailVerified int64 `json:"date_email_verified"`
+
+ Name string `json:"name"`
+ Picture string `json:"picture"`
+ GivenName string `json:"given_name"`
+ FamilyName string `json:"family_name"`
+ Locale string `json:"locale"`
+
+ TeamName string `json:"https://slack.com/team_name"`
+ TeamDomain string `json:"https://slack.com/team_domain"`
+ TeamImage34 string `json:"https://slack.com/team_image_34"`
+ TeamImage44 string `json:"https://slack.com/team_image_44"`
+ TeamImage68 string `json:"https://slack.com/team_image_68"`
+ TeamImage88 string `json:"https://slack.com/team_image_88"`
+ TeamImage102 string `json:"https://slack.com/team_image_102"`
+ TeamImage132 string `json:"https://slack.com/team_image_132"`
+ TeamImage230 string `json:"https://slack.com/team_image_230"`
+
+ // `TeamImageDefault` indicates whether the image is a default one (true), or someone
+ // uploaded their own (false).
+ TeamImageDefault bool `json:"https://slack.com/team_image_default"`
+
+ UserImage24 string `json:"https://slack.com/user_image_24"`
+ UserImage32 string `json:"https://slack.com/user_image_32"`
+ UserImage48 string `json:"https://slack.com/user_image_48"`
+ UserImage72 string `json:"https://slack.com/user_image_72"`
+ UserImage192 string `json:"https://slack.com/user_image_192"`
+ UserImage512 string `json:"https://slack.com/user_image_512"`
+ UserImage1024 string `json:"https://slack.com/user_image_1024"`
+ UserImageOriginal string `json:"https://slack.com/user_image_original"`
+
+ SlackResponse
+}
+
+// GetOpenIDConnectUserInfo returns the user info for the token.
+// For more details, see GetOpenIDConnectUserInfoContext documentation.
+func (api *Client) GetOpenIDConnectUserInfo() (*OpenIDConnectUserInfoResponse, error) {
+ return api.GetOpenIDConnectUserInfoContext(context.Background())
+}
+
+// GetOpenIDConnectUserInfoContext returns identity information about the user associated with the token.
+// Slack API docs: https://docs.slack.dev/reference/methods/openid.connect.userInfo
+func (api *Client) GetOpenIDConnectUserInfoContext(ctx context.Context) (*OpenIDConnectUserInfoResponse, error) {
+ values := url.Values{
+ "token": {api.token},
+ }
+ response := &OpenIDConnectUserInfoResponse{}
+ err := api.postMethod(ctx, "openid.connect.userInfo", values, response)
+ if err != nil {
return nil, err
}
return response, response.Err()
@@ -179,13 +267,13 @@ func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID
// GetOpenIDConnectToken exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
// For more details, see GetOpenIDConnectTokenContext documentation.
-func GetOpenIDConnectToken(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OpenIDConnectResponse, err error) {
- return GetOpenIDConnectTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
+func GetOpenIDConnectToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OpenIDConnectResponse, err error) {
+ return GetOpenIDConnectTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...)
}
// GetOpenIDConnectTokenContext with a context, gets an access token for Sign in with Slack.
// Slack API docs: https://api.slack.com/methods/openid.connect.token
-func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OpenIDConnectResponse, err error) {
+func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OpenIDConnectResponse, err error) {
values := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
@@ -193,7 +281,7 @@ func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, client
"redirect_uri": {redirectURI},
}
response := &OpenIDConnectResponse{}
- if err = postForm(ctx, client, APIURL+"openid.connect.token", values, response, discard{}); err != nil {
+ if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"openid.connect.token", values, response, discard{}); err != nil {
return nil, err
}
return response, response.Err()
diff --git a/backend/vendor/github.com/slack-go/slack/reactions.go b/backend/vendor/github.com/slack-go/slack/reactions.go
index 240f5ba9..18befa69 100644
--- a/backend/vendor/github.com/slack-go/slack/reactions.go
+++ b/backend/vendor/github.com/slack-go/slack/reactions.go
@@ -33,44 +33,53 @@ func NewGetReactionsParameters() GetReactionsParameters {
}
type getReactionsResponseFull struct {
- Type string
- M struct {
- Reactions []ItemReaction
+ Type string
+ Channel string `json:"channel,omitempty"` // channel is at the root level for message types
+ M struct {
+ *Message // message structure already contains reactions
} `json:"message"`
F struct {
+ *File
Reactions []ItemReaction
} `json:"file"`
FC struct {
+ *Comment
Reactions []ItemReaction
} `json:"comment"`
SlackResponse
}
-func (res getReactionsResponseFull) extractReactions() []ItemReaction {
- switch res.Type {
+func (res getReactionsResponseFull) extractReactedItem() ReactedItem {
+ item := ReactedItem{}
+ item.Type = res.Type
+
+ switch item.Type {
case "message":
- return res.M.Reactions
+ item.Channel = res.Channel
+ item.Message = res.M.Message
+ item.Reactions = res.M.Reactions
case "file":
- return res.F.Reactions
+ item.File = res.F.File
+ item.Reactions = res.F.Reactions
case "file_comment":
- return res.FC.Reactions
+ item.File = res.F.File
+ item.Comment = res.FC.Comment
+ item.Reactions = res.FC.Reactions
}
- return []ItemReaction{}
+ return item
}
const (
- DEFAULT_REACTIONS_USER = ""
- DEFAULT_REACTIONS_COUNT = 100
- DEFAULT_REACTIONS_PAGE = 1
- DEFAULT_REACTIONS_FULL = false
+ DEFAULT_REACTIONS_USER = ""
+ DEFAULT_REACTIONS_FULL = false
)
// ListReactionsParameters is the inputs to find all reactions by a user.
type ListReactionsParameters struct {
User string
TeamID string
- Count int
- Page int
+ Cursor string
+ Limit int
Full bool
}
@@ -78,10 +87,8 @@ type ListReactionsParameters struct {
// performed by a user.
func NewListReactionsParameters() ListReactionsParameters {
return ListReactionsParameters{
- User: DEFAULT_REACTIONS_USER,
- Count: DEFAULT_REACTIONS_COUNT,
- Page: DEFAULT_REACTIONS_PAGE,
- Full: DEFAULT_REACTIONS_FULL,
+ User: DEFAULT_REACTIONS_USER,
+ Full: DEFAULT_REACTIONS_FULL,
}
}
@@ -101,8 +108,8 @@ type listReactionsResponseFull struct {
Reactions []ItemReaction
} `json:"comment"`
}
- Paging `json:"paging"`
SlackResponse
+ ResponseMetadata `json:"response_metadata"`
}
func (res listReactionsResponseFull) extractReactedItems() []ReactedItem {
@@ -200,15 +207,15 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item
return response.Err()
}
-// GetReactions returns details about the reactions on an item.
+// GetReactions returns item and details about the reactions on an item.
// For more details, see GetReactionsContext documentation.
-func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
+func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) (ReactedItem, error) {
return api.GetReactionsContext(context.Background(), item, params)
}
-// GetReactionsContext returns details about the reactions on an item with a custom context.
+// GetReactionsContext returns item and details about the reactions on an item with a custom context.
// Slack API docs: https://api.slack.com/methods/reactions.get
-func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
+func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) (ReactedItem, error) {
values := url.Values{
"token": {api.token},
}
@@ -224,31 +231,31 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params
if item.Comment != "" {
values.Set("file_comment", item.Comment)
}
- if params.Full != DEFAULT_REACTIONS_FULL {
+ if params.Full {
values.Set("full", strconv.FormatBool(params.Full))
}
response := &getReactionsResponseFull{}
if err := api.postMethod(ctx, "reactions.get", values, response); err != nil {
- return nil, err
+ return ReactedItem{}, err
}
if err := response.Err(); err != nil {
- return nil, err
+ return ReactedItem{}, err
}
- return response.extractReactions(), nil
+ return response.extractReactedItem(), nil
}
// ListReactions returns information about the items a user reacted to.
// For more details, see ListReactionsContext documentation.
-func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
+func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, string, error) {
return api.ListReactionsContext(context.Background(), params)
}
// ListReactionsContext returns information about the items a user reacted to with a custom context.
// Slack API docs: https://api.slack.com/methods/reactions.list
-func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
+func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, string, error) {
values := url.Values{
"token": {api.token},
}
@@ -258,25 +265,25 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction
if params.TeamID != "" {
values.Add("team_id", params.TeamID)
}
- if params.Count != DEFAULT_REACTIONS_COUNT {
- values.Add("count", strconv.Itoa(params.Count))
+ if params.Cursor != "" {
+ values.Add("cursor", params.Cursor)
}
- if params.Page != DEFAULT_REACTIONS_PAGE {
- values.Add("page", strconv.Itoa(params.Page))
+ if params.Limit != 0 {
+ values.Add("limit", strconv.Itoa(params.Limit))
}
- if params.Full != DEFAULT_REACTIONS_FULL {
+ if params.Full {
values.Add("full", strconv.FormatBool(params.Full))
}
response := &listReactionsResponseFull{}
err := api.postMethod(ctx, "reactions.list", values, response)
if err != nil {
- return nil, nil, err
+ return nil, "", err
}
if err := response.Err(); err != nil {
- return nil, nil, err
+ return nil, "", err
}
- return response.extractReactedItems(), &response.Paging, nil
+ return response.extractReactedItems(), response.ResponseMetadata.Cursor, nil
}
diff --git a/backend/vendor/github.com/slack-go/slack/remotefiles.go b/backend/vendor/github.com/slack-go/slack/remotefiles.go
index 42639a17..51d3e856 100644
--- a/backend/vendor/github.com/slack-go/slack/remotefiles.go
+++ b/backend/vendor/github.com/slack-go/slack/remotefiles.go
@@ -56,6 +56,10 @@ type RemoteFile struct {
// ExternalID is a user defined GUID, ExternalURL is where the remote file can be accessed,
// and Title is the name of the file.
//
+// PreviewImage is a file path to upload as preview. PreviewImageReader is an io.Reader
+// alternative. When using PreviewImageReader, set PreviewImageName to specify the filename
+// with proper extension (e.g., "preview.jpg") to preserve image format.
+//
// For more details:
// https://api.slack.com/methods/files.remote.add
type RemoteFileParameters struct {
@@ -66,6 +70,7 @@ type RemoteFileParameters struct {
IndexableFileContents string
PreviewImage string
PreviewImageReader io.Reader
+ PreviewImageName string // filename for PreviewImageReader (e.g., "preview.jpg")
}
// ListRemoteFilesParameters contains arguments for the ListRemoteFiles method.
@@ -124,7 +129,11 @@ func (api *Client) AddRemoteFileContext(ctx context.Context, params RemoteFilePa
if params.PreviewImage != "" {
err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", params.PreviewImage, "preview_image", api.token, values, response, api)
} else if params.PreviewImageReader != nil {
- err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", "preview.png", "preview_image", api.token, values, params.PreviewImageReader, response, api)
+ name := params.PreviewImageName
+ if name == "" {
+ name = "preview.png"
+ }
+ err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", name, "preview_image", api.token, values, params.PreviewImageReader, response, api)
} else {
response, err = api.remoteFileRequest(ctx, "files.remote.add", values)
}
@@ -214,7 +223,7 @@ func (api *Client) ShareRemoteFile(channels []string, externalID, fileID string)
// ShareRemoteFileContext shares a remote file to channels with a custom context.
// Slack API docs: https://api.slack.com/methods/files.remote.share
func (api *Client) ShareRemoteFileContext(ctx context.Context, channels []string, externalID, fileID string) (file *RemoteFile, err error) {
- if channels == nil || len(channels) == 0 {
+ if len(channels) == 0 {
return nil, ErrParametersMissing
}
if fileID == "" && externalID == "" {
@@ -266,8 +275,14 @@ func (api *Client) UpdateRemoteFileContext(ctx context.Context, fileID string, p
if params.IndexableFileContents != "" {
values.Add("indexable_file_contents", params.IndexableFileContents)
}
- if params.PreviewImageReader != nil {
- err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", "preview.png", "preview_image", api.token, values, params.PreviewImageReader, response, api)
+ if params.PreviewImage != "" {
+ err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", params.PreviewImage, "preview_image", api.token, values, response, api)
+ } else if params.PreviewImageReader != nil {
+ name := params.PreviewImageName
+ if name == "" {
+ name = "preview.png"
+ }
+ err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", name, "preview_image", api.token, values, params.PreviewImageReader, response, api)
} else {
values.Add("token", api.token)
response, err = api.remoteFileRequest(ctx, "files.remote.update", values)
diff --git a/backend/vendor/github.com/slack-go/slack/retry.go b/backend/vendor/github.com/slack-go/slack/retry.go
new file mode 100644
index 00000000..d2f62117
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/retry.go
@@ -0,0 +1,307 @@
+package slack
+
+// Optional HTTP retries improve reliability when Slack is busy or the network is flaky.
+// Retries are off by default; use OptionRetry or OptionRetryConfig to turn them on.
+//
+// Retry behavior is driven by pluggable handlers (parity with the Python SDK:
+// https://github.com/slackapi/python-slack-sdk). When Handlers is nil, only rate limit
+// (429) is retried (NewRateLimitErrorRetryHandler). Use
+// AllBuiltinRetryHandlers(cfg) for connection + 429; ConnectionOnlyRetryHandlers(cfg) for
+// connection-only; add NewServerErrorRetryHandler(cfg) to also retry 5xx.
+//
+// File uploads and other requests that stream the body cannot be retried (the body is sent once).
+// Regular API calls (form or JSON) are retried when a handler matches (429, 5xx, or connection error).
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand/v2"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/slack-go/slack/internal/backoff"
+)
+
+// minRetryAfter429 is the minimum wait before retrying after 429 when Retry-After is missing
+// or zero, to avoid tight retry loops when using a partial RetryConfig.
+const minRetryAfter429 = time.Second
+
+// RetryState holds the current attempt and max retries; passed to handlers.
+// Backoff is set by retryClient for use by handlers that want exponential backoff (e.g. connection, server error).
+// Handlers may call Backoff.Duration() when retrying; each call advances the backoff for the next retry.
+type RetryState struct {
+ Attempt int // current attempt (0-based)
+ MaxRetries int
+ Backoff *backoff.Backoff // optional; used by connection/server handlers for exponential backoff
+}
+
+// RetryHandler decides whether to retry a request and how long to wait.
+// The first handler that returns (true, wait) wins. resp may be nil (connection failure); err may be nil (got response).
+type RetryHandler interface {
+ ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (retry bool, wait time.Duration)
+}
+
+// RetryConfig configures HTTP retry behavior.
+// When MaxRetries is 0, retries are disabled.
+// If Handlers is nil, only rate limit (429) is retried (see DefaultRetryHandlers).
+type RetryConfig struct {
+ // MaxRetries is the maximum number of retry attempts (0 = no retries, 1 = one retry, etc.).
+ MaxRetries int
+ // Handlers is the list of handlers to consult; nil means 429 only (DefaultRetryHandlers).
+ Handlers []RetryHandler
+ // RetryAfterDuration is used for 429 when the Retry-After header is missing or invalid.
+ RetryAfterDuration time.Duration
+ // RetryAfterJitter adds random jitter [0, RetryAfterJitter] to 429 wait to avoid thundering herd (0 = no jitter).
+ RetryAfterJitter time.Duration
+ // BackoffInitial is the initial backoff for 5xx and connection errors.
+ BackoffInitial time.Duration
+ // BackoffMax caps the backoff duration.
+ BackoffMax time.Duration
+ // BackoffJitter adds random jitter [0, BackoffJitter] to backoff to avoid thundering herd (0 to disable).
+ BackoffJitter time.Duration
+}
+
+// DefaultRetryConfig returns a retry config with sensible defaults.
+func DefaultRetryConfig() RetryConfig {
+ return RetryConfig{
+ MaxRetries: 3,
+ RetryAfterDuration: 60 * time.Second,
+ RetryAfterJitter: 1 * time.Second,
+ BackoffInitial: 100 * time.Millisecond,
+ BackoffMax: 30 * time.Second,
+ BackoffJitter: 50 * time.Millisecond,
+ }
+}
+
+// connectionErrorRetryHandler retries on connection errors (e.g. connection reset).
+type connectionErrorRetryHandler struct{}
+
+// NewConnectionErrorRetryHandler returns a handler that retries on connection errors.
+func NewConnectionErrorRetryHandler() RetryHandler {
+ return &connectionErrorRetryHandler{}
+}
+
+func (h *connectionErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) {
+ if err == nil || resp != nil {
+ return false, 0
+ }
+ if !isRetryableConnError(err) || !requestRetryable(req) {
+ return false, 0
+ }
+ if state.Attempt >= state.MaxRetries {
+ return false, 0
+ }
+ // Backoff is always set by retryClient.Do().
+ wait := state.Backoff.Duration()
+ return true, wait
+}
+
+// rateLimitErrorRetryHandler retries on 429 Too Many Requests using Retry-After or config.
+type rateLimitErrorRetryHandler struct {
+ cfg RetryConfig
+}
+
+// NewRateLimitErrorRetryHandler returns a handler that retries on 429.
+func NewRateLimitErrorRetryHandler(cfg RetryConfig) RetryHandler {
+ return &rateLimitErrorRetryHandler{cfg: cfg}
+}
+
+func (h *rateLimitErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) {
+ if resp == nil || resp.StatusCode != http.StatusTooManyRequests || !requestRetryable(req) {
+ return false, 0
+ }
+ if state.Attempt >= state.MaxRetries {
+ return false, 0
+ }
+ dur := h.cfg.RetryAfterDuration
+ if s := resp.Header.Get("Retry-After"); s != "" {
+ // Parsing only integer seconds is appropriate for Slack (API sends seconds; RFC 7231 also allows HTTP-date).
+ if sec, parseErr := strconv.ParseInt(strings.TrimSpace(s), 10, 64); parseErr == nil {
+ if sec > 0 {
+ dur = time.Duration(sec) * time.Second
+ } else {
+ dur = minRetryAfter429 // Retry-After: 0 means use minimum delay
+ }
+ }
+ }
+ dur = max(dur, minRetryAfter429)
+ if h.cfg.RetryAfterJitter > 0 {
+ dur += time.Duration(rand.IntN(int(h.cfg.RetryAfterJitter)))
+ }
+ return true, dur
+}
+
+// serverErrorRetryHandler retries on 5xx server errors (opt-in).
+type serverErrorRetryHandler struct{}
+
+// NewServerErrorRetryHandler returns a handler that retries on 5xx. Opt-in; not in DefaultRetryHandlers, ConnectionOnlyRetryHandlers, or AllBuiltinRetryHandlers.
+func NewServerErrorRetryHandler(cfg RetryConfig) RetryHandler {
+ return &serverErrorRetryHandler{}
+}
+
+func (h *serverErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) {
+ if resp == nil || resp.StatusCode < http.StatusInternalServerError || !requestRetryable(req) {
+ return false, 0
+ }
+ if state.Attempt >= state.MaxRetries {
+ return false, 0
+ }
+ // Backoff is always set by retryClient.Do().
+ wait := state.Backoff.Duration()
+ return true, wait
+}
+
+// DefaultRetryHandlers returns the default handler when retries are on: rate limit (429) only.
+// Used when Handlers is nil. Use AllBuiltinRetryHandlers(cfg) for connection + 429.
+func DefaultRetryHandlers(cfg RetryConfig) []RetryHandler {
+ return []RetryHandler{NewRateLimitErrorRetryHandler(cfg)}
+}
+
+// ConnectionOnlyRetryHandlers returns connection-only handlers (no 429 retries).
+func ConnectionOnlyRetryHandlers() []RetryHandler {
+ return []RetryHandler{NewConnectionErrorRetryHandler()}
+}
+
+// AllBuiltinRetryHandlers returns connection + rate limit (429) handlers; no 5xx.
+func AllBuiltinRetryHandlers(cfg RetryConfig) []RetryHandler {
+ return []RetryHandler{
+ NewConnectionErrorRetryHandler(),
+ NewRateLimitErrorRetryHandler(cfg),
+ }
+}
+
+// retryClient wraps an httpClient and retries according to config.Handlers.
+type retryClient struct {
+ client httpClient
+ config RetryConfig
+ debug Debug // optional; when set and Debug() is true, retries are logged
+}
+
+var _ httpClient = (*retryClient)(nil)
+
+// handlers returns the list of retry handlers. Empty Handlers slice is treated like nil (default 429 only).
+func (c *retryClient) handlers() []RetryHandler {
+ if len(c.config.Handlers) > 0 {
+ return c.config.Handlers
+ }
+ return DefaultRetryHandlers(c.config)
+}
+
+func (c *retryClient) logRetry(attempt int, reason string, detail any) {
+ if c.debug == nil || !c.debug.Debug() {
+ return
+ }
+ c.debug.Debugf("slack retry: %s (attempt %d/%d), detail: %v", reason, attempt+1, c.config.MaxRetries+1, detail)
+}
+
+// requestRetryable reports whether the request can be safely retried: either it has no body
+// (e.g. GET) or the body can be replayed via GetBody (e.g. POST with GetBody set).
+// Requests with a non-nil body and nil GetBody (e.g. streaming uploads) must not be retried.
+func requestRetryable(req *http.Request) bool {
+ return req.Body == nil || req.GetBody != nil
+}
+
+// sleepWithContext sleeps for up to d, or until ctx is done. Returns true if the full duration
+// elapsed, false if ctx was cancelled (caller should return ctx.Err()).
+func sleepWithContext(ctx context.Context, d time.Duration) bool {
+ if d <= 0 {
+ return true
+ }
+ timer := time.NewTimer(d)
+ defer timer.Stop()
+ select {
+ case <-timer.C:
+ return true
+ case <-ctx.Done():
+ return false
+ }
+}
+
+func (c *retryClient) Do(req *http.Request) (*http.Response, error) {
+ handlers := c.handlers()
+ bo := &backoff.Backoff{
+ Initial: c.config.BackoffInitial,
+ Max: c.config.BackoffMax,
+ Jitter: c.config.BackoffJitter,
+ }
+ var lastErr error
+ maxAttempts := c.config.MaxRetries + 1
+
+ for attempt := range maxAttempts {
+ state := &RetryState{Attempt: attempt, MaxRetries: c.config.MaxRetries, Backoff: bo}
+
+ // Rewind body for retries (POST/PUT with GetBody).
+ if attempt > 0 && req.GetBody != nil {
+ newBody, err := req.GetBody()
+ if err != nil {
+ return nil, err
+ }
+ req.Body = newBody
+ }
+
+ resp, err := c.client.Do(req)
+ if err != nil {
+ lastErr = err
+ }
+
+ // Success: got response and status is not 429/5xx.
+ if err == nil && resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < http.StatusInternalServerError {
+ return resp, nil
+ }
+
+ // No retries left or request body cannot be replayed — return now.
+ if attempt >= c.config.MaxRetries || !requestRetryable(req) {
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+ }
+
+ // First handler that wants to retry wins.
+ var wait time.Duration
+ retry := false
+ for _, h := range handlers {
+ if r, w := h.ShouldRetry(state, req, resp, err); r {
+ retry, wait = true, w
+ break
+ }
+ }
+ if !retry {
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+ }
+
+ // Log, discard response body if we have one, sleep, then next attempt.
+ if err != nil {
+ c.logRetry(attempt, "connection error", err)
+ } else {
+ reason := fmt.Sprintf("%d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
+ c.logRetry(attempt, reason, wait)
+ _, _ = io.Copy(io.Discard, resp.Body)
+ _ = resp.Body.Close()
+ }
+ if !sleepWithContext(req.Context(), wait) {
+ return nil, req.Context().Err()
+ }
+ }
+
+ return nil, lastErr
+}
+
+func isRetryableConnError(err error) bool {
+ for ; err != nil; err = errors.Unwrap(err) {
+ s := err.Error()
+ if strings.Contains(s, "connection reset") ||
+ strings.Contains(s, "connection refused") ||
+ strings.Contains(s, "EOF") {
+ return true
+ }
+ }
+ return false
+}
diff --git a/backend/vendor/github.com/slack-go/slack/slack.go b/backend/vendor/github.com/slack-go/slack/slack.go
index 756106fe..45b9cd3b 100644
--- a/backend/vendor/github.com/slack-go/slack/slack.go
+++ b/backend/vendor/github.com/slack-go/slack/slack.go
@@ -12,6 +12,8 @@ import (
const (
// APIURL of the slack api.
APIURL = "https://slack.com/api/"
+ // AuditAPIURL is the base URL for the Audit Logs API.
+ AuditAPIURL = "https://api.slack.com/"
// WEBAPIURLFormat ...
WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d"
)
@@ -44,27 +46,32 @@ type AuthTestResponse struct {
TeamID string `json:"team_id"`
UserID string `json:"user_id"`
// EnterpriseID is only returned when an enterprise id present
- EnterpriseID string `json:"enterprise_id,omitempty"`
- BotID string `json:"bot_id"`
+ EnterpriseID string `json:"enterprise_id,omitempty"`
+ BotID string `json:"bot_id"`
+ Header http.Header `json:"-"`
}
type authTestResponseFull struct {
SlackResponse
AuthTestResponse
+ responseHeaders
}
-// Client for the slack api.
type ParamOption func(*url.Values)
+// Client for the slack api.
type Client struct {
token string
appLevelToken string
configToken string
configRefreshToken string
endpoint string
+ auditEndpoint string
debug bool
log ilogger
httpclient httpClient
+ onWarning func(path string, request any, w *Warning)
+ onResponseHeaders func(path string, headers http.Header)
}
// Option defines an option for a Client
@@ -91,11 +98,44 @@ func OptionLog(l logger) func(*Client) {
}
}
+// OptionOnWarning sets a callback invoked whenever an API response contains
+// warnings. The callback receives the API method path (e.g.
+// "conversations.join"), the request payload ([url.Values] for form-encoded
+// requests or []byte for JSON requests), and a [Warning] with the warning
+// codes and messages.
+//
+// Example:
+//
+// api := slack.New("YOUR_TOKEN",
+// slack.OptionOnWarning(func(path string, request any, w *slack.Warning) {
+// log.Printf("slack warnings for %s: codes=%v warnings=%v", path, w.Codes, w.Warnings)
+// }),
+// )
+func OptionOnWarning(fn func(path string, request any, w *Warning)) func(*Client) {
+ return func(c *Client) {
+ c.onWarning = fn
+ }
+}
+
+// OptionOnResponseHeaders sets a callback invoked after every API request
+// with the API method path and the HTTP response headers. This allows
+// accessing headers like X-OAuth-Scopes and X-Ratelimit-* for any request.
+func OptionOnResponseHeaders(fn func(path string, headers http.Header)) func(*Client) {
+ return func(c *Client) {
+ c.onResponseHeaders = fn
+ }
+}
+
// OptionAPIURL set the url for the client. only useful for testing.
func OptionAPIURL(u string) func(*Client) {
return func(c *Client) { c.endpoint = u }
}
+// OptionAuditAPIURL set the url for the Audit Logs API. only useful for testing.
+func OptionAuditAPIURL(u string) func(*Client) {
+ return func(c *Client) { c.auditEndpoint = u }
+}
+
// OptionAppLevelToken sets an app-level token for the client.
func OptionAppLevelToken(token string) func(*Client) {
return func(c *Client) { c.appLevelToken = token }
@@ -111,13 +151,47 @@ func OptionConfigRefreshToken(token string) func(*Client) {
return func(c *Client) { c.configRefreshToken = token }
}
+// OptionRetry enables HTTP retries for rate limit (429) only; 5xx and connection errors are not retried.
+// Uses DefaultRetryHandlers. Use OptionRetryConfig with AllBuiltinRetryHandlers for connection + 429.
+// If maxRetries is zero or negative, the client is not wrapped (no retries).
+// When using a custom HTTP client, pass OptionRetry after OptionHTTPClient so the retry wrapper is applied to it.
+func OptionRetry(maxRetries int) func(*Client) {
+ return func(c *Client) {
+ if maxRetries <= 0 {
+ return
+ }
+ cfg := DefaultRetryConfig()
+ cfg.MaxRetries = maxRetries
+ cfg.Handlers = DefaultRetryHandlers(cfg)
+ c.httpclient = &retryClient{client: c.httpclient, config: cfg, debug: c}
+ }
+}
+
+// OptionRetryConfig enables HTTP retries with a custom config.
+// If config.MaxRetries is 0, the client is not wrapped (no retries).
+// If config.Handlers is nil, DefaultRetryHandlers(cfg) is used (429 only).
+// When using a custom HTTP client, pass OptionRetryConfig after OptionHTTPClient so the retry wrapper is applied to it.
+func OptionRetryConfig(config RetryConfig) func(*Client) {
+ return func(c *Client) {
+ if config.MaxRetries <= 0 {
+ return
+ }
+ cfg := config
+ if cfg.Handlers == nil {
+ cfg.Handlers = DefaultRetryHandlers(cfg)
+ }
+ c.httpclient = &retryClient{client: c.httpclient, config: cfg, debug: c}
+ }
+}
+
// New builds a slack client from the provided token and options.
func New(token string, options ...Option) *Client {
s := &Client{
- token: token,
- endpoint: APIURL,
- httpclient: &http.Client{},
- log: log.New(os.Stderr, "slack-go/slack", log.LstdFlags|log.Lshortfile),
+ token: token,
+ endpoint: APIURL,
+ auditEndpoint: AuditAPIURL,
+ httpclient: &http.Client{},
+ log: log.New(os.Stderr, "slack-go/slack", log.LstdFlags|log.Lshortfile),
}
for _, opt := range options {
@@ -141,18 +215,19 @@ func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestRespo
return nil, err
}
+ responseFull.AuthTestResponse.Header = responseFull.responseHeaders.header
return &responseFull.AuthTestResponse, responseFull.Err()
}
// Debugf print a formatted debug line.
-func (api *Client) Debugf(format string, v ...interface{}) {
+func (api *Client) Debugf(format string, v ...any) {
if api.debug {
api.log.Output(2, fmt.Sprintf(format, v...))
}
}
// Debugln print a debug line.
-func (api *Client) Debugln(v ...interface{}) {
+func (api *Client) Debugln(v ...any) {
if api.debug {
api.log.Output(2, fmt.Sprintln(v...))
}
@@ -164,11 +239,42 @@ func (api *Client) Debug() bool {
}
// post to a slack web method.
-func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf interface{}) error {
- return postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api)
+func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf any) error {
+ headers, err := postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api)
+ api.checkWarnings(intf, path, values)
+ api.fireResponseHeaders(path, headers)
+ return err
}
// get a slack web method.
-func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf interface{}) error {
- return getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api)
+func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf any) error {
+ headers, err := getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api)
+ api.checkWarnings(intf, path, values)
+ api.fireResponseHeaders(path, headers)
+ return err
+}
+
+// postJSONMethod posts JSON to a slack web method.
+func (api *Client) postJSONMethod(ctx context.Context, path string, token string, jsonBody []byte, intf any) error {
+ headers, err := postJSON(ctx, api.httpclient, api.endpoint+path, token, jsonBody, intf, api)
+ api.checkWarnings(intf, path, jsonBody)
+ api.fireResponseHeaders(path, headers)
+ return err
+}
+
+func (api *Client) checkWarnings(intf any, path string, request any) {
+ if api.onWarning == nil {
+ return
+ }
+ if w, ok := intf.(warner); ok {
+ if warning := w.Warn(); warning != nil {
+ api.onWarning(path, request, warning)
+ }
+ }
+}
+
+func (api *Client) fireResponseHeaders(path string, headers http.Header) {
+ if api.onResponseHeaders != nil && headers != nil {
+ api.onResponseHeaders(path, headers)
+ }
}
diff --git a/backend/vendor/github.com/slack-go/slack/socket_mode.go b/backend/vendor/github.com/slack-go/slack/socket_mode.go
index ea9ea3b7..d9e7b586 100644
--- a/backend/vendor/github.com/slack-go/slack/socket_mode.go
+++ b/backend/vendor/github.com/slack-go/slack/socket_mode.go
@@ -22,7 +22,7 @@ type openResponseFull struct {
// To have a fully managed Socket Mode connection, use `socketmode.New()`, and call `Run()` on it.
func (api *Client) StartSocketModeContext(ctx context.Context) (info *SocketModeConnection, websocketURL string, err error) {
response := &openResponseFull{}
- err = postJSON(ctx, api.httpclient, api.endpoint+"apps.connections.open", api.appLevelToken, nil, response, api)
+ err = api.postJSONMethod(ctx, "apps.connections.open", api.appLevelToken, nil, response)
if err != nil {
return nil, "", err
}
diff --git a/backend/vendor/github.com/slack-go/slack/stars.go b/backend/vendor/github.com/slack-go/slack/stars.go
index 51926854..0adb28c5 100644
--- a/backend/vendor/github.com/slack-go/slack/stars.go
+++ b/backend/vendor/github.com/slack-go/slack/stars.go
@@ -8,31 +8,28 @@ import (
)
const (
- DEFAULT_STARS_USER = ""
- DEFAULT_STARS_COUNT = 100
- DEFAULT_STARS_PAGE = 1
+ DEFAULT_STARS_USER = ""
)
type StarsParameters struct {
- User string
- Count int
- Page int
+ User string
+ Cursor string
+ Limit int
+ TeamID string
}
type StarredItem Item
type listResponseFull struct {
- Items []Item `json:"items"`
- Paging `json:"paging"`
+ Items []Item `json:"items"`
SlackResponse
+ ResponseMetadata `json:"response_metadata"`
}
// NewStarsParameters initialises StarsParameters with default values
func NewStarsParameters() StarsParameters {
return StarsParameters{
- User: DEFAULT_STARS_USER,
- Count: DEFAULT_STARS_COUNT,
- Page: DEFAULT_STARS_PAGE,
+ User: DEFAULT_STARS_USER,
}
}
@@ -100,37 +97,40 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I
// ListStars returns information about the stars a user added.
// For more information see the ListStarsContext documentation.
-func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
+func (api *Client) ListStars(params StarsParameters) ([]Item, string, error) {
return api.ListStarsContext(context.Background(), params)
}
// ListStarsContext returns information about the stars a user added with a custom context.
// Slack API docs: https://api.slack.com/methods/stars.list
-func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) {
+func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, string, error) {
values := url.Values{
"token": {api.token},
}
if params.User != DEFAULT_STARS_USER {
values.Add("user", params.User)
}
- if params.Count != DEFAULT_STARS_COUNT {
- values.Add("count", strconv.Itoa(params.Count))
+ if params.Cursor != "" {
+ values.Add("cursor", params.Cursor)
}
- if params.Page != DEFAULT_STARS_PAGE {
- values.Add("page", strconv.Itoa(params.Page))
+ if params.Limit != 0 {
+ values.Add("limit", strconv.Itoa(params.Limit))
+ }
+ if params.TeamID != "" {
+ values.Add("team_id", params.TeamID)
}
response := &listResponseFull{}
err := api.postMethod(ctx, "stars.list", values, response)
if err != nil {
- return nil, nil, err
+ return nil, "", err
}
if err := response.Err(); err != nil {
- return nil, nil, err
+ return nil, "", err
}
- return response.Items, &response.Paging, nil
+ return response.Items, response.ResponseMetadata.Cursor, nil
}
// GetStarred returns a list of StarredItem items.
@@ -139,31 +139,31 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters)
// be looking at according to what is in the Type:
//
// for _, item := range items {
-// switch c.Type {
-// case "file_comment":
-// log.Println(c.Comment)
-// case "file":
-// ...
+// switch c.Type {
+// case "file_comment":
+// log.Println(c.Comment)
+// case "file":
+// ...
// }
//
// This function still exists to maintain backwards compatibility.
// I exposed it as returning []StarredItem, so it shall stay as StarredItem.
-func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) {
+func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, string, error) {
return api.GetStarredContext(context.Background(), params)
}
// GetStarredContext returns a list of StarredItem items with a custom context
// For more details see GetStarred
-func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, *Paging, error) {
- items, paging, err := api.ListStarsContext(ctx, params)
+func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, string, error) {
+ items, nextCursor, err := api.ListStarsContext(ctx, params)
if err != nil {
- return nil, nil, err
+ return nil, "", err
}
starredItems := make([]StarredItem, len(items))
for i, item := range items {
starredItems[i] = StarredItem(item)
}
- return starredItems, paging, nil
+ return starredItems, nextCursor, nil
}
type listResponsePaginated struct {
diff --git a/backend/vendor/github.com/slack-go/slack/team.go b/backend/vendor/github.com/slack-go/slack/team.go
index 35b69927..55364a9b 100644
--- a/backend/vendor/github.com/slack-go/slack/team.go
+++ b/backend/vendor/github.com/slack-go/slack/team.go
@@ -6,11 +6,6 @@ import (
"strconv"
)
-const (
- DEFAULT_LOGINS_COUNT = 100
- DEFAULT_LOGINS_PAGE = 1
-)
-
type TeamResponse struct {
Team TeamInfo `json:"team"`
SlackResponse
@@ -46,8 +41,8 @@ type TeamProfileField struct {
type LoginResponse struct {
Logins []Login `json:"logins"`
- Paging `json:"paging"`
SlackResponse
+ ResponseMetadata `json:"response_metadata"`
}
type Login struct {
@@ -75,16 +70,14 @@ type BillingActive struct {
// AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request
type AccessLogParameters struct {
TeamID string
- Count int
- Page int
+ Cursor string
+ Limit int
+ Before int
}
// NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set
func NewAccessLogParameters() AccessLogParameters {
- return AccessLogParameters{
- Count: DEFAULT_LOGINS_COUNT,
- Page: DEFAULT_LOGINS_PAGE,
- }
+ return AccessLogParameters{}
}
func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) {
@@ -193,31 +186,34 @@ func (api *Client) GetTeamProfileContext(ctx context.Context, teamID ...string)
// GetAccessLogs retrieves a page of logins according to the parameters given.
// For more information see the GetAccessLogsContext documentation.
-func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) {
+func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, string, error) {
return api.GetAccessLogsContext(context.Background(), params)
}
// GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context.
// Slack API docs: https://api.slack.com/methods/team.accessLogs
-func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) {
+func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, string, error) {
values := url.Values{
"token": {api.token},
}
if params.TeamID != "" {
values.Add("team_id", params.TeamID)
}
- if params.Count != DEFAULT_LOGINS_COUNT {
- values.Add("count", strconv.Itoa(params.Count))
+ if params.Cursor != "" {
+ values.Add("cursor", params.Cursor)
}
- if params.Page != DEFAULT_LOGINS_PAGE {
- values.Add("page", strconv.Itoa(params.Page))
+ if params.Limit != 0 {
+ values.Add("limit", strconv.Itoa(params.Limit))
+ }
+ if params.Before != 0 {
+ values.Add("before", strconv.Itoa(params.Before))
}
response, err := api.accessLogsRequest(ctx, "team.accessLogs", values)
if err != nil {
- return nil, nil, err
+ return nil, "", err
}
- return response.Logins, &response.Paging, nil
+ return response.Logins, response.ResponseMetadata.Cursor, nil
}
type GetBillableInfoParams struct {
diff --git a/backend/vendor/github.com/slack-go/slack/usergroups.go b/backend/vendor/github.com/slack-go/slack/usergroups.go
index 616bbf15..b5c54545 100644
--- a/backend/vendor/github.com/slack-go/slack/usergroups.go
+++ b/backend/vendor/github.com/slack-go/slack/usergroups.go
@@ -568,3 +568,17 @@ func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup
}
return response.UserGroup, nil
}
+
+// UpdateUserGroupMembersList updates the members of an existing user group,
+// accepting a slice of user IDs. This is a convenience wrapper around
+// UpdateUserGroupMembers for use with APIs that return []string (e.g.
+// GetUserGroupMembers).
+func (api *Client) UpdateUserGroupMembersList(userGroup string, members []string, options ...UpdateUserGroupMembersOption) (UserGroup, error) {
+ return api.UpdateUserGroupMembersContext(context.Background(), userGroup, strings.Join(members, ","), options...)
+}
+
+// UpdateUserGroupMembersListContext updates the members of an existing user
+// group with a custom context, accepting a slice of user IDs.
+func (api *Client) UpdateUserGroupMembersListContext(ctx context.Context, userGroup string, members []string, options ...UpdateUserGroupMembersOption) (UserGroup, error) {
+ return api.UpdateUserGroupMembersContext(ctx, userGroup, strings.Join(members, ","), options...)
+}
diff --git a/backend/vendor/github.com/slack-go/slack/users.go b/backend/vendor/github.com/slack-go/slack/users.go
index 91a9a4c7..541baae7 100644
--- a/backend/vendor/github.com/slack-go/slack/users.go
+++ b/backend/vendor/github.com/slack-go/slack/users.go
@@ -17,32 +17,41 @@ const (
// UserProfile contains all the information details of a given user
type UserProfile struct {
- FirstName string `json:"first_name,omitempty"`
- LastName string `json:"last_name,omitempty"`
- RealName string `json:"real_name"`
- RealNameNormalized string `json:"real_name_normalized"`
- DisplayName string `json:"display_name"`
- DisplayNameNormalized string `json:"display_name_normalized"`
- AvatarHash string `json:"avatar_hash"`
- Email string `json:"email,omitempty"`
- Skype string `json:"skyp,omitempty"`
- Phone string `json:"phone,omitempty"`
- Image24 string `json:"image_24"`
- Image32 string `json:"image_32"`
- Image48 string `json:"image_48"`
- Image72 string `json:"image_72"`
- Image192 string `json:"image_192"`
- Image512 string `json:"image_512"`
- ImageOriginal string `json:"image_original,omitempty"`
- Title string `json:"title,omitempty"`
- BotID string `json:"bot_id,omitempty"`
- ApiAppID string `json:"api_app_id,omitempty"`
- StatusText string `json:"status_text,omitempty"`
- StatusEmoji string `json:"status_emoji,omitempty"`
- StatusEmojiDisplayInfo []UserProfileStatusEmojiDisplayInfo `json:"status_emoji_display_info,omitempty"`
- StatusExpiration int `json:"status_expiration,omitempty"`
- Team string `json:"team"`
- Fields UserProfileCustomFields `json:"fields,omitempty"`
+ FirstName string `json:"first_name,omitempty"`
+ LastName string `json:"last_name,omitempty"`
+ RealName string `json:"real_name"`
+ RealNameNormalized string `json:"real_name_normalized"`
+ DisplayName string `json:"display_name"`
+ DisplayNameNormalized string `json:"display_name_normalized"`
+ Pronouns string `json:"pronouns,omitempty"`
+ AvatarHash string `json:"avatar_hash"`
+ Email string `json:"email,omitempty"`
+ Skype string `json:"skype,omitempty"`
+ Phone string `json:"phone,omitempty"`
+ Image24 string `json:"image_24"`
+ Image32 string `json:"image_32"`
+ Image48 string `json:"image_48"`
+ Image72 string `json:"image_72"`
+ Image192 string `json:"image_192"`
+ Image512 string `json:"image_512"`
+ Image1024 string `json:"image_1024,omitempty"`
+ ImageOriginal string `json:"image_original,omitempty"`
+ IsCustomImage bool `json:"is_custom_image,omitempty"`
+ Title string `json:"title,omitempty"`
+ BotID string `json:"bot_id,omitempty"`
+ ApiAppID string `json:"api_app_id,omitempty"`
+ AlwaysActive bool `json:"always_active,omitempty"`
+ StatusText string `json:"status_text,omitempty"`
+ StatusEmoji string `json:"status_emoji,omitempty"`
+ StatusEmojiDisplayInfo []UserProfileStatusEmojiDisplayInfo `json:"status_emoji_display_info,omitempty"`
+ StatusExpiration int `json:"status_expiration,omitempty"`
+ StatusTextCanonical string `json:"status_text_canonical,omitempty"`
+ HuddleState string `json:"huddle_state,omitempty"`
+ HuddleStateExpirationTS int `json:"huddle_state_expiration_ts,omitempty"`
+ StartDate string `json:"start_date,omitempty"`
+ GuestInvitedBy string `json:"guest_invited_by,omitempty"`
+ Team string `json:"team"`
+ Fields UserProfileCustomFields `json:"fields,omitempty"`
}
type UserProfileStatusEmojiDisplayInfo struct {
@@ -72,7 +81,7 @@ func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error {
// MarshalJSON is the implementation of the json.Marshaler interface.
func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) {
if len(fields.fields) == 0 {
- return []byte("[]"), nil
+ return []byte("{}"), nil
}
return json.Marshal(fields.fields)
}
@@ -111,33 +120,37 @@ type UserProfileCustomField struct {
// User contains all the information of a user
type User struct {
- ID string `json:"id"`
- TeamID string `json:"team_id"`
- Name string `json:"name"`
- Deleted bool `json:"deleted"`
- Color string `json:"color"`
- RealName string `json:"real_name"`
- TZ string `json:"tz,omitempty"`
- TZLabel string `json:"tz_label"`
- TZOffset int `json:"tz_offset"`
- Profile UserProfile `json:"profile"`
- IsBot bool `json:"is_bot"`
- IsAdmin bool `json:"is_admin"`
- IsOwner bool `json:"is_owner"`
- IsPrimaryOwner bool `json:"is_primary_owner"`
- IsRestricted bool `json:"is_restricted"`
- IsUltraRestricted bool `json:"is_ultra_restricted"`
- IsStranger bool `json:"is_stranger"`
- IsAppUser bool `json:"is_app_user"`
- IsInvitedUser bool `json:"is_invited_user"`
- IsEmailConfirmed bool `json:"is_email_confirmed"`
- Has2FA bool `json:"has_2fa"`
- TwoFactorType *string `json:"two_factor_type"`
- HasFiles bool `json:"has_files"`
- Presence string `json:"presence"`
- Locale string `json:"locale"`
- Updated JSONTime `json:"updated"`
- Enterprise EnterpriseUser `json:"enterprise_user,omitempty"`
+ ID string `json:"id"`
+ TeamID string `json:"team_id"`
+ Name string `json:"name"`
+ Username string `json:"username,omitempty"`
+ Deleted bool `json:"deleted"`
+ Color string `json:"color"`
+ RealName string `json:"real_name"`
+ TZ string `json:"tz,omitempty"`
+ TZLabel string `json:"tz_label"`
+ TZOffset int `json:"tz_offset"`
+ Profile UserProfile `json:"profile"`
+ IsBot bool `json:"is_bot"`
+ IsAdmin bool `json:"is_admin"`
+ IsOwner bool `json:"is_owner"`
+ IsPrimaryOwner bool `json:"is_primary_owner"`
+ IsRestricted bool `json:"is_restricted"`
+ IsUltraRestricted bool `json:"is_ultra_restricted"`
+ IsStranger bool `json:"is_stranger"`
+ IsAppUser bool `json:"is_app_user"`
+ IsConnectorBot bool `json:"is_connector_bot"`
+ IsWorkflowBot bool `json:"is_workflow_bot"`
+ IsInvitedUser bool `json:"is_invited_user"`
+ IsEmailConfirmed bool `json:"is_email_confirmed"`
+ Has2FA *bool `json:"has_2fa,omitempty"`
+ TwoFactorType *string `json:"two_factor_type"`
+ HasFiles bool `json:"has_files"`
+ Presence string `json:"presence"`
+ Locale string `json:"locale"`
+ Updated JSONTime `json:"updated"`
+ WhoCanShareContactCard string `json:"who_can_share_contact_card,omitempty"`
+ Enterprise EnterpriseUser `json:"enterprise_user,omitempty"`
}
// UserPresence contains details about a user online status
@@ -169,13 +182,14 @@ type UserIdentity struct {
}
// EnterpriseUser is present when a user is part of Slack Enterprise Grid
-// https://api.slack.com/types/user#enterprise_grid_user_objects
+// https://docs.slack.dev/reference/objects/user-object/#fields
type EnterpriseUser struct {
ID string `json:"id"`
EnterpriseID string `json:"enterprise_id"`
EnterpriseName string `json:"enterprise_name"`
IsAdmin bool `json:"is_admin"`
IsOwner bool `json:"is_owner"`
+ IsPrimaryOwner bool `json:"is_primary_owner"`
Teams []string `json:"teams"`
}
@@ -316,6 +330,13 @@ func GetUsersOptionTeamID(teamId string) GetUsersOption {
}
}
+// GetUsersOptionCursor set the cursor to the next page of results
+func GetUsersOptionCursor(cursor string) GetUsersOption {
+ return func(p *UserPagination) {
+ p.Cursor = cursor
+ }
+}
+
func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) {
up = UserPagination{
c: c,
@@ -331,12 +352,13 @@ func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination)
// UserPagination allows for paginating over the users
type UserPagination struct {
- Users []User
- limit int
- presence bool
- teamId string
- previousResp *ResponseMetadata
- c *Client
+ Users []User
+ Cursor string
+ limit int
+ presence bool
+ teamId string
+ complete bool
+ c *Client
}
// Done checks if the pagination has completed
@@ -358,17 +380,15 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error)
resp *userResponseFull
)
- if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") {
+ if t.c == nil || t.complete {
return t, errPaginationComplete
}
- t.previousResp = t.previousResp.initialize()
-
values := url.Values{
"limit": {strconv.Itoa(t.limit)},
"presence": {strconv.FormatBool(t.presence)},
"token": {t.c.token},
- "cursor": {t.previousResp.Cursor},
+ "cursor": {t.Cursor},
"team_id": {t.teamId},
"include_locale": {strconv.FormatBool(true)},
}
@@ -379,7 +399,8 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error)
t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata)
t.Users = resp.Members
- t.previousResp = &resp.Metadata
+ t.Cursor = resp.Metadata.Cursor
+ t.complete = t.Cursor == ""
return t, nil
}
@@ -585,6 +606,54 @@ func (api *Client) SetUserRealNameContextWithUser(ctx context.Context, user, rea
return response.Err()
}
+// SetUserProfile sets the profile for the provided user.
+// For more information see the SetUserProfileContext documentation.
+func (api *Client) SetUserProfile(user string, profile *UserProfile) error {
+ return api.SetUserProfileContext(context.Background(), user, profile)
+}
+
+// SetUserProfileContext sets the profile for the provided user with a custom context.
+//
+// The profile parameter is serialized as-is. Fields present in the JSON (including
+// zero-value fields without an omitempty tag, such as RealName and DisplayName) will
+// be updated by Slack. To avoid unintended changes, retrieve the current profile with
+// GetUserProfile, modify the desired fields, and pass the result.
+//
+// For setting individual fields, prefer the targeted methods: SetUserRealName,
+// SetUserCustomStatus, SetUserCustomFields.
+//
+// If a workspace admin has mapped custom profile fields to standard fields (e.g.
+// title), the custom field takes precedence. Update the custom field via
+// SetUserCustomFields instead.
+//
+// The user parameter is required when setting another user's profile (admin only,
+// paid plans). Pass an empty string to modify the authenticated user's own profile.
+//
+// Slack API docs: https://docs.slack.dev/reference/methods/users.profile.set/
+func (api *Client) SetUserProfileContext(ctx context.Context, user string, profile *UserProfile) error {
+ profileJSON, err := json.Marshal(profile)
+ if err != nil {
+ return err
+ }
+
+ values := url.Values{
+ "token": {api.token},
+ "profile": {string(profileJSON)},
+ }
+
+ // optional field. It should not be set if empty
+ if user != "" {
+ values["user"] = []string{user}
+ }
+
+ response := &userResponseFull{}
+ if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
+ return err
+ }
+
+ return response.Err()
+}
+
// SetUserCustomFields sets Custom Profile fields on the provided users account.
// For more information see the SetUserCustomFieldsContext documentation.
func (api *Client) SetUserCustomFields(userID string, customFields map[string]UserProfileCustomField) error {
@@ -628,7 +697,7 @@ func (api *Client) SetUserCustomFieldsContext(ctx context.Context, userID string
}
response := &userResponseFull{}
- if err := postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil {
+ if _, err := postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil {
return err
}
@@ -661,16 +730,16 @@ func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji str
//
// Slack API docs: https://api.slack.com/methods/users.profile.set
func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error {
- // XXX(theckman): this anonymous struct is for making requests to the Slack
- // API for setting and unsetting a User's Custom Status/Emoji. To change
- // these values we must provide a JSON document as the profile POST field.
+ // This anonymous struct is for making requests to the Slack API for setting and
+ // unsetting a User's Custom Status/Emoji. To change these values we must provide a
+ // JSON document as the profile POST field.
//
- // We use an anonymous struct over UserProfile because to unset the values
- // on the User's profile we cannot use the `json:"omitempty"` tag. This is
- // because an empty string ("") is what's used to unset the values. Check
- // out the API docs for more details:
+ // We use an anonymous struct over UserProfile because to unset the values on the
+ // User's profile we cannot use the `json:"omitempty"` tag. This is because an empty
+ // string ("") is what's used to unset the values. Check out the API docs for more
+ // details:
//
- // - https://api.slack.com/docs/presence-and-status#custom_status
+ // - https://docs.slack.dev/apis/web-api/user-presence-and-status/#custom-status
profile, err := json.Marshal(
&struct {
StatusText string `json:"status_text"`
diff --git a/backend/vendor/github.com/slack-go/slack/views.go b/backend/vendor/github.com/slack-go/slack/views.go
index 5f55b537..c16503c0 100644
--- a/backend/vendor/github.com/slack-go/slack/views.go
+++ b/backend/vendor/github.com/slack-go/slack/views.go
@@ -70,12 +70,30 @@ type ViewSubmissionResponse struct {
Errors map[string]string `json:"errors,omitempty"`
}
+// NewClearViewSubmissionResponse closes all open modals in the current stack.
+//
+// For HTTP-based apps, marshal this to JSON and write it as the HTTP response
+// body. The response is not sent until the handler returns, so start any slow
+// work in a goroutine and return promptly.
+//
+// For Socket Mode apps, pass this as the payload argument to Ack().
+//
+// See https://docs.slack.dev/surfaces/modals#closing_views
func NewClearViewSubmissionResponse() *ViewSubmissionResponse {
return &ViewSubmissionResponse{
ResponseAction: RAClear,
}
}
+// NewUpdateViewSubmissionResponse replaces the current modal with a new view.
+//
+// For HTTP-based apps, marshal this to JSON and write it as the HTTP response
+// body. The response is not sent until the handler returns, so start any slow
+// work in a goroutine and return promptly.
+//
+// For Socket Mode apps, pass this as the payload argument to Ack().
+//
+// See https://docs.slack.dev/surfaces/modals#updating_views
func NewUpdateViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse {
return &ViewSubmissionResponse{
ResponseAction: RAUpdate,
@@ -83,6 +101,15 @@ func NewUpdateViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResp
}
}
+// NewPushViewSubmissionResponse pushes a new view onto the modal stack.
+//
+// For HTTP-based apps, marshal this to JSON and write it as the HTTP response
+// body. The response is not sent until the handler returns, so start any slow
+// work in a goroutine and return promptly.
+//
+// For Socket Mode apps, pass this as the payload argument to Ack().
+//
+// See https://docs.slack.dev/surfaces/modals#pushing_views
func NewPushViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse {
return &ViewSubmissionResponse{
ResponseAction: RAPush,
@@ -90,6 +117,19 @@ func NewPushViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionRespon
}
}
+// NewErrorsViewSubmissionResponse displays validation errors on form fields.
+//
+// The errors map keys must be the BlockID of an InputBlock in the view. Keys
+// that reference other block types (e.g. SectionBlock) are silently ignored
+// by Slack, which shows a generic "trouble connecting" error instead.
+//
+// For HTTP-based apps, marshal this to JSON and write it as the HTTP response
+// body. The response is not sent until the handler returns, so start any slow
+// work in a goroutine and return promptly.
+//
+// For Socket Mode apps, pass this as the payload argument to Ack().
+//
+// See https://docs.slack.dev/surfaces/modals/#displaying_errors
func NewErrorsViewSubmissionResponse(errors map[string]string) *ViewSubmissionResponse {
return &ViewSubmissionResponse{
ResponseAction: RAErrors,
@@ -167,6 +207,9 @@ func ValidateUniqueBlockID(view ModalViewRequest) bool {
for _, b := range view.Blocks.BlockSet {
if inputBlock, ok := b.(*InputBlock); ok {
+ if inputBlock.BlockID == "" {
+ continue
+ }
if _, ok := uniqueBlockID[inputBlock.BlockID]; ok {
return false
}
@@ -178,7 +221,7 @@ func ValidateUniqueBlockID(view ModalViewRequest) bool {
}
// OpenViewContext opens a view for a user with a custom context.
-// Slack API docs: https://api.slack.com/methods/views.open
+// Slack API docs: https://docs.slack.dev/reference/methods/views.open
func (api *Client) OpenViewContext(
ctx context.Context,
triggerID string,
@@ -200,9 +243,8 @@ func (api *Client) OpenViewContext(
if err != nil {
return nil, err
}
- endpoint := api.endpoint + "views.open"
resp := &ViewResponse{}
- err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api)
+ err = api.postJSONMethod(ctx, "views.open", api.token, encoded, resp)
if err != nil {
return nil, err
}
@@ -212,11 +254,15 @@ func (api *Client) OpenViewContext(
// PublishView publishes a static view for a user.
// For more information see the PublishViewContext documentation.
func (api *Client) PublishView(userID string, view HomeTabViewRequest, hash string) (*ViewResponse, error) {
- return api.PublishViewContext(context.Background(), PublishViewContextRequest{UserID: userID, View: view, Hash: &hash})
+ var hashPtr *string
+ if hash != "" {
+ hashPtr = &hash
+ }
+ return api.PublishViewContext(context.Background(), PublishViewContextRequest{UserID: userID, View: view, Hash: hashPtr})
}
// PublishViewContext publishes a static view for a user with a custom context.
-// Slack API docs: https://api.slack.com/methods/views.publish
+// Slack API docs: https://docs.slack.dev/reference/methods/views.publish
func (api *Client) PublishViewContext(
ctx context.Context,
req PublishViewContextRequest,
@@ -228,9 +274,8 @@ func (api *Client) PublishViewContext(
if err != nil {
return nil, err
}
- endpoint := api.endpoint + "views.publish"
resp := &ViewResponse{}
- err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api)
+ err = api.postJSONMethod(ctx, "views.publish", api.token, encoded, resp)
if err != nil {
return nil, err
}
@@ -244,7 +289,7 @@ func (api *Client) PushView(triggerID string, view ModalViewRequest) (*ViewRespo
}
// PushViewContext pushes a view onto the stack of a root view with a custom context.
-// Slack API docs: https://api.slack.com/methods/views.push
+// Slack API docs: https://docs.slack.dev/reference/methods/views.push
func (api *Client) PushViewContext(
ctx context.Context,
triggerID string,
@@ -261,9 +306,8 @@ func (api *Client) PushViewContext(
if err != nil {
return nil, err
}
- endpoint := api.endpoint + "views.push"
resp := &ViewResponse{}
- err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api)
+ err = api.postJSONMethod(ctx, "views.push", api.token, encoded, resp)
if err != nil {
return nil, err
}
@@ -277,7 +321,7 @@ func (api *Client) UpdateView(view ModalViewRequest, externalID, hash, viewID st
}
// UpdateViewContext updates an existing view with a custom context.
-// Slack API docs: https://api.slack.com/methods/views.update
+// Slack API docs: https://docs.slack.dev/reference/methods/views.update
func (api *Client) UpdateViewContext(
ctx context.Context,
view ModalViewRequest,
@@ -297,9 +341,8 @@ func (api *Client) UpdateViewContext(
if err != nil {
return nil, err
}
- endpoint := api.endpoint + "views.update"
resp := &ViewResponse{}
- err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api)
+ err = api.postJSONMethod(ctx, "views.update", api.token, encoded, resp)
if err != nil {
return nil, err
}
diff --git a/backend/vendor/github.com/slack-go/slack/webhooks.go b/backend/vendor/github.com/slack-go/slack/webhooks.go
index 5a854f38..729bce40 100644
--- a/backend/vendor/github.com/slack-go/slack/webhooks.go
+++ b/backend/vendor/github.com/slack-go/slack/webhooks.go
@@ -23,8 +23,8 @@ type WebhookMessage struct {
ReplaceOriginal bool `json:"replace_original"`
DeleteOriginal bool `json:"delete_original"`
ReplyBroadcast bool `json:"reply_broadcast,omitempty"`
- UnfurlLinks bool `json:"unfurl_links,omitempty"`
- UnfurlMedia bool `json:"unfurl_media,omitempty"`
+ UnfurlLinks *bool `json:"unfurl_links,omitempty"`
+ UnfurlMedia *bool `json:"unfurl_media,omitempty"`
}
func PostWebhook(url string, msg *WebhookMessage) error {
diff --git a/backend/vendor/github.com/slack-go/slack/websocket_groups.go b/backend/vendor/github.com/slack-go/slack/websocket_groups.go
index eb88985c..c35d5f35 100644
--- a/backend/vendor/github.com/slack-go/slack/websocket_groups.go
+++ b/backend/vendor/github.com/slack-go/slack/websocket_groups.go
@@ -7,9 +7,6 @@ type GroupCreatedEvent struct {
Channel ChannelCreatedInfo `json:"channel"`
}
-// XXX: Should we really do this? event.Group is probably nicer than event.Channel
-// even though the api returns "channel"
-
// GroupMarkedEvent represents the Group marked event
type GroupMarkedEvent ChannelInfoEvent
diff --git a/backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go b/backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go
index f107b2a4..da861fa8 100644
--- a/backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go
+++ b/backend/vendor/github.com/slack-go/slack/websocket_managed_conn.go
@@ -582,7 +582,10 @@ var EventMapping = map[string]interface{}{
"manual_presence_change": ManualPresenceChangeEvent{},
- "user_change": UserChangeEvent{},
+ "user_change": UserChangeEvent{},
+ "user_status_changed": UserStatusChangedEvent{},
+ "user_huddle_changed": UserHuddleChangedEvent{},
+ "user_profile_changed": UserProfileChangedEvent{},
"emoji_changed": EmojiChangedEvent{},
@@ -595,6 +598,10 @@ var EventMapping = map[string]interface{}{
"accounts_changed": AccountsChangedEvent{},
+ "apps_uninstalled": AppsUninstalledEvent{},
+ "activity": ActivityEvent{},
+ "badge_counts_updated": BadgeCountsUpdatedEvent{},
+
"reconnect_url": ReconnectUrlEvent{},
"member_joined_channel": MemberJoinedChannelEvent{},
@@ -608,4 +615,10 @@ var EventMapping = map[string]interface{}{
"desktop_notification": DesktopNotificationEvent{},
"mobile_in_app_notification": MobileInAppNotificationEvent{},
+
+ "channel_updated": ChannelUpdatedEvent{},
+
+ "sh_room_join": SHRoomJoinEvent{},
+ "sh_room_leave": SHRoomLeaveEvent{},
+ "sh_room_update": SHRoomUpdateEvent{},
}
diff --git a/backend/vendor/github.com/slack-go/slack/websocket_misc.go b/backend/vendor/github.com/slack-go/slack/websocket_misc.go
index 65a8bb65..fb301f5e 100644
--- a/backend/vendor/github.com/slack-go/slack/websocket_misc.go
+++ b/backend/vendor/github.com/slack-go/slack/websocket_misc.go
@@ -71,8 +71,34 @@ type ManualPresenceChangeEvent struct {
// UserChangeEvent represents the user change event
type UserChangeEvent struct {
- Type string `json:"type"`
- User User `json:"user"`
+ Type string `json:"type"`
+ User User `json:"user"`
+ CacheTS int64 `json:"cache_ts"`
+ EventTS string `json:"event_ts"`
+}
+
+// UserStatusChangedEvent represents the user status changed event
+type UserStatusChangedEvent struct {
+ Type string `json:"type"`
+ User User `json:"user"`
+ CacheTS int64 `json:"cache_ts"`
+ EventTS string `json:"event_ts"`
+}
+
+// UserHuddleChangedEvent represents the user huddle changed event
+type UserHuddleChangedEvent struct {
+ Type string `json:"type"`
+ User User `json:"user"`
+ CacheTS int64 `json:"cache_ts"`
+ EventTS string `json:"event_ts"`
+}
+
+// UserProfileChangedEvent represents the user profile changed event
+type UserProfileChangedEvent struct {
+ Type string `json:"type"`
+ User User `json:"user"`
+ CacheTS int64 `json:"cache_ts"`
+ EventTS string `json:"event_ts"`
}
// EmojiChangedEvent represents the emoji changed event
@@ -139,3 +165,115 @@ type MemberLeftChannelEvent struct {
ChannelType string `json:"channel_type"`
Team string `json:"team"`
}
+
+// ChannelUpdatedEvent is fired when a channel's properties are updated (tabs, meeting
+// notes, etc.).
+type ChannelUpdatedEvent struct {
+ Type string `json:"type"`
+ Updates map[string]any `json:"updates"`
+ Channel string `json:"channel"`
+ Channels []string `json:"channels"`
+ EventTS string `json:"event_ts"`
+ TS string `json:"ts"`
+}
+
+// SHRoomRecording holds recording metadata for a Slack Call/Huddle room.
+type SHRoomRecording struct {
+ CanRecordSummary string `json:"can_record_summary,omitempty"`
+}
+
+// SHRoom represents a Slack Huddle/Call room.
+type SHRoom struct {
+ ID string `json:"id"`
+ Name *string `json:"name"` // nullable in Slack's response
+ MediaServer string `json:"media_server"`
+ CreatedBy string `json:"created_by"`
+ DateStart int64 `json:"date_start"`
+ DateEnd int64 `json:"date_end"`
+ Participants []string `json:"participants"`
+ ParticipantHistory []string `json:"participant_history"`
+ ParticipantsEvents map[string]map[string]any `json:"participants_events,omitempty"`
+ ParticipantsCameraOn []string `json:"participants_camera_on"`
+ ParticipantsCameraOff []string `json:"participants_camera_off"`
+ ParticipantsScreenshareOn []string `json:"participants_screenshare_on"`
+ ParticipantsScreenshareOff []string `json:"participants_screenshare_off"`
+ CanvasThreadTS string `json:"canvas_thread_ts,omitempty"`
+ ThreadRootTS string `json:"thread_root_ts,omitempty"`
+ Channels []string `json:"channels"`
+ IsDMCall bool `json:"is_dm_call"`
+ WasRejected bool `json:"was_rejected"`
+ WasMissed bool `json:"was_missed"`
+ WasAccepted bool `json:"was_accepted"`
+ HasEnded bool `json:"has_ended"`
+ BackgroundID string `json:"background_id,omitempty"`
+ CanvasBackground string `json:"canvas_background,omitempty"`
+ IsPrewarmed bool `json:"is_prewarmed,omitempty"`
+ IsScheduled bool `json:"is_scheduled,omitempty"`
+ Recording *SHRoomRecording `json:"recording,omitempty"`
+ Locale string `json:"locale,omitempty"`
+ AttachedFileIDs []string `json:"attached_file_ids,omitempty"`
+ MediaBackendType string `json:"media_backend_type"`
+ DisplayID string `json:"display_id,omitempty"`
+ ExternalUniqueID string `json:"external_unique_id"`
+ AppID string `json:"app_id"`
+ CallFamily string `json:"call_family,omitempty"`
+ HuddleLink string `json:"huddle_link,omitempty"`
+}
+
+// SHRoomHuddle holds the huddle-specific metadata on sh_room events.
+type SHRoomHuddle struct {
+ ChannelID string `json:"channel_id"`
+}
+
+// SHRoomJoinEvent is fired when a user joins a Slack Call/Huddle room.
+type SHRoomJoinEvent struct {
+ Type string `json:"type"`
+ Room SHRoom `json:"room"`
+ User string `json:"user"`
+ Huddle *SHRoomHuddle `json:"huddle,omitempty"`
+ EventTS string `json:"event_ts"`
+ TS string `json:"ts"`
+}
+
+// SHRoomLeaveEvent is fired when a user leaves a Slack Call/Huddle room.
+type SHRoomLeaveEvent struct {
+ Type string `json:"type"`
+ Room SHRoom `json:"room"`
+ User string `json:"user"`
+ Huddle *SHRoomHuddle `json:"huddle,omitempty"`
+ EventTS string `json:"event_ts"`
+ TS string `json:"ts"`
+}
+
+// SHRoomUpdateEvent is fired when a Slack Call/Huddle room is updated.
+type SHRoomUpdateEvent struct {
+ Type string `json:"type"`
+ Room SHRoom `json:"room"`
+ User string `json:"user"`
+ Huddle *SHRoomHuddle `json:"huddle,omitempty"`
+ EventTS string `json:"event_ts"`
+ TS string `json:"ts"`
+}
+
+// AppsUninstalledEvent represents the apps_uninstalled event sent via RTM
+// when one or more apps are uninstalled from the workspace.
+type AppsUninstalledEvent struct {
+ Type string `json:"type"`
+}
+
+// ActivityEvent represents the activity event sent via RTM. This is an
+// internal Slack event that fires during normal workspace usage (e.g. new
+// messages, bundle updates).
+type ActivityEvent struct {
+ Type string `json:"type"`
+ SubType string `json:"subtype"`
+ Key string `json:"key"`
+ Entry json.RawMessage `json:"entry"`
+ EventTimestamp string `json:"event_ts"`
+}
+
+// BadgeCountsUpdatedEvent represents the badge_counts_updated event sent via
+// RTM when notification badge counts change.
+type BadgeCountsUpdatedEvent struct {
+ Type string `json:"type"`
+}
diff --git a/backend/vendor/github.com/slack-go/slack/workflows_featured.go b/backend/vendor/github.com/slack-go/slack/workflows_featured.go
new file mode 100644
index 00000000..08b33986
--- /dev/null
+++ b/backend/vendor/github.com/slack-go/slack/workflows_featured.go
@@ -0,0 +1,143 @@
+package slack
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+)
+
+type (
+ FeaturedWorkflowTrigger struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ }
+
+ FeaturedWorkflow struct {
+ ChannelID string `json:"channel_id"`
+ Triggers []FeaturedWorkflowTrigger `json:"triggers"`
+ }
+
+ WorkflowsFeaturedAddInput struct {
+ ChannelID string `json:"channel_id"`
+ TriggerIDs []string `json:"trigger_ids"`
+ }
+
+ WorkflowsFeaturedListInput struct {
+ ChannelIDs []string `json:"channel_ids"`
+ }
+
+ WorkflowsFeaturedListOutput struct {
+ FeaturedWorkflows []FeaturedWorkflow `json:"featured_workflows"`
+ }
+
+ WorkflowsFeaturedRemoveInput struct {
+ ChannelID string `json:"channel_id"`
+ TriggerIDs []string `json:"trigger_ids"`
+ }
+
+ WorkflowsFeaturedSetInput struct {
+ ChannelID string `json:"channel_id"`
+ TriggerIDs []string `json:"trigger_ids"`
+ }
+)
+
+// WorkflowsFeaturedAdd adds featured workflows to a channel.
+//
+// Slack API Docs:https://api.slack.com/methods/workflows.featured.add
+func (api *Client) WorkflowsFeaturedAdd(ctx context.Context, input *WorkflowsFeaturedAddInput) error {
+ response := struct {
+ SlackResponse
+ }{}
+
+ jsonPayload, err := json.Marshal(input)
+ if err != nil {
+ return fmt.Errorf("failed to marshal WorkflowsFeaturedAddInput: %w", err)
+ }
+
+ err = api.postJSONMethod(ctx, "workflows.featured.add", api.token, jsonPayload, &response)
+ if err != nil {
+ return err
+ }
+
+ if err := response.Err(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// WorkflowsFeaturedList lists featured workflows for the given channels.
+//
+// Slack API Docs:https://api.slack.com/methods/workflows.featured.list
+func (api *Client) WorkflowsFeaturedList(ctx context.Context, input *WorkflowsFeaturedListInput) (*WorkflowsFeaturedListOutput, error) {
+ response := struct {
+ SlackResponse
+ *WorkflowsFeaturedListOutput
+ }{}
+
+ jsonPayload, err := json.Marshal(input)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal WorkflowsFeaturedListInput: %w", err)
+ }
+
+ err = api.postJSONMethod(ctx, "workflows.featured.list", api.token, jsonPayload, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := response.Err(); err != nil {
+ return nil, err
+ }
+
+ return response.WorkflowsFeaturedListOutput, nil
+}
+
+// WorkflowsFeaturedRemove removes featured workflows from a channel.
+//
+// Slack API Docs:https://api.slack.com/methods/workflows.featured.remove
+func (api *Client) WorkflowsFeaturedRemove(ctx context.Context, input *WorkflowsFeaturedRemoveInput) error {
+ response := struct {
+ SlackResponse
+ }{}
+
+ jsonPayload, err := json.Marshal(input)
+ if err != nil {
+ return fmt.Errorf("failed to marshal WorkflowsFeaturedRemoveInput: %w", err)
+ }
+
+ err = api.postJSONMethod(ctx, "workflows.featured.remove", api.token, jsonPayload, &response)
+ if err != nil {
+ return err
+ }
+
+ if err := response.Err(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// WorkflowsFeaturedSet replaces all featured workflows in a channel with the given triggers.
+//
+// Slack API Docs:https://api.slack.com/methods/workflows.featured.set
+func (api *Client) WorkflowsFeaturedSet(ctx context.Context, input *WorkflowsFeaturedSetInput) error {
+ response := struct {
+ SlackResponse
+ }{}
+
+ jsonPayload, err := json.Marshal(input)
+ if err != nil {
+ return fmt.Errorf("failed to marshal WorkflowsFeaturedSetInput: %w", err)
+ }
+
+ err = api.postJSONMethod(ctx, "workflows.featured.set", api.token, jsonPayload, &response)
+ if err != nil {
+ return err
+ }
+
+ if err := response.Err(); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/backend/vendor/github.com/slack-go/slack/workflows_triggers.go b/backend/vendor/github.com/slack-go/slack/workflows_triggers.go
index f9a7cd90..34834806 100644
--- a/backend/vendor/github.com/slack-go/slack/workflows_triggers.go
+++ b/backend/vendor/github.com/slack-go/slack/workflows_triggers.go
@@ -84,7 +84,7 @@ func (api *Client) WorkflowsTriggersPermissionsAdd(ctx context.Context, input *W
return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsAddInput: %w", err)
}
- err = postJSON(ctx, api.httpclient, api.endpoint+"workflows.triggers.permissions.add", api.token, jsonPayload, &response, api)
+ err = api.postJSONMethod(ctx, "workflows.triggers.permissions.add", api.token, jsonPayload, &response)
if err != nil {
return nil, err
}
@@ -111,7 +111,7 @@ func (api *Client) WorkflowsTriggersPermissionsList(ctx context.Context, input *
return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsListInput: %w", err)
}
- err = postJSON(ctx, api.httpclient, api.endpoint+"workflows.triggers.permissions.list", api.token, jsonPayload, &response, api)
+ err = api.postJSONMethod(ctx, "workflows.triggers.permissions.list", api.token, jsonPayload, &response)
if err != nil {
return nil, err
}
@@ -138,7 +138,7 @@ func (api *Client) WorkflowsTriggersPermissionsRemove(ctx context.Context, input
return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsRemoveInput: %w", err)
}
- err = postJSON(ctx, api.httpclient, api.endpoint+"workflows.triggers.permissions.remove", api.token, jsonPayload, &response, api)
+ err = api.postJSONMethod(ctx, "workflows.triggers.permissions.remove", api.token, jsonPayload, &response)
if err != nil {
return nil, err
}
@@ -164,7 +164,7 @@ func (api *Client) WorkflowsTriggersPermissionsSet(ctx context.Context, input *W
return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsSetInput: %w", err)
}
- err = postJSON(ctx, api.httpclient, api.endpoint+"workflows.triggers.permissions.set", api.token, jsonPayload, &response, api)
+ err = api.postJSONMethod(ctx, "workflows.triggers.permissions.set", api.token, jsonPayload, &response)
if err != nil {
return nil, err
}
diff --git a/backend/vendor/go.mongodb.org/mongo-driver/mongo/options/clientoptions.go b/backend/vendor/go.mongodb.org/mongo-driver/mongo/options/clientoptions.go
index c3a9d439..b1dc0b6a 100644
--- a/backend/vendor/go.mongodb.org/mongo-driver/mongo/options/clientoptions.go
+++ b/backend/vendor/go.mongodb.org/mongo-driver/mongo/options/clientoptions.go
@@ -1062,9 +1062,6 @@ func (c *ClientOptions) SetSRVServiceName(srvName string) *ClientOptions {
// MergeClientOptions combines the given *ClientOptions into a single *ClientOptions in a last one wins fashion.
// The specified options are merged with the existing options on the client, with the specified options taking
// precedence.
-//
-// Deprecated: Merging options structs will not be supported in Go Driver 2.0. Users should create a
-// single options struct instead.
func MergeClientOptions(opts ...*ClientOptions) *ClientOptions {
c := Client()
diff --git a/backend/vendor/go.mongodb.org/mongo-driver/version/version.go b/backend/vendor/go.mongodb.org/mongo-driver/version/version.go
index ace11008..a727d0fb 100644
--- a/backend/vendor/go.mongodb.org/mongo-driver/version/version.go
+++ b/backend/vendor/go.mongodb.org/mongo-driver/version/version.go
@@ -11,4 +11,4 @@
package version
// Driver is the current version of the driver.
-var Driver = "1.17.6"
+var Driver = "1.17.9"
diff --git a/backend/vendor/go.mongodb.org/mongo-driver/x/mongo/driver/auth/internal/gssapi/gss_wrapper.c b/backend/vendor/go.mongodb.org/mongo-driver/x/mongo/driver/auth/internal/gssapi/gss_wrapper.c
index 68b72541..e426037e 100644
--- a/backend/vendor/go.mongodb.org/mongo-driver/x/mongo/driver/auth/internal/gssapi/gss_wrapper.c
+++ b/backend/vendor/go.mongodb.org/mongo-driver/x/mongo/driver/auth/internal/gssapi/gss_wrapper.c
@@ -72,8 +72,8 @@ int gssapi_error_desc(
free(*desc);
}
- *desc = malloc(desc_buffer.length+1);
- memcpy(*desc, desc_buffer.value, desc_buffer.length+1);
+ *desc = calloc(1, desc_buffer.length + 1);
+ memcpy(*desc, desc_buffer.value, desc_buffer.length);
gss_release_buffer(&local_min_stat, &desc_buffer);
}
@@ -144,8 +144,8 @@ int gssapi_client_username(
return GSSAPI_ERROR;
}
- *username = malloc(name_buffer.length+1);
- memcpy(*username, name_buffer.value, name_buffer.length+1);
+ *username = calloc(1, name_buffer.length + 1);
+ memcpy(*username, name_buffer.value, name_buffer.length);
gss_release_buffer(&ignored, &name_buffer);
gss_release_name(&ignored, &name);
diff --git a/backend/vendor/go.mongodb.org/mongo-driver/x/mongo/driver/topology/rtt_monitor.go b/backend/vendor/go.mongodb.org/mongo-driver/x/mongo/driver/topology/rtt_monitor.go
index c7b168dc..cc37db59 100644
--- a/backend/vendor/go.mongodb.org/mongo-driver/x/mongo/driver/topology/rtt_monitor.go
+++ b/backend/vendor/go.mongodb.org/mongo-driver/x/mongo/driver/topology/rtt_monitor.go
@@ -119,7 +119,7 @@ func (r *rttMonitor) start() {
defer ticker.Stop()
for {
- conn := r.cfg.createConnectionFn()
+ conn = r.cfg.createConnectionFn()
err := conn.connect(r.ctx)
// Add an RTT sample from the new connection handshake and start a runHellos() loop if we
diff --git a/backend/vendor/go.uber.org/atomic/.codecov.yml b/backend/vendor/go.uber.org/atomic/.codecov.yml
new file mode 100644
index 00000000..571116cc
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/.codecov.yml
@@ -0,0 +1,19 @@
+coverage:
+ range: 80..100
+ round: down
+ precision: 2
+
+ status:
+ project: # measuring the overall project coverage
+ default: # context, you can create multiple ones with custom titles
+ enabled: yes # must be yes|true to enable this status
+ target: 100 # specify the target coverage for each commit status
+ # option: "auto" (must increase from parent commit or pull request base)
+ # option: "X%" a static target percentage to hit
+ if_not_found: success # if parent is not found report status as success, error, or failure
+ if_ci_failed: error # if ci fails report status as success, error, or failure
+
+# Also update COVER_IGNORE_PKGS in the Makefile.
+ignore:
+ - /internal/gen-atomicint/
+ - /internal/gen-valuewrapper/
diff --git a/backend/vendor/go.uber.org/atomic/.gitignore b/backend/vendor/go.uber.org/atomic/.gitignore
new file mode 100644
index 00000000..2e337a0e
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/.gitignore
@@ -0,0 +1,15 @@
+/bin
+.DS_Store
+/vendor
+cover.html
+cover.out
+lint.log
+
+# Binaries
+*.test
+
+# Profiling output
+*.prof
+
+# Output of fossa analyzer
+/fossa
diff --git a/backend/vendor/go.uber.org/atomic/CHANGELOG.md b/backend/vendor/go.uber.org/atomic/CHANGELOG.md
new file mode 100644
index 00000000..6f87f33f
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/CHANGELOG.md
@@ -0,0 +1,127 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.11.0] - 2023-05-02
+### Fixed
+- Fix initialization of `Value` wrappers.
+
+### Added
+- Add `String` method to `atomic.Pointer[T]` type allowing users to safely print
+underlying values of pointers.
+
+[1.11.0]: https://github.com/uber-go/atomic/compare/v1.10.0...v1.11.0
+
+## [1.10.0] - 2022-08-11
+### Added
+- Add `atomic.Float32` type for atomic operations on `float32`.
+- Add `CompareAndSwap` and `Swap` methods to `atomic.String`, `atomic.Error`,
+ and `atomic.Value`.
+- Add generic `atomic.Pointer[T]` type for atomic operations on pointers of any
+ type. This is present only for Go 1.18 or higher, and is a drop-in for
+ replacement for the standard library's `sync/atomic.Pointer` type.
+
+### Changed
+- Deprecate `CAS` methods on all types in favor of corresponding
+ `CompareAndSwap` methods.
+
+Thanks to @eNV25 and @icpd for their contributions to this release.
+
+[1.10.0]: https://github.com/uber-go/atomic/compare/v1.9.0...v1.10.0
+
+## [1.9.0] - 2021-07-15
+### Added
+- Add `Float64.Swap` to match int atomic operations.
+- Add `atomic.Time` type for atomic operations on `time.Time` values.
+
+[1.9.0]: https://github.com/uber-go/atomic/compare/v1.8.0...v1.9.0
+
+## [1.8.0] - 2021-06-09
+### Added
+- Add `atomic.Uintptr` type for atomic operations on `uintptr` values.
+- Add `atomic.UnsafePointer` type for atomic operations on `unsafe.Pointer` values.
+
+[1.8.0]: https://github.com/uber-go/atomic/compare/v1.7.0...v1.8.0
+
+## [1.7.0] - 2020-09-14
+### Added
+- Support JSON serialization and deserialization of primitive atomic types.
+- Support Text marshalling and unmarshalling for string atomics.
+
+### Changed
+- Disallow incorrect comparison of atomic values in a non-atomic way.
+
+### Removed
+- Remove dependency on `golang.org/x/{lint, tools}`.
+
+[1.7.0]: https://github.com/uber-go/atomic/compare/v1.6.0...v1.7.0
+
+## [1.6.0] - 2020-02-24
+### Changed
+- Drop library dependency on `golang.org/x/{lint, tools}`.
+
+[1.6.0]: https://github.com/uber-go/atomic/compare/v1.5.1...v1.6.0
+
+## [1.5.1] - 2019-11-19
+- Fix bug where `Bool.CAS` and `Bool.Toggle` do work correctly together
+ causing `CAS` to fail even though the old value matches.
+
+[1.5.1]: https://github.com/uber-go/atomic/compare/v1.5.0...v1.5.1
+
+## [1.5.0] - 2019-10-29
+### Changed
+- With Go modules, only the `go.uber.org/atomic` import path is supported now.
+ If you need to use the old import path, please add a `replace` directive to
+ your `go.mod`.
+
+[1.5.0]: https://github.com/uber-go/atomic/compare/v1.4.0...v1.5.0
+
+## [1.4.0] - 2019-05-01
+### Added
+ - Add `atomic.Error` type for atomic operations on `error` values.
+
+[1.4.0]: https://github.com/uber-go/atomic/compare/v1.3.2...v1.4.0
+
+## [1.3.2] - 2018-05-02
+### Added
+- Add `atomic.Duration` type for atomic operations on `time.Duration` values.
+
+[1.3.2]: https://github.com/uber-go/atomic/compare/v1.3.1...v1.3.2
+
+## [1.3.1] - 2017-11-14
+### Fixed
+- Revert optimization for `atomic.String.Store("")` which caused data races.
+
+[1.3.1]: https://github.com/uber-go/atomic/compare/v1.3.0...v1.3.1
+
+## [1.3.0] - 2017-11-13
+### Added
+- Add `atomic.Bool.CAS` for compare-and-swap semantics on bools.
+
+### Changed
+- Optimize `atomic.String.Store("")` by avoiding an allocation.
+
+[1.3.0]: https://github.com/uber-go/atomic/compare/v1.2.0...v1.3.0
+
+## [1.2.0] - 2017-04-12
+### Added
+- Shadow `atomic.Value` from `sync/atomic`.
+
+[1.2.0]: https://github.com/uber-go/atomic/compare/v1.1.0...v1.2.0
+
+## [1.1.0] - 2017-03-10
+### Added
+- Add atomic `Float64` type.
+
+### Changed
+- Support new `go.uber.org/atomic` import path.
+
+[1.1.0]: https://github.com/uber-go/atomic/compare/v1.0.0...v1.1.0
+
+## [1.0.0] - 2016-07-18
+
+- Initial release.
+
+[1.0.0]: https://github.com/uber-go/atomic/releases/tag/v1.0.0
diff --git a/backend/vendor/go.uber.org/atomic/LICENSE.txt b/backend/vendor/go.uber.org/atomic/LICENSE.txt
new file mode 100644
index 00000000..8765c9fb
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2016 Uber Technologies, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/backend/vendor/go.uber.org/atomic/Makefile b/backend/vendor/go.uber.org/atomic/Makefile
new file mode 100644
index 00000000..46c945b3
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/Makefile
@@ -0,0 +1,79 @@
+# Directory to place `go install`ed binaries into.
+export GOBIN ?= $(shell pwd)/bin
+
+GOLINT = $(GOBIN)/golint
+GEN_ATOMICINT = $(GOBIN)/gen-atomicint
+GEN_ATOMICWRAPPER = $(GOBIN)/gen-atomicwrapper
+STATICCHECK = $(GOBIN)/staticcheck
+
+GO_FILES ?= $(shell find . '(' -path .git -o -path vendor ')' -prune -o -name '*.go' -print)
+
+# Also update ignore section in .codecov.yml.
+COVER_IGNORE_PKGS = \
+ go.uber.org/atomic/internal/gen-atomicint \
+ go.uber.org/atomic/internal/gen-atomicwrapper
+
+.PHONY: build
+build:
+ go build ./...
+
+.PHONY: test
+test:
+ go test -race ./...
+
+.PHONY: gofmt
+gofmt:
+ $(eval FMT_LOG := $(shell mktemp -t gofmt.XXXXX))
+ gofmt -e -s -l $(GO_FILES) > $(FMT_LOG) || true
+ @[ ! -s "$(FMT_LOG)" ] || (echo "gofmt failed:" && cat $(FMT_LOG) && false)
+
+$(GOLINT):
+ cd tools && go install golang.org/x/lint/golint
+
+$(STATICCHECK):
+ cd tools && go install honnef.co/go/tools/cmd/staticcheck
+
+$(GEN_ATOMICWRAPPER): $(wildcard ./internal/gen-atomicwrapper/*)
+ go build -o $@ ./internal/gen-atomicwrapper
+
+$(GEN_ATOMICINT): $(wildcard ./internal/gen-atomicint/*)
+ go build -o $@ ./internal/gen-atomicint
+
+.PHONY: golint
+golint: $(GOLINT)
+ $(GOLINT) ./...
+
+.PHONY: staticcheck
+staticcheck: $(STATICCHECK)
+ $(STATICCHECK) ./...
+
+.PHONY: lint
+lint: gofmt golint staticcheck generatenodirty
+
+# comma separated list of packages to consider for code coverage.
+COVER_PKG = $(shell \
+ go list -find ./... | \
+ grep -v $(foreach pkg,$(COVER_IGNORE_PKGS),-e "^$(pkg)$$") | \
+ paste -sd, -)
+
+.PHONY: cover
+cover:
+ go test -coverprofile=cover.out -coverpkg $(COVER_PKG) -v ./...
+ go tool cover -html=cover.out -o cover.html
+
+.PHONY: generate
+generate: $(GEN_ATOMICINT) $(GEN_ATOMICWRAPPER)
+ go generate ./...
+
+.PHONY: generatenodirty
+generatenodirty:
+ @[ -z "$$(git status --porcelain)" ] || ( \
+ echo "Working tree is dirty. Commit your changes first."; \
+ git status; \
+ exit 1 )
+ @make generate
+ @status=$$(git status --porcelain); \
+ [ -z "$$status" ] || ( \
+ echo "Working tree is dirty after `make generate`:"; \
+ echo "$$status"; \
+ echo "Please ensure that the generated code is up-to-date." )
diff --git a/backend/vendor/go.uber.org/atomic/README.md b/backend/vendor/go.uber.org/atomic/README.md
new file mode 100644
index 00000000..96b47a1f
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/README.md
@@ -0,0 +1,63 @@
+# atomic [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![Go Report Card][reportcard-img]][reportcard]
+
+Simple wrappers for primitive types to enforce atomic access.
+
+## Installation
+
+```shell
+$ go get -u go.uber.org/atomic@v1
+```
+
+### Legacy Import Path
+
+As of v1.5.0, the import path `go.uber.org/atomic` is the only supported way
+of using this package. If you are using Go modules, this package will fail to
+compile with the legacy import path path `github.com/uber-go/atomic`.
+
+We recommend migrating your code to the new import path but if you're unable
+to do so, or if your dependencies are still using the old import path, you
+will have to add a `replace` directive to your `go.mod` file downgrading the
+legacy import path to an older version.
+
+```
+replace github.com/uber-go/atomic => github.com/uber-go/atomic v1.4.0
+```
+
+You can do so automatically by running the following command.
+
+```shell
+$ go mod edit -replace github.com/uber-go/atomic=github.com/uber-go/atomic@v1.4.0
+```
+
+## Usage
+
+The standard library's `sync/atomic` is powerful, but it's easy to forget which
+variables must be accessed atomically. `go.uber.org/atomic` preserves all the
+functionality of the standard library, but wraps the primitive types to
+provide a safer, more convenient API.
+
+```go
+var atom atomic.Uint32
+atom.Store(42)
+atom.Sub(2)
+atom.CAS(40, 11)
+```
+
+See the [documentation][doc] for a complete API specification.
+
+## Development Status
+
+Stable.
+
+---
+
+Released under the [MIT License](LICENSE.txt).
+
+[doc-img]: https://godoc.org/github.com/uber-go/atomic?status.svg
+[doc]: https://godoc.org/go.uber.org/atomic
+[ci-img]: https://github.com/uber-go/atomic/actions/workflows/go.yml/badge.svg
+[ci]: https://github.com/uber-go/atomic/actions/workflows/go.yml
+[cov-img]: https://codecov.io/gh/uber-go/atomic/branch/master/graph/badge.svg
+[cov]: https://codecov.io/gh/uber-go/atomic
+[reportcard-img]: https://goreportcard.com/badge/go.uber.org/atomic
+[reportcard]: https://goreportcard.com/report/go.uber.org/atomic
diff --git a/backend/vendor/go.uber.org/atomic/bool.go b/backend/vendor/go.uber.org/atomic/bool.go
new file mode 100644
index 00000000..f0a2ddd1
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/bool.go
@@ -0,0 +1,88 @@
+// @generated Code generated by gen-atomicwrapper.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+)
+
+// Bool is an atomic type-safe wrapper for bool values.
+type Bool struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v Uint32
+}
+
+var _zeroBool bool
+
+// NewBool creates a new Bool.
+func NewBool(val bool) *Bool {
+ x := &Bool{}
+ if val != _zeroBool {
+ x.Store(val)
+ }
+ return x
+}
+
+// Load atomically loads the wrapped bool.
+func (x *Bool) Load() bool {
+ return truthy(x.v.Load())
+}
+
+// Store atomically stores the passed bool.
+func (x *Bool) Store(val bool) {
+ x.v.Store(boolToInt(val))
+}
+
+// CAS is an atomic compare-and-swap for bool values.
+//
+// Deprecated: Use CompareAndSwap.
+func (x *Bool) CAS(old, new bool) (swapped bool) {
+ return x.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap for bool values.
+func (x *Bool) CompareAndSwap(old, new bool) (swapped bool) {
+ return x.v.CompareAndSwap(boolToInt(old), boolToInt(new))
+}
+
+// Swap atomically stores the given bool and returns the old
+// value.
+func (x *Bool) Swap(val bool) (old bool) {
+ return truthy(x.v.Swap(boolToInt(val)))
+}
+
+// MarshalJSON encodes the wrapped bool into JSON.
+func (x *Bool) MarshalJSON() ([]byte, error) {
+ return json.Marshal(x.Load())
+}
+
+// UnmarshalJSON decodes a bool from JSON.
+func (x *Bool) UnmarshalJSON(b []byte) error {
+ var v bool
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ x.Store(v)
+ return nil
+}
diff --git a/backend/vendor/go.uber.org/atomic/bool_ext.go b/backend/vendor/go.uber.org/atomic/bool_ext.go
new file mode 100644
index 00000000..a2e60e98
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/bool_ext.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2020 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "strconv"
+)
+
+//go:generate bin/gen-atomicwrapper -name=Bool -type=bool -wrapped=Uint32 -pack=boolToInt -unpack=truthy -cas -swap -json -file=bool.go
+
+func truthy(n uint32) bool {
+ return n == 1
+}
+
+func boolToInt(b bool) uint32 {
+ if b {
+ return 1
+ }
+ return 0
+}
+
+// Toggle atomically negates the Boolean and returns the previous value.
+func (b *Bool) Toggle() (old bool) {
+ for {
+ old := b.Load()
+ if b.CAS(old, !old) {
+ return old
+ }
+ }
+}
+
+// String encodes the wrapped value as a string.
+func (b *Bool) String() string {
+ return strconv.FormatBool(b.Load())
+}
diff --git a/backend/vendor/go.uber.org/atomic/doc.go b/backend/vendor/go.uber.org/atomic/doc.go
new file mode 100644
index 00000000..ae7390ee
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/doc.go
@@ -0,0 +1,23 @@
+// Copyright (c) 2020 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// Package atomic provides simple wrappers around numerics to enforce atomic
+// access.
+package atomic
diff --git a/backend/vendor/go.uber.org/atomic/duration.go b/backend/vendor/go.uber.org/atomic/duration.go
new file mode 100644
index 00000000..7c23868f
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/duration.go
@@ -0,0 +1,89 @@
+// @generated Code generated by gen-atomicwrapper.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+ "time"
+)
+
+// Duration is an atomic type-safe wrapper for time.Duration values.
+type Duration struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v Int64
+}
+
+var _zeroDuration time.Duration
+
+// NewDuration creates a new Duration.
+func NewDuration(val time.Duration) *Duration {
+ x := &Duration{}
+ if val != _zeroDuration {
+ x.Store(val)
+ }
+ return x
+}
+
+// Load atomically loads the wrapped time.Duration.
+func (x *Duration) Load() time.Duration {
+ return time.Duration(x.v.Load())
+}
+
+// Store atomically stores the passed time.Duration.
+func (x *Duration) Store(val time.Duration) {
+ x.v.Store(int64(val))
+}
+
+// CAS is an atomic compare-and-swap for time.Duration values.
+//
+// Deprecated: Use CompareAndSwap.
+func (x *Duration) CAS(old, new time.Duration) (swapped bool) {
+ return x.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap for time.Duration values.
+func (x *Duration) CompareAndSwap(old, new time.Duration) (swapped bool) {
+ return x.v.CompareAndSwap(int64(old), int64(new))
+}
+
+// Swap atomically stores the given time.Duration and returns the old
+// value.
+func (x *Duration) Swap(val time.Duration) (old time.Duration) {
+ return time.Duration(x.v.Swap(int64(val)))
+}
+
+// MarshalJSON encodes the wrapped time.Duration into JSON.
+func (x *Duration) MarshalJSON() ([]byte, error) {
+ return json.Marshal(x.Load())
+}
+
+// UnmarshalJSON decodes a time.Duration from JSON.
+func (x *Duration) UnmarshalJSON(b []byte) error {
+ var v time.Duration
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ x.Store(v)
+ return nil
+}
diff --git a/backend/vendor/go.uber.org/atomic/duration_ext.go b/backend/vendor/go.uber.org/atomic/duration_ext.go
new file mode 100644
index 00000000..4c18b0a9
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/duration_ext.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2020 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import "time"
+
+//go:generate bin/gen-atomicwrapper -name=Duration -type=time.Duration -wrapped=Int64 -pack=int64 -unpack=time.Duration -cas -swap -json -imports time -file=duration.go
+
+// Add atomically adds to the wrapped time.Duration and returns the new value.
+func (d *Duration) Add(delta time.Duration) time.Duration {
+ return time.Duration(d.v.Add(int64(delta)))
+}
+
+// Sub atomically subtracts from the wrapped time.Duration and returns the new value.
+func (d *Duration) Sub(delta time.Duration) time.Duration {
+ return time.Duration(d.v.Sub(int64(delta)))
+}
+
+// String encodes the wrapped value as a string.
+func (d *Duration) String() string {
+ return d.Load().String()
+}
diff --git a/backend/vendor/go.uber.org/atomic/error.go b/backend/vendor/go.uber.org/atomic/error.go
new file mode 100644
index 00000000..b7e3f129
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/error.go
@@ -0,0 +1,72 @@
+// @generated Code generated by gen-atomicwrapper.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+// Error is an atomic type-safe wrapper for error values.
+type Error struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v Value
+}
+
+var _zeroError error
+
+// NewError creates a new Error.
+func NewError(val error) *Error {
+ x := &Error{}
+ if val != _zeroError {
+ x.Store(val)
+ }
+ return x
+}
+
+// Load atomically loads the wrapped error.
+func (x *Error) Load() error {
+ return unpackError(x.v.Load())
+}
+
+// Store atomically stores the passed error.
+func (x *Error) Store(val error) {
+ x.v.Store(packError(val))
+}
+
+// CompareAndSwap is an atomic compare-and-swap for error values.
+func (x *Error) CompareAndSwap(old, new error) (swapped bool) {
+ if x.v.CompareAndSwap(packError(old), packError(new)) {
+ return true
+ }
+
+ if old == _zeroError {
+ // If the old value is the empty value, then it's possible the
+ // underlying Value hasn't been set and is nil, so retry with nil.
+ return x.v.CompareAndSwap(nil, packError(new))
+ }
+
+ return false
+}
+
+// Swap atomically stores the given error and returns the old
+// value.
+func (x *Error) Swap(val error) (old error) {
+ return unpackError(x.v.Swap(packError(val)))
+}
diff --git a/backend/vendor/go.uber.org/atomic/error_ext.go b/backend/vendor/go.uber.org/atomic/error_ext.go
new file mode 100644
index 00000000..d31fb633
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/error_ext.go
@@ -0,0 +1,39 @@
+// Copyright (c) 2020-2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+// atomic.Value panics on nil inputs, or if the underlying type changes.
+// Stabilize by always storing a custom struct that we control.
+
+//go:generate bin/gen-atomicwrapper -name=Error -type=error -wrapped=Value -pack=packError -unpack=unpackError -compareandswap -swap -file=error.go
+
+type packedError struct{ Value error }
+
+func packError(v error) interface{} {
+ return packedError{v}
+}
+
+func unpackError(v interface{}) error {
+ if err, ok := v.(packedError); ok {
+ return err.Value
+ }
+ return nil
+}
diff --git a/backend/vendor/go.uber.org/atomic/float32.go b/backend/vendor/go.uber.org/atomic/float32.go
new file mode 100644
index 00000000..62c36334
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/float32.go
@@ -0,0 +1,77 @@
+// @generated Code generated by gen-atomicwrapper.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+ "math"
+)
+
+// Float32 is an atomic type-safe wrapper for float32 values.
+type Float32 struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v Uint32
+}
+
+var _zeroFloat32 float32
+
+// NewFloat32 creates a new Float32.
+func NewFloat32(val float32) *Float32 {
+ x := &Float32{}
+ if val != _zeroFloat32 {
+ x.Store(val)
+ }
+ return x
+}
+
+// Load atomically loads the wrapped float32.
+func (x *Float32) Load() float32 {
+ return math.Float32frombits(x.v.Load())
+}
+
+// Store atomically stores the passed float32.
+func (x *Float32) Store(val float32) {
+ x.v.Store(math.Float32bits(val))
+}
+
+// Swap atomically stores the given float32 and returns the old
+// value.
+func (x *Float32) Swap(val float32) (old float32) {
+ return math.Float32frombits(x.v.Swap(math.Float32bits(val)))
+}
+
+// MarshalJSON encodes the wrapped float32 into JSON.
+func (x *Float32) MarshalJSON() ([]byte, error) {
+ return json.Marshal(x.Load())
+}
+
+// UnmarshalJSON decodes a float32 from JSON.
+func (x *Float32) UnmarshalJSON(b []byte) error {
+ var v float32
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ x.Store(v)
+ return nil
+}
diff --git a/backend/vendor/go.uber.org/atomic/float32_ext.go b/backend/vendor/go.uber.org/atomic/float32_ext.go
new file mode 100644
index 00000000..b0cd8d9c
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/float32_ext.go
@@ -0,0 +1,76 @@
+// Copyright (c) 2020-2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "math"
+ "strconv"
+)
+
+//go:generate bin/gen-atomicwrapper -name=Float32 -type=float32 -wrapped=Uint32 -pack=math.Float32bits -unpack=math.Float32frombits -swap -json -imports math -file=float32.go
+
+// Add atomically adds to the wrapped float32 and returns the new value.
+func (f *Float32) Add(delta float32) float32 {
+ for {
+ old := f.Load()
+ new := old + delta
+ if f.CAS(old, new) {
+ return new
+ }
+ }
+}
+
+// Sub atomically subtracts from the wrapped float32 and returns the new value.
+func (f *Float32) Sub(delta float32) float32 {
+ return f.Add(-delta)
+}
+
+// CAS is an atomic compare-and-swap for float32 values.
+//
+// Deprecated: Use CompareAndSwap
+func (f *Float32) CAS(old, new float32) (swapped bool) {
+ return f.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap for float32 values.
+//
+// Note: CompareAndSwap handles NaN incorrectly. NaN != NaN using Go's inbuilt operators
+// but CompareAndSwap allows a stored NaN to compare equal to a passed in NaN.
+// This avoids typical CompareAndSwap loops from blocking forever, e.g.,
+//
+// for {
+// old := atom.Load()
+// new = f(old)
+// if atom.CompareAndSwap(old, new) {
+// break
+// }
+// }
+//
+// If CompareAndSwap did not match NaN to match, then the above would loop forever.
+func (f *Float32) CompareAndSwap(old, new float32) (swapped bool) {
+ return f.v.CompareAndSwap(math.Float32bits(old), math.Float32bits(new))
+}
+
+// String encodes the wrapped value as a string.
+func (f *Float32) String() string {
+ // 'g' is the behavior for floats with %v.
+ return strconv.FormatFloat(float64(f.Load()), 'g', -1, 32)
+}
diff --git a/backend/vendor/go.uber.org/atomic/float64.go b/backend/vendor/go.uber.org/atomic/float64.go
new file mode 100644
index 00000000..5bc11caa
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/float64.go
@@ -0,0 +1,77 @@
+// @generated Code generated by gen-atomicwrapper.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+ "math"
+)
+
+// Float64 is an atomic type-safe wrapper for float64 values.
+type Float64 struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v Uint64
+}
+
+var _zeroFloat64 float64
+
+// NewFloat64 creates a new Float64.
+func NewFloat64(val float64) *Float64 {
+ x := &Float64{}
+ if val != _zeroFloat64 {
+ x.Store(val)
+ }
+ return x
+}
+
+// Load atomically loads the wrapped float64.
+func (x *Float64) Load() float64 {
+ return math.Float64frombits(x.v.Load())
+}
+
+// Store atomically stores the passed float64.
+func (x *Float64) Store(val float64) {
+ x.v.Store(math.Float64bits(val))
+}
+
+// Swap atomically stores the given float64 and returns the old
+// value.
+func (x *Float64) Swap(val float64) (old float64) {
+ return math.Float64frombits(x.v.Swap(math.Float64bits(val)))
+}
+
+// MarshalJSON encodes the wrapped float64 into JSON.
+func (x *Float64) MarshalJSON() ([]byte, error) {
+ return json.Marshal(x.Load())
+}
+
+// UnmarshalJSON decodes a float64 from JSON.
+func (x *Float64) UnmarshalJSON(b []byte) error {
+ var v float64
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ x.Store(v)
+ return nil
+}
diff --git a/backend/vendor/go.uber.org/atomic/float64_ext.go b/backend/vendor/go.uber.org/atomic/float64_ext.go
new file mode 100644
index 00000000..48c52b0a
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/float64_ext.go
@@ -0,0 +1,76 @@
+// Copyright (c) 2020-2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "math"
+ "strconv"
+)
+
+//go:generate bin/gen-atomicwrapper -name=Float64 -type=float64 -wrapped=Uint64 -pack=math.Float64bits -unpack=math.Float64frombits -swap -json -imports math -file=float64.go
+
+// Add atomically adds to the wrapped float64 and returns the new value.
+func (f *Float64) Add(delta float64) float64 {
+ for {
+ old := f.Load()
+ new := old + delta
+ if f.CAS(old, new) {
+ return new
+ }
+ }
+}
+
+// Sub atomically subtracts from the wrapped float64 and returns the new value.
+func (f *Float64) Sub(delta float64) float64 {
+ return f.Add(-delta)
+}
+
+// CAS is an atomic compare-and-swap for float64 values.
+//
+// Deprecated: Use CompareAndSwap
+func (f *Float64) CAS(old, new float64) (swapped bool) {
+ return f.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap for float64 values.
+//
+// Note: CompareAndSwap handles NaN incorrectly. NaN != NaN using Go's inbuilt operators
+// but CompareAndSwap allows a stored NaN to compare equal to a passed in NaN.
+// This avoids typical CompareAndSwap loops from blocking forever, e.g.,
+//
+// for {
+// old := atom.Load()
+// new = f(old)
+// if atom.CompareAndSwap(old, new) {
+// break
+// }
+// }
+//
+// If CompareAndSwap did not match NaN to match, then the above would loop forever.
+func (f *Float64) CompareAndSwap(old, new float64) (swapped bool) {
+ return f.v.CompareAndSwap(math.Float64bits(old), math.Float64bits(new))
+}
+
+// String encodes the wrapped value as a string.
+func (f *Float64) String() string {
+ // 'g' is the behavior for floats with %v.
+ return strconv.FormatFloat(f.Load(), 'g', -1, 64)
+}
diff --git a/backend/vendor/go.uber.org/atomic/gen.go b/backend/vendor/go.uber.org/atomic/gen.go
new file mode 100644
index 00000000..1e9ef4f8
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/gen.go
@@ -0,0 +1,27 @@
+// Copyright (c) 2020 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+//go:generate bin/gen-atomicint -name=Int32 -wrapped=int32 -file=int32.go
+//go:generate bin/gen-atomicint -name=Int64 -wrapped=int64 -file=int64.go
+//go:generate bin/gen-atomicint -name=Uint32 -wrapped=uint32 -unsigned -file=uint32.go
+//go:generate bin/gen-atomicint -name=Uint64 -wrapped=uint64 -unsigned -file=uint64.go
+//go:generate bin/gen-atomicint -name=Uintptr -wrapped=uintptr -unsigned -file=uintptr.go
diff --git a/backend/vendor/go.uber.org/atomic/int32.go b/backend/vendor/go.uber.org/atomic/int32.go
new file mode 100644
index 00000000..5320eac1
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/int32.go
@@ -0,0 +1,109 @@
+// @generated Code generated by gen-atomicint.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+ "strconv"
+ "sync/atomic"
+)
+
+// Int32 is an atomic wrapper around int32.
+type Int32 struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v int32
+}
+
+// NewInt32 creates a new Int32.
+func NewInt32(val int32) *Int32 {
+ return &Int32{v: val}
+}
+
+// Load atomically loads the wrapped value.
+func (i *Int32) Load() int32 {
+ return atomic.LoadInt32(&i.v)
+}
+
+// Add atomically adds to the wrapped int32 and returns the new value.
+func (i *Int32) Add(delta int32) int32 {
+ return atomic.AddInt32(&i.v, delta)
+}
+
+// Sub atomically subtracts from the wrapped int32 and returns the new value.
+func (i *Int32) Sub(delta int32) int32 {
+ return atomic.AddInt32(&i.v, -delta)
+}
+
+// Inc atomically increments the wrapped int32 and returns the new value.
+func (i *Int32) Inc() int32 {
+ return i.Add(1)
+}
+
+// Dec atomically decrements the wrapped int32 and returns the new value.
+func (i *Int32) Dec() int32 {
+ return i.Sub(1)
+}
+
+// CAS is an atomic compare-and-swap.
+//
+// Deprecated: Use CompareAndSwap.
+func (i *Int32) CAS(old, new int32) (swapped bool) {
+ return i.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap.
+func (i *Int32) CompareAndSwap(old, new int32) (swapped bool) {
+ return atomic.CompareAndSwapInt32(&i.v, old, new)
+}
+
+// Store atomically stores the passed value.
+func (i *Int32) Store(val int32) {
+ atomic.StoreInt32(&i.v, val)
+}
+
+// Swap atomically swaps the wrapped int32 and returns the old value.
+func (i *Int32) Swap(val int32) (old int32) {
+ return atomic.SwapInt32(&i.v, val)
+}
+
+// MarshalJSON encodes the wrapped int32 into JSON.
+func (i *Int32) MarshalJSON() ([]byte, error) {
+ return json.Marshal(i.Load())
+}
+
+// UnmarshalJSON decodes JSON into the wrapped int32.
+func (i *Int32) UnmarshalJSON(b []byte) error {
+ var v int32
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ i.Store(v)
+ return nil
+}
+
+// String encodes the wrapped value as a string.
+func (i *Int32) String() string {
+ v := i.Load()
+ return strconv.FormatInt(int64(v), 10)
+}
diff --git a/backend/vendor/go.uber.org/atomic/int64.go b/backend/vendor/go.uber.org/atomic/int64.go
new file mode 100644
index 00000000..460821d0
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/int64.go
@@ -0,0 +1,109 @@
+// @generated Code generated by gen-atomicint.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+ "strconv"
+ "sync/atomic"
+)
+
+// Int64 is an atomic wrapper around int64.
+type Int64 struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v int64
+}
+
+// NewInt64 creates a new Int64.
+func NewInt64(val int64) *Int64 {
+ return &Int64{v: val}
+}
+
+// Load atomically loads the wrapped value.
+func (i *Int64) Load() int64 {
+ return atomic.LoadInt64(&i.v)
+}
+
+// Add atomically adds to the wrapped int64 and returns the new value.
+func (i *Int64) Add(delta int64) int64 {
+ return atomic.AddInt64(&i.v, delta)
+}
+
+// Sub atomically subtracts from the wrapped int64 and returns the new value.
+func (i *Int64) Sub(delta int64) int64 {
+ return atomic.AddInt64(&i.v, -delta)
+}
+
+// Inc atomically increments the wrapped int64 and returns the new value.
+func (i *Int64) Inc() int64 {
+ return i.Add(1)
+}
+
+// Dec atomically decrements the wrapped int64 and returns the new value.
+func (i *Int64) Dec() int64 {
+ return i.Sub(1)
+}
+
+// CAS is an atomic compare-and-swap.
+//
+// Deprecated: Use CompareAndSwap.
+func (i *Int64) CAS(old, new int64) (swapped bool) {
+ return i.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap.
+func (i *Int64) CompareAndSwap(old, new int64) (swapped bool) {
+ return atomic.CompareAndSwapInt64(&i.v, old, new)
+}
+
+// Store atomically stores the passed value.
+func (i *Int64) Store(val int64) {
+ atomic.StoreInt64(&i.v, val)
+}
+
+// Swap atomically swaps the wrapped int64 and returns the old value.
+func (i *Int64) Swap(val int64) (old int64) {
+ return atomic.SwapInt64(&i.v, val)
+}
+
+// MarshalJSON encodes the wrapped int64 into JSON.
+func (i *Int64) MarshalJSON() ([]byte, error) {
+ return json.Marshal(i.Load())
+}
+
+// UnmarshalJSON decodes JSON into the wrapped int64.
+func (i *Int64) UnmarshalJSON(b []byte) error {
+ var v int64
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ i.Store(v)
+ return nil
+}
+
+// String encodes the wrapped value as a string.
+func (i *Int64) String() string {
+ v := i.Load()
+ return strconv.FormatInt(int64(v), 10)
+}
diff --git a/backend/vendor/go.uber.org/atomic/nocmp.go b/backend/vendor/go.uber.org/atomic/nocmp.go
new file mode 100644
index 00000000..54b74174
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/nocmp.go
@@ -0,0 +1,35 @@
+// Copyright (c) 2020 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+// nocmp is an uncomparable struct. Embed this inside another struct to make
+// it uncomparable.
+//
+// type Foo struct {
+// nocmp
+// // ...
+// }
+//
+// This DOES NOT:
+//
+// - Disallow shallow copies of structs
+// - Disallow comparison of pointers to uncomparable structs
+type nocmp [0]func()
diff --git a/backend/vendor/go.uber.org/atomic/pointer_go118.go b/backend/vendor/go.uber.org/atomic/pointer_go118.go
new file mode 100644
index 00000000..1fb6c03b
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/pointer_go118.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+//go:build go1.18
+// +build go1.18
+
+package atomic
+
+import "fmt"
+
+// String returns a human readable representation of a Pointer's underlying value.
+func (p *Pointer[T]) String() string {
+ return fmt.Sprint(p.Load())
+}
diff --git a/backend/vendor/go.uber.org/atomic/pointer_go118_pre119.go b/backend/vendor/go.uber.org/atomic/pointer_go118_pre119.go
new file mode 100644
index 00000000..e0f47dba
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/pointer_go118_pre119.go
@@ -0,0 +1,60 @@
+// Copyright (c) 2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+//go:build go1.18 && !go1.19
+// +build go1.18,!go1.19
+
+package atomic
+
+import "unsafe"
+
+type Pointer[T any] struct {
+ _ nocmp // disallow non-atomic comparison
+ p UnsafePointer
+}
+
+// NewPointer creates a new Pointer.
+func NewPointer[T any](v *T) *Pointer[T] {
+ var p Pointer[T]
+ if v != nil {
+ p.p.Store(unsafe.Pointer(v))
+ }
+ return &p
+}
+
+// Load atomically loads the wrapped value.
+func (p *Pointer[T]) Load() *T {
+ return (*T)(p.p.Load())
+}
+
+// Store atomically stores the passed value.
+func (p *Pointer[T]) Store(val *T) {
+ p.p.Store(unsafe.Pointer(val))
+}
+
+// Swap atomically swaps the wrapped pointer and returns the old value.
+func (p *Pointer[T]) Swap(val *T) (old *T) {
+ return (*T)(p.p.Swap(unsafe.Pointer(val)))
+}
+
+// CompareAndSwap is an atomic compare-and-swap.
+func (p *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) {
+ return p.p.CompareAndSwap(unsafe.Pointer(old), unsafe.Pointer(new))
+}
diff --git a/backend/vendor/go.uber.org/atomic/pointer_go119.go b/backend/vendor/go.uber.org/atomic/pointer_go119.go
new file mode 100644
index 00000000..6726f17a
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/pointer_go119.go
@@ -0,0 +1,61 @@
+// Copyright (c) 2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+//go:build go1.19
+// +build go1.19
+
+package atomic
+
+import "sync/atomic"
+
+// Pointer is an atomic pointer of type *T.
+type Pointer[T any] struct {
+ _ nocmp // disallow non-atomic comparison
+ p atomic.Pointer[T]
+}
+
+// NewPointer creates a new Pointer.
+func NewPointer[T any](v *T) *Pointer[T] {
+ var p Pointer[T]
+ if v != nil {
+ p.p.Store(v)
+ }
+ return &p
+}
+
+// Load atomically loads the wrapped value.
+func (p *Pointer[T]) Load() *T {
+ return p.p.Load()
+}
+
+// Store atomically stores the passed value.
+func (p *Pointer[T]) Store(val *T) {
+ p.p.Store(val)
+}
+
+// Swap atomically swaps the wrapped pointer and returns the old value.
+func (p *Pointer[T]) Swap(val *T) (old *T) {
+ return p.p.Swap(val)
+}
+
+// CompareAndSwap is an atomic compare-and-swap.
+func (p *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) {
+ return p.p.CompareAndSwap(old, new)
+}
diff --git a/backend/vendor/go.uber.org/atomic/string.go b/backend/vendor/go.uber.org/atomic/string.go
new file mode 100644
index 00000000..061466c5
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/string.go
@@ -0,0 +1,72 @@
+// @generated Code generated by gen-atomicwrapper.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+// String is an atomic type-safe wrapper for string values.
+type String struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v Value
+}
+
+var _zeroString string
+
+// NewString creates a new String.
+func NewString(val string) *String {
+ x := &String{}
+ if val != _zeroString {
+ x.Store(val)
+ }
+ return x
+}
+
+// Load atomically loads the wrapped string.
+func (x *String) Load() string {
+ return unpackString(x.v.Load())
+}
+
+// Store atomically stores the passed string.
+func (x *String) Store(val string) {
+ x.v.Store(packString(val))
+}
+
+// CompareAndSwap is an atomic compare-and-swap for string values.
+func (x *String) CompareAndSwap(old, new string) (swapped bool) {
+ if x.v.CompareAndSwap(packString(old), packString(new)) {
+ return true
+ }
+
+ if old == _zeroString {
+ // If the old value is the empty value, then it's possible the
+ // underlying Value hasn't been set and is nil, so retry with nil.
+ return x.v.CompareAndSwap(nil, packString(new))
+ }
+
+ return false
+}
+
+// Swap atomically stores the given string and returns the old
+// value.
+func (x *String) Swap(val string) (old string) {
+ return unpackString(x.v.Swap(packString(val)))
+}
diff --git a/backend/vendor/go.uber.org/atomic/string_ext.go b/backend/vendor/go.uber.org/atomic/string_ext.go
new file mode 100644
index 00000000..019109c8
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/string_ext.go
@@ -0,0 +1,54 @@
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+//go:generate bin/gen-atomicwrapper -name=String -type=string -wrapped Value -pack packString -unpack unpackString -compareandswap -swap -file=string.go
+
+func packString(s string) interface{} {
+ return s
+}
+
+func unpackString(v interface{}) string {
+ if s, ok := v.(string); ok {
+ return s
+ }
+ return ""
+}
+
+// String returns the wrapped value.
+func (s *String) String() string {
+ return s.Load()
+}
+
+// MarshalText encodes the wrapped string into a textual form.
+//
+// This makes it encodable as JSON, YAML, XML, and more.
+func (s *String) MarshalText() ([]byte, error) {
+ return []byte(s.Load()), nil
+}
+
+// UnmarshalText decodes text and replaces the wrapped string with it.
+//
+// This makes it decodable from JSON, YAML, XML, and more.
+func (s *String) UnmarshalText(b []byte) error {
+ s.Store(string(b))
+ return nil
+}
diff --git a/backend/vendor/go.uber.org/atomic/time.go b/backend/vendor/go.uber.org/atomic/time.go
new file mode 100644
index 00000000..cc2a230c
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/time.go
@@ -0,0 +1,55 @@
+// @generated Code generated by gen-atomicwrapper.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "time"
+)
+
+// Time is an atomic type-safe wrapper for time.Time values.
+type Time struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v Value
+}
+
+var _zeroTime time.Time
+
+// NewTime creates a new Time.
+func NewTime(val time.Time) *Time {
+ x := &Time{}
+ if val != _zeroTime {
+ x.Store(val)
+ }
+ return x
+}
+
+// Load atomically loads the wrapped time.Time.
+func (x *Time) Load() time.Time {
+ return unpackTime(x.v.Load())
+}
+
+// Store atomically stores the passed time.Time.
+func (x *Time) Store(val time.Time) {
+ x.v.Store(packTime(val))
+}
diff --git a/backend/vendor/go.uber.org/atomic/time_ext.go b/backend/vendor/go.uber.org/atomic/time_ext.go
new file mode 100644
index 00000000..1e3dc978
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/time_ext.go
@@ -0,0 +1,36 @@
+// Copyright (c) 2021 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import "time"
+
+//go:generate bin/gen-atomicwrapper -name=Time -type=time.Time -wrapped=Value -pack=packTime -unpack=unpackTime -imports time -file=time.go
+
+func packTime(t time.Time) interface{} {
+ return t
+}
+
+func unpackTime(v interface{}) time.Time {
+ if t, ok := v.(time.Time); ok {
+ return t
+ }
+ return time.Time{}
+}
diff --git a/backend/vendor/go.uber.org/atomic/uint32.go b/backend/vendor/go.uber.org/atomic/uint32.go
new file mode 100644
index 00000000..4adc294a
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/uint32.go
@@ -0,0 +1,109 @@
+// @generated Code generated by gen-atomicint.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+ "strconv"
+ "sync/atomic"
+)
+
+// Uint32 is an atomic wrapper around uint32.
+type Uint32 struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v uint32
+}
+
+// NewUint32 creates a new Uint32.
+func NewUint32(val uint32) *Uint32 {
+ return &Uint32{v: val}
+}
+
+// Load atomically loads the wrapped value.
+func (i *Uint32) Load() uint32 {
+ return atomic.LoadUint32(&i.v)
+}
+
+// Add atomically adds to the wrapped uint32 and returns the new value.
+func (i *Uint32) Add(delta uint32) uint32 {
+ return atomic.AddUint32(&i.v, delta)
+}
+
+// Sub atomically subtracts from the wrapped uint32 and returns the new value.
+func (i *Uint32) Sub(delta uint32) uint32 {
+ return atomic.AddUint32(&i.v, ^(delta - 1))
+}
+
+// Inc atomically increments the wrapped uint32 and returns the new value.
+func (i *Uint32) Inc() uint32 {
+ return i.Add(1)
+}
+
+// Dec atomically decrements the wrapped uint32 and returns the new value.
+func (i *Uint32) Dec() uint32 {
+ return i.Sub(1)
+}
+
+// CAS is an atomic compare-and-swap.
+//
+// Deprecated: Use CompareAndSwap.
+func (i *Uint32) CAS(old, new uint32) (swapped bool) {
+ return i.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap.
+func (i *Uint32) CompareAndSwap(old, new uint32) (swapped bool) {
+ return atomic.CompareAndSwapUint32(&i.v, old, new)
+}
+
+// Store atomically stores the passed value.
+func (i *Uint32) Store(val uint32) {
+ atomic.StoreUint32(&i.v, val)
+}
+
+// Swap atomically swaps the wrapped uint32 and returns the old value.
+func (i *Uint32) Swap(val uint32) (old uint32) {
+ return atomic.SwapUint32(&i.v, val)
+}
+
+// MarshalJSON encodes the wrapped uint32 into JSON.
+func (i *Uint32) MarshalJSON() ([]byte, error) {
+ return json.Marshal(i.Load())
+}
+
+// UnmarshalJSON decodes JSON into the wrapped uint32.
+func (i *Uint32) UnmarshalJSON(b []byte) error {
+ var v uint32
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ i.Store(v)
+ return nil
+}
+
+// String encodes the wrapped value as a string.
+func (i *Uint32) String() string {
+ v := i.Load()
+ return strconv.FormatUint(uint64(v), 10)
+}
diff --git a/backend/vendor/go.uber.org/atomic/uint64.go b/backend/vendor/go.uber.org/atomic/uint64.go
new file mode 100644
index 00000000..0e2eddb3
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/uint64.go
@@ -0,0 +1,109 @@
+// @generated Code generated by gen-atomicint.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+ "strconv"
+ "sync/atomic"
+)
+
+// Uint64 is an atomic wrapper around uint64.
+type Uint64 struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v uint64
+}
+
+// NewUint64 creates a new Uint64.
+func NewUint64(val uint64) *Uint64 {
+ return &Uint64{v: val}
+}
+
+// Load atomically loads the wrapped value.
+func (i *Uint64) Load() uint64 {
+ return atomic.LoadUint64(&i.v)
+}
+
+// Add atomically adds to the wrapped uint64 and returns the new value.
+func (i *Uint64) Add(delta uint64) uint64 {
+ return atomic.AddUint64(&i.v, delta)
+}
+
+// Sub atomically subtracts from the wrapped uint64 and returns the new value.
+func (i *Uint64) Sub(delta uint64) uint64 {
+ return atomic.AddUint64(&i.v, ^(delta - 1))
+}
+
+// Inc atomically increments the wrapped uint64 and returns the new value.
+func (i *Uint64) Inc() uint64 {
+ return i.Add(1)
+}
+
+// Dec atomically decrements the wrapped uint64 and returns the new value.
+func (i *Uint64) Dec() uint64 {
+ return i.Sub(1)
+}
+
+// CAS is an atomic compare-and-swap.
+//
+// Deprecated: Use CompareAndSwap.
+func (i *Uint64) CAS(old, new uint64) (swapped bool) {
+ return i.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap.
+func (i *Uint64) CompareAndSwap(old, new uint64) (swapped bool) {
+ return atomic.CompareAndSwapUint64(&i.v, old, new)
+}
+
+// Store atomically stores the passed value.
+func (i *Uint64) Store(val uint64) {
+ atomic.StoreUint64(&i.v, val)
+}
+
+// Swap atomically swaps the wrapped uint64 and returns the old value.
+func (i *Uint64) Swap(val uint64) (old uint64) {
+ return atomic.SwapUint64(&i.v, val)
+}
+
+// MarshalJSON encodes the wrapped uint64 into JSON.
+func (i *Uint64) MarshalJSON() ([]byte, error) {
+ return json.Marshal(i.Load())
+}
+
+// UnmarshalJSON decodes JSON into the wrapped uint64.
+func (i *Uint64) UnmarshalJSON(b []byte) error {
+ var v uint64
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ i.Store(v)
+ return nil
+}
+
+// String encodes the wrapped value as a string.
+func (i *Uint64) String() string {
+ v := i.Load()
+ return strconv.FormatUint(uint64(v), 10)
+}
diff --git a/backend/vendor/go.uber.org/atomic/uintptr.go b/backend/vendor/go.uber.org/atomic/uintptr.go
new file mode 100644
index 00000000..7d5b000d
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/uintptr.go
@@ -0,0 +1,109 @@
+// @generated Code generated by gen-atomicint.
+
+// Copyright (c) 2020-2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "encoding/json"
+ "strconv"
+ "sync/atomic"
+)
+
+// Uintptr is an atomic wrapper around uintptr.
+type Uintptr struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v uintptr
+}
+
+// NewUintptr creates a new Uintptr.
+func NewUintptr(val uintptr) *Uintptr {
+ return &Uintptr{v: val}
+}
+
+// Load atomically loads the wrapped value.
+func (i *Uintptr) Load() uintptr {
+ return atomic.LoadUintptr(&i.v)
+}
+
+// Add atomically adds to the wrapped uintptr and returns the new value.
+func (i *Uintptr) Add(delta uintptr) uintptr {
+ return atomic.AddUintptr(&i.v, delta)
+}
+
+// Sub atomically subtracts from the wrapped uintptr and returns the new value.
+func (i *Uintptr) Sub(delta uintptr) uintptr {
+ return atomic.AddUintptr(&i.v, ^(delta - 1))
+}
+
+// Inc atomically increments the wrapped uintptr and returns the new value.
+func (i *Uintptr) Inc() uintptr {
+ return i.Add(1)
+}
+
+// Dec atomically decrements the wrapped uintptr and returns the new value.
+func (i *Uintptr) Dec() uintptr {
+ return i.Sub(1)
+}
+
+// CAS is an atomic compare-and-swap.
+//
+// Deprecated: Use CompareAndSwap.
+func (i *Uintptr) CAS(old, new uintptr) (swapped bool) {
+ return i.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap.
+func (i *Uintptr) CompareAndSwap(old, new uintptr) (swapped bool) {
+ return atomic.CompareAndSwapUintptr(&i.v, old, new)
+}
+
+// Store atomically stores the passed value.
+func (i *Uintptr) Store(val uintptr) {
+ atomic.StoreUintptr(&i.v, val)
+}
+
+// Swap atomically swaps the wrapped uintptr and returns the old value.
+func (i *Uintptr) Swap(val uintptr) (old uintptr) {
+ return atomic.SwapUintptr(&i.v, val)
+}
+
+// MarshalJSON encodes the wrapped uintptr into JSON.
+func (i *Uintptr) MarshalJSON() ([]byte, error) {
+ return json.Marshal(i.Load())
+}
+
+// UnmarshalJSON decodes JSON into the wrapped uintptr.
+func (i *Uintptr) UnmarshalJSON(b []byte) error {
+ var v uintptr
+ if err := json.Unmarshal(b, &v); err != nil {
+ return err
+ }
+ i.Store(v)
+ return nil
+}
+
+// String encodes the wrapped value as a string.
+func (i *Uintptr) String() string {
+ v := i.Load()
+ return strconv.FormatUint(uint64(v), 10)
+}
diff --git a/backend/vendor/go.uber.org/atomic/unsafe_pointer.go b/backend/vendor/go.uber.org/atomic/unsafe_pointer.go
new file mode 100644
index 00000000..34868baf
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/unsafe_pointer.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2021-2022 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import (
+ "sync/atomic"
+ "unsafe"
+)
+
+// UnsafePointer is an atomic wrapper around unsafe.Pointer.
+type UnsafePointer struct {
+ _ nocmp // disallow non-atomic comparison
+
+ v unsafe.Pointer
+}
+
+// NewUnsafePointer creates a new UnsafePointer.
+func NewUnsafePointer(val unsafe.Pointer) *UnsafePointer {
+ return &UnsafePointer{v: val}
+}
+
+// Load atomically loads the wrapped value.
+func (p *UnsafePointer) Load() unsafe.Pointer {
+ return atomic.LoadPointer(&p.v)
+}
+
+// Store atomically stores the passed value.
+func (p *UnsafePointer) Store(val unsafe.Pointer) {
+ atomic.StorePointer(&p.v, val)
+}
+
+// Swap atomically swaps the wrapped unsafe.Pointer and returns the old value.
+func (p *UnsafePointer) Swap(val unsafe.Pointer) (old unsafe.Pointer) {
+ return atomic.SwapPointer(&p.v, val)
+}
+
+// CAS is an atomic compare-and-swap.
+//
+// Deprecated: Use CompareAndSwap
+func (p *UnsafePointer) CAS(old, new unsafe.Pointer) (swapped bool) {
+ return p.CompareAndSwap(old, new)
+}
+
+// CompareAndSwap is an atomic compare-and-swap.
+func (p *UnsafePointer) CompareAndSwap(old, new unsafe.Pointer) (swapped bool) {
+ return atomic.CompareAndSwapPointer(&p.v, old, new)
+}
diff --git a/backend/vendor/go.uber.org/atomic/value.go b/backend/vendor/go.uber.org/atomic/value.go
new file mode 100644
index 00000000..52caedb9
--- /dev/null
+++ b/backend/vendor/go.uber.org/atomic/value.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2020 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package atomic
+
+import "sync/atomic"
+
+// Value shadows the type of the same name from sync/atomic
+// https://godoc.org/sync/atomic#Value
+type Value struct {
+ _ nocmp // disallow non-atomic comparison
+
+ atomic.Value
+}
diff --git a/backend/vendor/golang.org/x/crypto/acme/autocert/autocert.go b/backend/vendor/golang.org/x/crypto/acme/autocert/autocert.go
index cde9066f..69461e31 100644
--- a/backend/vendor/golang.org/x/crypto/acme/autocert/autocert.go
+++ b/backend/vendor/golang.org/x/crypto/acme/autocert/autocert.go
@@ -248,10 +248,6 @@ func (m *Manager) TLSConfig() *tls.Config {
// If GetCertificate is used directly, instead of via Manager.TLSConfig, package users will
// also have to add acme.ALPNProto to NextProtos for tls-alpn-01, or use HTTPHandler for http-01.
func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
- if m.Prompt == nil {
- return nil, errors.New("acme/autocert: Manager.Prompt not set")
- }
-
name := hello.ServerName
if name == "" {
return nil, errors.New("acme/autocert: missing server name")
diff --git a/backend/vendor/golang.org/x/crypto/acme/rfc8555.go b/backend/vendor/golang.org/x/crypto/acme/rfc8555.go
index 976b2770..1fb110e0 100644
--- a/backend/vendor/golang.org/x/crypto/acme/rfc8555.go
+++ b/backend/vendor/golang.org/x/crypto/acme/rfc8555.go
@@ -53,6 +53,9 @@ func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tos
Contact: acct.Contact,
}
if c.dir.Terms != "" {
+ if prompt == nil {
+ return nil, errors.New("acme: missing Manager.Prompt to accept server's terms of service")
+ }
req.TermsAgreed = prompt(c.dir.Terms)
}
diff --git a/backend/vendor/golang.org/x/net/html/iter.go b/backend/vendor/golang.org/x/net/html/iter.go
index 54be8fd3..349ef73e 100644
--- a/backend/vendor/golang.org/x/net/html/iter.go
+++ b/backend/vendor/golang.org/x/net/html/iter.go
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build go1.23
-
package html
import "iter"
diff --git a/backend/vendor/golang.org/x/net/html/node.go b/backend/vendor/golang.org/x/net/html/node.go
index 77741a19..253e4679 100644
--- a/backend/vendor/golang.org/x/net/html/node.go
+++ b/backend/vendor/golang.org/x/net/html/node.go
@@ -11,6 +11,7 @@ import (
// A NodeType is the type of a Node.
type NodeType uint32
+//go:generate stringer -type NodeType
const (
ErrorNode NodeType = iota
TextNode
diff --git a/backend/vendor/golang.org/x/net/html/nodetype_string.go b/backend/vendor/golang.org/x/net/html/nodetype_string.go
new file mode 100644
index 00000000..8253af49
--- /dev/null
+++ b/backend/vendor/golang.org/x/net/html/nodetype_string.go
@@ -0,0 +1,31 @@
+// Code generated by "stringer -type NodeType"; DO NOT EDIT.
+
+package html
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[ErrorNode-0]
+ _ = x[TextNode-1]
+ _ = x[DocumentNode-2]
+ _ = x[ElementNode-3]
+ _ = x[CommentNode-4]
+ _ = x[DoctypeNode-5]
+ _ = x[RawNode-6]
+ _ = x[scopeMarkerNode-7]
+}
+
+const _NodeType_name = "ErrorNodeTextNodeDocumentNodeElementNodeCommentNodeDoctypeNodeRawNodescopeMarkerNode"
+
+var _NodeType_index = [...]uint8{0, 9, 17, 29, 40, 51, 62, 69, 84}
+
+func (i NodeType) String() string {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_NodeType_index)-1 {
+ return "NodeType(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _NodeType_name[_NodeType_index[idx]:_NodeType_index[idx+1]]
+}
diff --git a/backend/vendor/golang.org/x/oauth2/google/default.go b/backend/vendor/golang.org/x/oauth2/google/default.go
index 0260935b..6e572069 100644
--- a/backend/vendor/golang.org/x/oauth2/google/default.go
+++ b/backend/vendor/golang.org/x/oauth2/google/default.go
@@ -153,6 +153,43 @@ func (params CredentialsParams) deepCopy() CredentialsParams {
return paramsCopy
}
+// CredentialsType specifies the type of JSON credentials being provided
+// to a loading function.
+type CredentialsType string
+
+const (
+ // ServiceAccount represents a service account file type.
+ ServiceAccount CredentialsType = "service_account"
+ // AuthorizedUser represents a user credentials file type.
+ AuthorizedUser CredentialsType = "authorized_user"
+ // ExternalAccount represents an external account file type.
+ //
+ // IMPORTANT:
+ // This credential type does not validate the credential configuration. A security
+ // risk occurs when a credential configuration configured with malicious urls
+ // is used.
+ // You should validate credential configurations provided by untrusted sources.
+ // See [Security requirements when using credential configurations from an external
+ // source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
+ // for more details.
+ ExternalAccount CredentialsType = "external_account"
+ // ExternalAccountAuthorizedUser represents an external account authorized user file type.
+ ExternalAccountAuthorizedUser CredentialsType = "external_account_authorized_user"
+ // ImpersonatedServiceAccount represents an impersonated service account file type.
+ //
+ // IMPORTANT:
+ // This credential type does not validate the credential configuration. A security
+ // risk occurs when a credential configuration configured with malicious urls
+ // is used.
+ // You should validate credential configurations provided by untrusted sources.
+ // See [Security requirements when using credential configurations from an external
+ // source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
+ // for more details.
+ ImpersonatedServiceAccount CredentialsType = "impersonated_service_account"
+ // GDCHServiceAccount represents a GDCH service account credentials.
+ GDCHServiceAccount CredentialsType = "gdch_service_account"
+)
+
// DefaultClient returns an HTTP Client that uses the
// DefaultTokenSource to obtain authentication credentials.
func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) {
@@ -246,17 +283,71 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials
return FindDefaultCredentialsWithParams(ctx, params)
}
-// CredentialsFromJSONWithParams obtains Google credentials from a JSON value. The JSON can
-// represent either a Google Developers Console client_credentials.json file (as in ConfigFromJSON),
-// a Google Developers service account key file, a gcloud user credentials file (a.k.a. refresh
-// token JSON), or the JSON configuration file for workload identity federation in non-Google cloud
-// platforms (see https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation).
+// CredentialsFromJSONWithType invokes CredentialsFromJSONWithTypeAndParams with the specified scopes.
//
// Important: If you accept a credential configuration (credential JSON/File/Stream) from an
// external source for authentication to Google Cloud Platform, you must validate it before
// providing it to any Google API or library. Providing an unvalidated credential configuration to
// Google APIs can compromise the security of your systems and data. For more information, refer to
// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
+func CredentialsFromJSONWithType(ctx context.Context, jsonData []byte, credType CredentialsType, scopes ...string) (*Credentials, error) {
+ var params CredentialsParams
+ params.Scopes = scopes
+ return CredentialsFromJSONWithTypeAndParams(ctx, jsonData, credType, params)
+}
+
+// CredentialsFromJSONWithTypeAndParams obtains Google credentials from a JSON value and
+// validates that the credentials match the specified type.
+//
+// Important: If you accept a credential configuration (credential JSON/File/Stream) from an
+// external source for authentication to Google Cloud Platform, you must validate it before
+// providing it to any Google API or library. Providing an unvalidated credential configuration to
+// Google APIs can compromise the security of your systems and data. For more information, refer to
+// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
+func CredentialsFromJSONWithTypeAndParams(ctx context.Context, jsonData []byte, credType CredentialsType, params CredentialsParams) (*Credentials, error) {
+ var f struct {
+ Type string `json:"type"`
+ }
+ if err := json.Unmarshal(jsonData, &f); err != nil {
+ return nil, err
+ }
+ if CredentialsType(f.Type) != credType {
+ return nil, fmt.Errorf("google: expected credential type %q, found %q", credType, f.Type)
+ }
+ return CredentialsFromJSONWithParams(ctx, jsonData, params)
+}
+
+// CredentialsFromJSONWithParams obtains Google credentials from a JSON value. The JSON can
+// represent either a Google Developers Console client_credentials.json file (as in ConfigFromJSON),
+// a Google Developers service account key file, a gcloud user credentials file (a.k.a. refresh
+// token JSON), or the JSON configuration file for workload identity federation in non-Google cloud
+// platforms (see https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation).
+//
+// Deprecated: This function is deprecated because of a potential security risk.
+// It does not validate the credential configuration. The security risk occurs
+// when a credential configuration is accepted from a source that is not
+// under your control and used without validation on your side.
+//
+// If you know that you will be loading credential configurations of a
+// specific type, it is recommended to use a credential-type-specific
+// CredentialsFromJSONWithTypeAndParams method. This will ensure that an unexpected
+// credential type with potential for malicious intent is not loaded
+// unintentionally. You might still have to do validation for certain
+// credential types. Please follow the recommendation for that method. For
+// example, if you want to load only service accounts, you can use
+//
+// creds, err := google.CredentialsFromJSONWithTypeAndParams(ctx, jsonData, google.ServiceAccount, params)
+//
+// If you are loading your credential configuration from an untrusted source
+// and have not mitigated the risks (e.g. by validating the configuration
+// yourself), make these changes as soon as possible to prevent security
+// risks to your environment.
+//
+// Regardless of the method used, it is always your responsibility to
+// validate configurations received from external sources.
+//
+// For more details see:
+// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) {
// Make defensive copy of the slices in params.
params = params.deepCopy()
@@ -301,11 +392,31 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params
// CredentialsFromJSON invokes CredentialsFromJSONWithParams with the specified scopes.
//
-// Important: If you accept a credential configuration (credential JSON/File/Stream) from an
-// external source for authentication to Google Cloud Platform, you must validate it before
-// providing it to any Google API or library. Providing an unvalidated credential configuration to
-// Google APIs can compromise the security of your systems and data. For more information, refer to
-// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
+// Deprecated: This function is deprecated because of a potential security risk.
+// It does not validate the credential configuration. The security risk occurs
+// when a credential configuration is accepted from a source that is not
+// under your control and used without validation on your side.
+//
+// If you know that you will be loading credential configurations of a
+// specific type, it is recommended to use a credential-type-specific
+// CredentialsFromJSONWithType method. This will ensure that an unexpected
+// credential type with potential for malicious intent is not loaded
+// unintentionally. You might still have to do validation for certain
+// credential types. Please follow the recommendation for that method. For
+// example, if you want to load only service accounts, you can use
+//
+// creds, err := google.CredentialsFromJSONWithType(ctx, jsonData, google.ServiceAccount, scopes...)
+//
+// If you are loading your credential configuration from an untrusted source
+// and have not mitigated the risks (e.g. by validating the configuration
+// yourself), make these changes as soon as possible to prevent security
+// risks to your environment.
+//
+// Regardless of the method used, it is always your responsibility to
+// validate configurations received from external sources.
+//
+// For more details see:
+// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) {
var params CredentialsParams
params.Scopes = scopes
diff --git a/backend/vendor/golang.org/x/oauth2/google/google.go b/backend/vendor/golang.org/x/oauth2/google/google.go
index 7d1fdd31..14c98eb6 100644
--- a/backend/vendor/golang.org/x/oauth2/google/google.go
+++ b/backend/vendor/golang.org/x/oauth2/google/google.go
@@ -103,6 +103,7 @@ const (
externalAccountKey = "external_account"
externalAccountAuthorizedUserKey = "external_account_authorized_user"
impersonatedServiceAccount = "impersonated_service_account"
+ gdchServiceAccountKey = "gdch_service_account"
)
// credentialsFile is the unmarshalled representation of a credentials file.
@@ -165,7 +166,7 @@ func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config
func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) {
switch f.Type {
- case serviceAccountKey:
+ case serviceAccountKey, gdchServiceAccountKey:
cfg := f.jwtConfig(params.Scopes, params.Subject)
return cfg.TokenSource(ctx), nil
case userCredentialsKey:
diff --git a/backend/vendor/golang.org/x/sync/singleflight/singleflight.go b/backend/vendor/golang.org/x/sync/singleflight/singleflight.go
index 40518309..90ca138a 100644
--- a/backend/vendor/golang.org/x/sync/singleflight/singleflight.go
+++ b/backend/vendor/golang.org/x/sync/singleflight/singleflight.go
@@ -22,7 +22,7 @@ var errGoexit = errors.New("runtime.Goexit was called")
// A panicError is an arbitrary value recovered from a panic
// with the stack trace during the execution of given function.
type panicError struct {
- value interface{}
+ value any
stack []byte
}
@@ -40,7 +40,7 @@ func (p *panicError) Unwrap() error {
return err
}
-func newPanicError(v interface{}) error {
+func newPanicError(v any) error {
stack := debug.Stack()
// The first line of the stack trace is of the form "goroutine N [status]:"
@@ -58,7 +58,7 @@ type call struct {
// These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
- val interface{}
+ val any
err error
// These fields are read and written with the singleflight
@@ -78,7 +78,7 @@ type Group struct {
// Result holds the results of Do, so they can be passed
// on a channel.
type Result struct {
- Val interface{}
+ Val any
Err error
Shared bool
}
@@ -88,7 +88,7 @@ type Result struct {
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
-func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
+func (g *Group) Do(key string, fn func() (any, error)) (v any, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
@@ -118,7 +118,7 @@ func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, e
// results when they are ready.
//
// The returned channel will not be closed.
-func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
+func (g *Group) DoChan(key string, fn func() (any, error)) <-chan Result {
ch := make(chan Result, 1)
g.mu.Lock()
if g.m == nil {
@@ -141,7 +141,7 @@ func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result
}
// doCall handles the single call for a key.
-func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
+func (g *Group) doCall(c *call, key string, fn func() (any, error)) {
normalReturn := false
recovered := false
diff --git a/backend/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s b/backend/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s
new file mode 100644
index 00000000..e07fa75e
--- /dev/null
+++ b/backend/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s
@@ -0,0 +1,12 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build darwin && arm64 && gc
+
+#include "textflag.h"
+
+TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0
+ JMP libc_sysctlbyname(SB)
+GLOBL ·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8
+DATA ·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB)
diff --git a/backend/vendor/golang.org/x/sys/cpu/cpu_arm64.go b/backend/vendor/golang.org/x/sys/cpu/cpu_arm64.go
index 6d8eb784..5fc09e29 100644
--- a/backend/vendor/golang.org/x/sys/cpu/cpu_arm64.go
+++ b/backend/vendor/golang.org/x/sys/cpu/cpu_arm64.go
@@ -44,14 +44,11 @@ func initOptions() {
}
func archInit() {
- switch runtime.GOOS {
- case "freebsd":
+ if runtime.GOOS == "freebsd" {
readARM64Registers()
- case "linux", "netbsd", "openbsd", "windows":
+ } else {
+ // Most platforms don't seem to allow directly reading these registers.
doinit()
- default:
- // Many platforms don't seem to allow reading these registers.
- setMinimalFeatures()
}
}
diff --git a/backend/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go b/backend/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go
new file mode 100644
index 00000000..0b470744
--- /dev/null
+++ b/backend/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go
@@ -0,0 +1,67 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build darwin && arm64 && gc
+
+package cpu
+
+func doinit() {
+ setMinimalFeatures()
+
+ // The feature flags are explained in [Instruction Set Detection].
+ // There are some differences between MacOS versions:
+ //
+ // MacOS 11 and 12 do not have "hw.optional" sysctl values for some of the features.
+ //
+ // MacOS 13 changed some of the naming conventions to align with ARM Architecture Reference Manual.
+ // For example "hw.optional.armv8_2_sha512" became "hw.optional.arm.FEAT_SHA512".
+ // It currently checks both to stay compatible with MacOS 11 and 12.
+ // The old names also work with MacOS 13, however it's not clear whether
+ // they will continue working with future OS releases.
+ //
+ // Once MacOS 12 is no longer supported the old names can be removed.
+ //
+ // [Instruction Set Detection]: https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics
+
+ // Encryption, hashing and checksum capabilities
+
+ // For the following flags there are no MacOS 11 sysctl flags.
+ ARM64.HasAES = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_AES\x00"))
+ ARM64.HasPMULL = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_PMULL\x00"))
+ ARM64.HasSHA1 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA1\x00"))
+ ARM64.HasSHA2 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA256\x00"))
+
+ ARM64.HasSHA3 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha3\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA3\x00"))
+ ARM64.HasSHA512 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha512\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA512\x00"))
+
+ ARM64.HasCRC32 = darwinSysctlEnabled([]byte("hw.optional.armv8_crc32\x00"))
+
+ // Atomic and memory ordering
+ ARM64.HasATOMICS = darwinSysctlEnabled([]byte("hw.optional.armv8_1_atomics\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LSE\x00"))
+ ARM64.HasLRCPC = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LRCPC\x00"))
+
+ // SIMD and floating point capabilities
+ ARM64.HasFPHP = darwinSysctlEnabled([]byte("hw.optional.neon_fp16\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FP16\x00"))
+ ARM64.HasASIMDHP = darwinSysctlEnabled([]byte("hw.optional.neon_hpfp\x00")) || darwinSysctlEnabled([]byte("hw.optional.AdvSIMD_HPFPCvt\x00"))
+ ARM64.HasASIMDRDM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_RDM\x00"))
+ ARM64.HasASIMDDP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DotProd\x00"))
+ ARM64.HasASIMDFHM = darwinSysctlEnabled([]byte("hw.optional.armv8_2_fhm\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FHM\x00"))
+ ARM64.HasI8MM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_I8MM\x00"))
+
+ ARM64.HasJSCVT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_JSCVT\x00"))
+ ARM64.HasFCMA = darwinSysctlEnabled([]byte("hw.optional.armv8_3_compnum\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FCMA\x00"))
+
+ // Miscellaneous
+ ARM64.HasDCPOP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DPB\x00"))
+ ARM64.HasEVTSTRM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_ECV\x00"))
+ ARM64.HasDIT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DIT\x00"))
+
+ // Not supported, but added for completeness
+ ARM64.HasCPUID = false
+
+ ARM64.HasSM3 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM3\x00"))
+ ARM64.HasSM4 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM4\x00"))
+ ARM64.HasSVE = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE\x00"))
+ ARM64.HasSVE2 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE2\x00"))
+}
diff --git a/backend/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go b/backend/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go
new file mode 100644
index 00000000..37ecc664
--- /dev/null
+++ b/backend/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go
@@ -0,0 +1,31 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build darwin && arm64 && !gc
+
+package cpu
+
+import "runtime"
+
+func doinit() {
+ setMinimalFeatures()
+
+ ARM64.HasASIMD = true
+ ARM64.HasFP = true
+
+ // Go already assumes these to be available because they were on the M1
+ // and these are supported on all Apple arm64 chips.
+ ARM64.HasAES = true
+ ARM64.HasPMULL = true
+ ARM64.HasSHA1 = true
+ ARM64.HasSHA2 = true
+
+ if runtime.GOOS != "ios" {
+ // Apple A7 processors do not support these, however
+ // M-series SoCs are at least armv8.4-a
+ ARM64.HasCRC32 = true // armv8.1
+ ARM64.HasATOMICS = true // armv8.2
+ ARM64.HasJSCVT = true // armv8.3, if HasFP
+ }
+}
diff --git a/backend/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go b/backend/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go
index 7f194678..05913081 100644
--- a/backend/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go
+++ b/backend/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go
@@ -9,3 +9,4 @@ package cpu
func getisar0() uint64 { return 0 }
func getisar1() uint64 { return 0 }
func getpfr0() uint64 { return 0 }
+func getzfr0() uint64 { return 0 }
diff --git a/backend/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go b/backend/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go
index ff74d7af..53f814d7 100644
--- a/backend/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go
+++ b/backend/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go
@@ -2,8 +2,10 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build !linux && !netbsd && !openbsd && !windows && arm64
+//go:build !darwin && !linux && !netbsd && !openbsd && arm64
package cpu
-func doinit() {}
+func doinit() {
+ setMinimalFeatures()
+}
diff --git a/backend/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go b/backend/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go
deleted file mode 100644
index d09e85a3..00000000
--- a/backend/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2026 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package cpu
-
-import (
- "golang.org/x/sys/windows"
-)
-
-func doinit() {
- // set HasASIMD and HasFP to true as per
- // https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170#base-requirements
- //
- // The ARM64 version of Windows always presupposes that it's running on an ARMv8 or later architecture.
- // Both floating-point and NEON support are presumed to be present in hardware.
- //
- ARM64.HasASIMD = true
- ARM64.HasFP = true
-
- if windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE) {
- ARM64.HasAES = true
- ARM64.HasPMULL = true
- ARM64.HasSHA1 = true
- ARM64.HasSHA2 = true
- }
- ARM64.HasSHA3 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE)
- ARM64.HasCRC32 = windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE)
- ARM64.HasSHA512 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE)
- ARM64.HasATOMICS = windows.IsProcessorFeaturePresent(windows.PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE)
- if windows.IsProcessorFeaturePresent(windows.PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE) {
- ARM64.HasASIMDDP = true
- ARM64.HasASIMDRDM = true
- }
- if windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE) {
- ARM64.HasLRCPC = true
- ARM64.HasSM3 = true
- }
- ARM64.HasSVE = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE_INSTRUCTIONS_AVAILABLE)
- ARM64.HasSVE2 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE)
- ARM64.HasJSCVT = windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE)
-}
diff --git a/backend/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go b/backend/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go
new file mode 100644
index 00000000..7b4e67ff
--- /dev/null
+++ b/backend/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go
@@ -0,0 +1,54 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Minimal copy from internal/cpu and runtime to make sysctl calls.
+
+//go:build darwin && arm64 && gc
+
+package cpu
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+type Errno = syscall.Errno
+
+// adapted from internal/cpu/cpu_arm64_darwin.go
+func darwinSysctlEnabled(name []byte) bool {
+ out := int32(0)
+ nout := unsafe.Sizeof(out)
+ if ret := sysctlbyname(&name[0], (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); ret != nil {
+ return false
+ }
+ return out > 0
+}
+
+//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib"
+
+var libc_sysctlbyname_trampoline_addr uintptr
+
+// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix
+func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error {
+ if _, _, err := syscall_syscall6(
+ libc_sysctlbyname_trampoline_addr,
+ uintptr(unsafe.Pointer(name)),
+ uintptr(unsafe.Pointer(old)),
+ uintptr(unsafe.Pointer(oldlen)),
+ uintptr(unsafe.Pointer(new)),
+ uintptr(newlen),
+ 0,
+ ); err != 0 {
+ return err
+ }
+
+ return nil
+}
+
+//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib"
+
+// Implemented in the runtime package (runtime/sys_darwin.go)
+func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
+
+//go:linkname syscall_syscall6 syscall.syscall6
diff --git a/backend/vendor/golang.org/x/sys/unix/ztypes_linux.go b/backend/vendor/golang.org/x/sys/unix/ztypes_linux.go
index c1a46701..45476a73 100644
--- a/backend/vendor/golang.org/x/sys/unix/ztypes_linux.go
+++ b/backend/vendor/golang.org/x/sys/unix/ztypes_linux.go
@@ -593,110 +593,115 @@ const (
)
const (
- NDA_UNSPEC = 0x0
- NDA_DST = 0x1
- NDA_LLADDR = 0x2
- NDA_CACHEINFO = 0x3
- NDA_PROBES = 0x4
- NDA_VLAN = 0x5
- NDA_PORT = 0x6
- NDA_VNI = 0x7
- NDA_IFINDEX = 0x8
- NDA_MASTER = 0x9
- NDA_LINK_NETNSID = 0xa
- NDA_SRC_VNI = 0xb
- NTF_USE = 0x1
- NTF_SELF = 0x2
- NTF_MASTER = 0x4
- NTF_PROXY = 0x8
- NTF_EXT_LEARNED = 0x10
- NTF_OFFLOADED = 0x20
- NTF_ROUTER = 0x80
- NUD_INCOMPLETE = 0x1
- NUD_REACHABLE = 0x2
- NUD_STALE = 0x4
- NUD_DELAY = 0x8
- NUD_PROBE = 0x10
- NUD_FAILED = 0x20
- NUD_NOARP = 0x40
- NUD_PERMANENT = 0x80
- NUD_NONE = 0x0
- IFA_UNSPEC = 0x0
- IFA_ADDRESS = 0x1
- IFA_LOCAL = 0x2
- IFA_LABEL = 0x3
- IFA_BROADCAST = 0x4
- IFA_ANYCAST = 0x5
- IFA_CACHEINFO = 0x6
- IFA_MULTICAST = 0x7
- IFA_FLAGS = 0x8
- IFA_RT_PRIORITY = 0x9
- IFA_TARGET_NETNSID = 0xa
- IFAL_LABEL = 0x2
- IFAL_ADDRESS = 0x1
- RT_SCOPE_UNIVERSE = 0x0
- RT_SCOPE_SITE = 0xc8
- RT_SCOPE_LINK = 0xfd
- RT_SCOPE_HOST = 0xfe
- RT_SCOPE_NOWHERE = 0xff
- RT_TABLE_UNSPEC = 0x0
- RT_TABLE_COMPAT = 0xfc
- RT_TABLE_DEFAULT = 0xfd
- RT_TABLE_MAIN = 0xfe
- RT_TABLE_LOCAL = 0xff
- RT_TABLE_MAX = 0xffffffff
- RTA_UNSPEC = 0x0
- RTA_DST = 0x1
- RTA_SRC = 0x2
- RTA_IIF = 0x3
- RTA_OIF = 0x4
- RTA_GATEWAY = 0x5
- RTA_PRIORITY = 0x6
- RTA_PREFSRC = 0x7
- RTA_METRICS = 0x8
- RTA_MULTIPATH = 0x9
- RTA_FLOW = 0xb
- RTA_CACHEINFO = 0xc
- RTA_TABLE = 0xf
- RTA_MARK = 0x10
- RTA_MFC_STATS = 0x11
- RTA_VIA = 0x12
- RTA_NEWDST = 0x13
- RTA_PREF = 0x14
- RTA_ENCAP_TYPE = 0x15
- RTA_ENCAP = 0x16
- RTA_EXPIRES = 0x17
- RTA_PAD = 0x18
- RTA_UID = 0x19
- RTA_TTL_PROPAGATE = 0x1a
- RTA_IP_PROTO = 0x1b
- RTA_SPORT = 0x1c
- RTA_DPORT = 0x1d
- RTN_UNSPEC = 0x0
- RTN_UNICAST = 0x1
- RTN_LOCAL = 0x2
- RTN_BROADCAST = 0x3
- RTN_ANYCAST = 0x4
- RTN_MULTICAST = 0x5
- RTN_BLACKHOLE = 0x6
- RTN_UNREACHABLE = 0x7
- RTN_PROHIBIT = 0x8
- RTN_THROW = 0x9
- RTN_NAT = 0xa
- RTN_XRESOLVE = 0xb
- SizeofNlMsghdr = 0x10
- SizeofNlMsgerr = 0x14
- SizeofRtGenmsg = 0x1
- SizeofNlAttr = 0x4
- SizeofRtAttr = 0x4
- SizeofIfInfomsg = 0x10
- SizeofIfAddrmsg = 0x8
- SizeofIfAddrlblmsg = 0xc
- SizeofIfaCacheinfo = 0x10
- SizeofRtMsg = 0xc
- SizeofRtNexthop = 0x8
- SizeofNdUseroptmsg = 0x10
- SizeofNdMsg = 0xc
+ NDA_UNSPEC = 0x0
+ NDA_DST = 0x1
+ NDA_LLADDR = 0x2
+ NDA_CACHEINFO = 0x3
+ NDA_PROBES = 0x4
+ NDA_VLAN = 0x5
+ NDA_PORT = 0x6
+ NDA_VNI = 0x7
+ NDA_IFINDEX = 0x8
+ NDA_MASTER = 0x9
+ NDA_LINK_NETNSID = 0xa
+ NDA_SRC_VNI = 0xb
+ NTF_USE = 0x1
+ NTF_SELF = 0x2
+ NTF_MASTER = 0x4
+ NTF_PROXY = 0x8
+ NTF_EXT_LEARNED = 0x10
+ NTF_OFFLOADED = 0x20
+ NTF_ROUTER = 0x80
+ NUD_INCOMPLETE = 0x1
+ NUD_REACHABLE = 0x2
+ NUD_STALE = 0x4
+ NUD_DELAY = 0x8
+ NUD_PROBE = 0x10
+ NUD_FAILED = 0x20
+ NUD_NOARP = 0x40
+ NUD_PERMANENT = 0x80
+ NUD_NONE = 0x0
+ IFA_UNSPEC = 0x0
+ IFA_ADDRESS = 0x1
+ IFA_LOCAL = 0x2
+ IFA_LABEL = 0x3
+ IFA_BROADCAST = 0x4
+ IFA_ANYCAST = 0x5
+ IFA_CACHEINFO = 0x6
+ IFA_MULTICAST = 0x7
+ IFA_FLAGS = 0x8
+ IFA_RT_PRIORITY = 0x9
+ IFA_TARGET_NETNSID = 0xa
+ IFAL_LABEL = 0x2
+ IFAL_ADDRESS = 0x1
+ RT_SCOPE_UNIVERSE = 0x0
+ RT_SCOPE_SITE = 0xc8
+ RT_SCOPE_LINK = 0xfd
+ RT_SCOPE_HOST = 0xfe
+ RT_SCOPE_NOWHERE = 0xff
+ RT_TABLE_UNSPEC = 0x0
+ RT_TABLE_COMPAT = 0xfc
+ RT_TABLE_DEFAULT = 0xfd
+ RT_TABLE_MAIN = 0xfe
+ RT_TABLE_LOCAL = 0xff
+ RT_TABLE_MAX = 0xffffffff
+ RTA_UNSPEC = 0x0
+ RTA_DST = 0x1
+ RTA_SRC = 0x2
+ RTA_IIF = 0x3
+ RTA_OIF = 0x4
+ RTA_GATEWAY = 0x5
+ RTA_PRIORITY = 0x6
+ RTA_PREFSRC = 0x7
+ RTA_METRICS = 0x8
+ RTA_MULTIPATH = 0x9
+ RTA_FLOW = 0xb
+ RTA_CACHEINFO = 0xc
+ RTA_TABLE = 0xf
+ RTA_MARK = 0x10
+ RTA_MFC_STATS = 0x11
+ RTA_VIA = 0x12
+ RTA_NEWDST = 0x13
+ RTA_PREF = 0x14
+ RTA_ENCAP_TYPE = 0x15
+ RTA_ENCAP = 0x16
+ RTA_EXPIRES = 0x17
+ RTA_PAD = 0x18
+ RTA_UID = 0x19
+ RTA_TTL_PROPAGATE = 0x1a
+ RTA_IP_PROTO = 0x1b
+ RTA_SPORT = 0x1c
+ RTA_DPORT = 0x1d
+ RTN_UNSPEC = 0x0
+ RTN_UNICAST = 0x1
+ RTN_LOCAL = 0x2
+ RTN_BROADCAST = 0x3
+ RTN_ANYCAST = 0x4
+ RTN_MULTICAST = 0x5
+ RTN_BLACKHOLE = 0x6
+ RTN_UNREACHABLE = 0x7
+ RTN_PROHIBIT = 0x8
+ RTN_THROW = 0x9
+ RTN_NAT = 0xa
+ RTN_XRESOLVE = 0xb
+ PREFIX_UNSPEC = 0x0
+ PREFIX_ADDRESS = 0x1
+ PREFIX_CACHEINFO = 0x2
+ SizeofNlMsghdr = 0x10
+ SizeofNlMsgerr = 0x14
+ SizeofRtGenmsg = 0x1
+ SizeofNlAttr = 0x4
+ SizeofRtAttr = 0x4
+ SizeofIfInfomsg = 0x10
+ SizeofPrefixmsg = 0xc
+ SizeofPrefixCacheinfo = 0x8
+ SizeofIfAddrmsg = 0x8
+ SizeofIfAddrlblmsg = 0xc
+ SizeofIfaCacheinfo = 0x10
+ SizeofRtMsg = 0xc
+ SizeofRtNexthop = 0x8
+ SizeofNdUseroptmsg = 0x10
+ SizeofNdMsg = 0xc
)
type NlMsghdr struct {
@@ -735,6 +740,22 @@ type IfInfomsg struct {
Change uint32
}
+type Prefixmsg struct {
+ Family uint8
+ Pad1 uint8
+ Pad2 uint16
+ Ifindex int32
+ Type uint8
+ Len uint8
+ Flags uint8
+ Pad3 uint8
+}
+
+type PrefixCacheinfo struct {
+ Preferred_time uint32
+ Valid_time uint32
+}
+
type IfAddrmsg struct {
Family uint8
Prefixlen uint8
diff --git a/backend/vendor/golang.org/x/sys/windows/aliases.go b/backend/vendor/golang.org/x/sys/windows/aliases.go
index 16f90560..96317966 100644
--- a/backend/vendor/golang.org/x/sys/windows/aliases.go
+++ b/backend/vendor/golang.org/x/sys/windows/aliases.go
@@ -8,5 +8,6 @@ package windows
import "syscall"
+type Signal = syscall.Signal
type Errno = syscall.Errno
type SysProcAttr = syscall.SysProcAttr
diff --git a/backend/vendor/golang.org/x/sys/windows/dll_windows.go b/backend/vendor/golang.org/x/sys/windows/dll_windows.go
index 3ca814f5..1157b06d 100644
--- a/backend/vendor/golang.org/x/sys/windows/dll_windows.go
+++ b/backend/vendor/golang.org/x/sys/windows/dll_windows.go
@@ -163,42 +163,7 @@ func (p *Proc) Addr() uintptr {
// (according to the semantics of the specific function being called) before consulting
// the error. The error will be guaranteed to contain windows.Errno.
func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) {
- switch len(a) {
- case 0:
- return syscall.Syscall(p.Addr(), uintptr(len(a)), 0, 0, 0)
- case 1:
- return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], 0, 0)
- case 2:
- return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], 0)
- case 3:
- return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], a[2])
- case 4:
- return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], 0, 0)
- case 5:
- return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], 0)
- case 6:
- return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5])
- case 7:
- return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], 0, 0)
- case 8:
- return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], 0)
- case 9:
- return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8])
- case 10:
- return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], 0, 0)
- case 11:
- return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], 0)
- case 12:
- return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11])
- case 13:
- return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], 0, 0)
- case 14:
- return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], 0)
- case 15:
- return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14])
- default:
- panic("Call " + p.Name + " with too many arguments " + itoa(len(a)) + ".")
- }
+ return syscall.SyscallN(p.Addr(), a...)
}
// A LazyDLL implements access to a single DLL.
diff --git a/backend/vendor/golang.org/x/sys/windows/registry/key.go b/backend/vendor/golang.org/x/sys/windows/registry/key.go
index 39aeeb64..7cc6ff3a 100644
--- a/backend/vendor/golang.org/x/sys/windows/registry/key.go
+++ b/backend/vendor/golang.org/x/sys/windows/registry/key.go
@@ -198,7 +198,20 @@ type KeyInfo struct {
// ModTime returns the key's last write time.
func (ki *KeyInfo) ModTime() time.Time {
- return time.Unix(0, ki.lastWriteTime.Nanoseconds())
+ lastHigh, lastLow := ki.lastWriteTime.HighDateTime, ki.lastWriteTime.LowDateTime
+ // 100-nanosecond intervals since January 1, 1601
+ hsec := uint64(lastHigh)<<32 + uint64(lastLow)
+ // Convert _before_ gauging; the nanosecond difference between Epoch (00:00:00
+ // UTC, January 1, 1970) and Filetime's zero offset (January 1, 1601) is out
+ // of bounds for int64: -11644473600*1e7*1e2 < math.MinInt64
+ sec := int64(hsec/1e7) - 11644473600
+ nsec := int64(hsec%1e7) * 100
+ return time.Unix(sec, nsec)
+}
+
+// modTimeZero reports whether the key's last write time is zero.
+func (ki *KeyInfo) modTimeZero() bool {
+ return ki.lastWriteTime.LowDateTime == 0 && ki.lastWriteTime.HighDateTime == 0
}
// Stat retrieves information about the open key k.
diff --git a/backend/vendor/golang.org/x/sys/windows/security_windows.go b/backend/vendor/golang.org/x/sys/windows/security_windows.go
index a8b0364c..6c955cea 100644
--- a/backend/vendor/golang.org/x/sys/windows/security_windows.go
+++ b/backend/vendor/golang.org/x/sys/windows/security_windows.go
@@ -1438,13 +1438,17 @@ func GetSecurityInfo(handle Handle, objectType SE_OBJECT_TYPE, securityInformati
}
// GetNamedSecurityInfo queries the security information for a given named object and returns the self-relative security
-// descriptor result on the Go heap.
+// descriptor result on the Go heap. The security descriptor might be nil, even when err is nil, if the object exists
+// but has no security descriptor.
func GetNamedSecurityInfo(objectName string, objectType SE_OBJECT_TYPE, securityInformation SECURITY_INFORMATION) (sd *SECURITY_DESCRIPTOR, err error) {
var winHeapSD *SECURITY_DESCRIPTOR
err = getNamedSecurityInfo(objectName, objectType, securityInformation, nil, nil, nil, nil, &winHeapSD)
if err != nil {
return
}
+ if winHeapSD == nil {
+ return nil, nil
+ }
defer LocalFree(Handle(unsafe.Pointer(winHeapSD)))
return winHeapSD.copySelfRelativeSecurityDescriptor(), nil
}
diff --git a/backend/vendor/golang.org/x/sys/windows/syscall_windows.go b/backend/vendor/golang.org/x/sys/windows/syscall_windows.go
index 738a9f21..d7664365 100644
--- a/backend/vendor/golang.org/x/sys/windows/syscall_windows.go
+++ b/backend/vendor/golang.org/x/sys/windows/syscall_windows.go
@@ -1490,20 +1490,6 @@ func Getgid() (gid int) { return -1 }
func Getegid() (egid int) { return -1 }
func Getgroups() (gids []int, err error) { return nil, syscall.EWINDOWS }
-type Signal int
-
-func (s Signal) Signal() {}
-
-func (s Signal) String() string {
- if 0 <= s && int(s) < len(signals) {
- str := signals[s]
- if str != "" {
- return str
- }
- }
- return "signal " + itoa(int(s))
-}
-
func LoadCreateSymbolicLink() error {
return procCreateSymbolicLinkW.Find()
}
diff --git a/backend/vendor/modules.txt b/backend/vendor/modules.txt
index e6ca9adc..ed37f4be 100644
--- a/backend/vendor/modules.txt
+++ b/backend/vendor/modules.txt
@@ -4,8 +4,8 @@ cloud.google.com/go/compute/metadata
# github.com/Depado/bfchroma/v2 v2.0.0
## explicit; go 1.18
github.com/Depado/bfchroma/v2
-# github.com/PuerkitoBio/goquery v1.11.0
-## explicit; go 1.24.0
+# github.com/PuerkitoBio/goquery v1.12.0
+## explicit; go 1.25.0
github.com/PuerkitoBio/goquery
# github.com/alecthomas/chroma/v2 v2.23.1
## explicit; go 1.22
@@ -78,7 +78,7 @@ github.com/go-pkgz/jrpc
## explicit; go 1.21
github.com/go-pkgz/lcw/v2
github.com/go-pkgz/lcw/v2/eventbus
-# github.com/go-pkgz/lgr v0.12.1
+# github.com/go-pkgz/lgr v0.12.3
## explicit; go 1.21
github.com/go-pkgz/lgr
# github.com/go-pkgz/notify v1.3.0
@@ -135,8 +135,8 @@ github.com/hashicorp/golang-lru/v2/simplelru
# github.com/jessevdk/go-flags v1.6.1
## explicit; go 1.20
github.com/jessevdk/go-flags
-# github.com/klauspost/compress v1.18.2
-## explicit; go 1.23
+# github.com/klauspost/compress v1.18.5
+## explicit; go 1.24
github.com/klauspost/compress
github.com/klauspost/compress/fse
github.com/klauspost/compress/huff0
@@ -152,14 +152,14 @@ github.com/kyokomi/emoji/v2
## explicit; go 1.19
github.com/microcosm-cc/bluemonday
github.com/microcosm-cc/bluemonday/css
-# github.com/montanaflynn/stats v0.7.1
+# github.com/montanaflynn/stats v0.9.0
## explicit; go 1.13
github.com/montanaflynn/stats
# github.com/pmezard/go-difflib v1.0.0
## explicit
github.com/pmezard/go-difflib/difflib
-# github.com/redis/go-redis/v9 v9.17.2
-## explicit; go 1.18
+# github.com/redis/go-redis/v9 v9.18.0
+## explicit; go 1.21
github.com/redis/go-redis/v9
github.com/redis/go-redis/v9/auth
github.com/redis/go-redis/v9/internal
@@ -168,9 +168,11 @@ github.com/redis/go-redis/v9/internal/hashtag
github.com/redis/go-redis/v9/internal/hscan
github.com/redis/go-redis/v9/internal/interfaces
github.com/redis/go-redis/v9/internal/maintnotifications/logs
+github.com/redis/go-redis/v9/internal/otel
github.com/redis/go-redis/v9/internal/pool
github.com/redis/go-redis/v9/internal/proto
github.com/redis/go-redis/v9/internal/rand
+github.com/redis/go-redis/v9/internal/routing
github.com/redis/go-redis/v9/internal/util
github.com/redis/go-redis/v9/maintnotifications
github.com/redis/go-redis/v9/push
@@ -188,8 +190,8 @@ github.com/russross/blackfriday/v2
github.com/skip2/go-qrcode
github.com/skip2/go-qrcode/bitset
github.com/skip2/go-qrcode/reedsolomon
-# github.com/slack-go/slack v0.17.3
-## explicit; go 1.22
+# github.com/slack-go/slack v0.21.1
+## explicit; go 1.25
github.com/slack-go/slack
github.com/slack-go/slack/internal/backoff
github.com/slack-go/slack/internal/errorsx
@@ -218,7 +220,7 @@ go.etcd.io/bbolt
go.etcd.io/bbolt/errors
go.etcd.io/bbolt/internal/common
go.etcd.io/bbolt/internal/freelist
-# go.mongodb.org/mongo-driver v1.17.6
+# go.mongodb.org/mongo-driver v1.17.9
## explicit; go 1.18
go.mongodb.org/mongo-driver/bson
go.mongodb.org/mongo-driver/bson/bsoncodec
@@ -268,12 +270,15 @@ go.mongodb.org/mongo-driver/x/mongo/driver/operation
go.mongodb.org/mongo-driver/x/mongo/driver/session
go.mongodb.org/mongo-driver/x/mongo/driver/topology
go.mongodb.org/mongo-driver/x/mongo/driver/wiremessage
+# go.uber.org/atomic v1.11.0
+## explicit; go 1.18
+go.uber.org/atomic
# go.uber.org/goleak v1.3.0
## explicit; go 1.20
go.uber.org/goleak
go.uber.org/goleak/internal/stack
-# golang.org/x/crypto v0.48.0
-## explicit; go 1.24.0
+# golang.org/x/crypto v0.50.0
+## explicit; go 1.25.0
golang.org/x/crypto/acme
golang.org/x/crypto/acme/autocert
golang.org/x/crypto/argon2
@@ -283,17 +288,17 @@ golang.org/x/crypto/blowfish
golang.org/x/crypto/ocsp
golang.org/x/crypto/pbkdf2
golang.org/x/crypto/scrypt
-# golang.org/x/image v0.36.0
-## explicit; go 1.24.0
+# golang.org/x/image v0.39.0
+## explicit; go 1.25.0
golang.org/x/image/draw
golang.org/x/image/math/f64
-# golang.org/x/net v0.49.0
-## explicit; go 1.24.0
+# golang.org/x/net v0.53.0
+## explicit; go 1.25.0
golang.org/x/net/html
golang.org/x/net/html/atom
golang.org/x/net/idna
-# golang.org/x/oauth2 v0.34.0
-## explicit; go 1.24.0
+# golang.org/x/oauth2 v0.36.0
+## explicit; go 1.25.0
golang.org/x/oauth2
golang.org/x/oauth2/authhandler
golang.org/x/oauth2/endpoints
@@ -309,18 +314,18 @@ golang.org/x/oauth2/jws
golang.org/x/oauth2/jwt
golang.org/x/oauth2/microsoft
golang.org/x/oauth2/yandex
-# golang.org/x/sync v0.19.0
-## explicit; go 1.24.0
+# golang.org/x/sync v0.20.0
+## explicit; go 1.25.0
golang.org/x/sync/errgroup
golang.org/x/sync/singleflight
-# golang.org/x/sys v0.41.0
-## explicit; go 1.24.0
+# golang.org/x/sys v0.43.0
+## explicit; go 1.25.0
golang.org/x/sys/cpu
golang.org/x/sys/unix
golang.org/x/sys/windows
golang.org/x/sys/windows/registry
-# golang.org/x/text v0.34.0
-## explicit; go 1.24.0
+# golang.org/x/text v0.36.0
+## explicit; go 1.25.0
golang.org/x/text/secure/bidirule
golang.org/x/text/transform
golang.org/x/text/unicode/bidi