feat: expose result subscription HTTP and WebSocket APIs

This commit is contained in:
2026-09-17 19:57:55 +08:00
parent 1c3300cc5b
commit 8485a662b2
38 changed files with 3462 additions and 26 deletions

View File

@@ -0,0 +1,128 @@
package httpserver
import (
"net/http"
"net/url"
"strconv"
"strings"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/platform/events"
platform "git.yuntuo.hk/k2safe/orbit/platform/model"
)
func RegisterEventSubscriptions(server *http.Server, s *events.Service) {
mux := server.Handler.(*http.ServeMux)
mux.HandleFunc("POST /v1/subscriptions", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
var in events.CreateInput
if err := decodeEventBody(r, &in); err != nil {
return nil, 0, err
}
key, err := idempotencyKey(r)
if err != nil {
return nil, 0, err
}
out, err := s.Create(r.Context(), t, key, in)
return out, createdStatus(out.Deduplicated), err
}))
mux.HandleFunc("GET /v1/subscriptions", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
queue, in, err := eventQuery(r, true)
if err != nil {
return nil, 0, err
}
out, err := s.List(r.Context(), t, queue, in)
return out, 200, err
}))
mux.HandleFunc("GET /v1/subscriptions/{id}", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
out, err := s.Get(r.Context(), t, core.ID(r.PathValue("id")))
return out, 200, err
}))
mux.HandleFunc("POST /v1/subscriptions/{command}", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
id, command, ok := strings.Cut(r.PathValue("command"), ":")
if !ok || command != "configure" {
return nil, 0, fault.NotFound
}
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
var in events.ControlInput
if err := decodeEventBody(r, &in); err != nil {
return nil, 0, err
}
key, err := idempotencyKey(r)
if err != nil {
return nil, 0, err
}
out, err := s.Control(r.Context(), t, core.ID(id), key, in)
return out, 200, err
}))
mux.HandleFunc("GET /v1/subscriptions/{id}/deliveries", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
_, in, err := eventQuery(r, false)
if err != nil {
return nil, 0, err
}
out, err := s.Deliveries(r.Context(), t, core.ID(r.PathValue("id")), in)
return out, 200, err
}))
mux.HandleFunc("GET /v1/subscriptions/{id}/deliveries/{delivery}", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
out, err := s.Detail(r.Context(), t, core.ID(r.PathValue("id")), core.ID(r.PathValue("delivery")))
return out, 200, err
}))
mux.HandleFunc("POST /v1/subscriptions/{id}/deliveries/{command}", wrap(func(r *http.Request, t auth.Token) (any, int, error) {
delivery, command, ok := strings.Cut(r.PathValue("command"), ":")
if !ok || command != "retry" {
return nil, 0, fault.NotFound
}
if err := noEventQuery(r); err != nil {
return nil, 0, err
}
var in events.RetryInput
if err := decodeEventBody(r, &in); err != nil {
return nil, 0, err
}
key, err := idempotencyKey(r)
if err != nil {
return nil, 0, err
}
out, err := s.Retry(r.Context(), t, core.ID(r.PathValue("id")), core.ID(delivery), key, in)
return out, 200, err
}))
}
func noEventQuery(r *http.Request) error {
if r.URL.RawQuery != "" || r.URL.ForceQuery {
return fault.Invalid("query parameters are not allowed")
}
return nil
}
func eventQuery(r *http.Request, subscriptions bool) (core.ID, events.PageInput, error) {
var in events.PageInput
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return "", in, fault.Invalid("invalid query encoding")
}
for k, v := range q {
if len(v) != 1 || (k != "limit" && k != "cursor" && !(subscriptions && k == "queue_id") && !(!subscriptions && k == "state")) {
return "", in, fault.Invalid("invalid subscription query")
}
}
if q.Has("limit") {
in.Limit, err = strconv.Atoi(q.Get("limit"))
if err != nil || in.Limit < 1 || strconv.Itoa(in.Limit) != q.Get("limit") {
return "", in, fault.Invalid("invalid limit")
}
}
in.Cursor = q.Get("cursor")
in.State = platform.DeliveryState(q.Get("state"))
return core.ID(q.Get("queue_id")), in, nil
}