refactor: move graphql into individual services
This commit is contained in:
@@ -1,94 +0,0 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/keys-pub/keys"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
return event.Register(ctx, &UserRegistered{})
|
||||
}
|
||||
|
||||
type SaltyUser struct {
|
||||
Name string
|
||||
Pubkey *keys.EdX25519PublicKey
|
||||
Inbox ulid.ULID
|
||||
|
||||
event.AggregateRoot
|
||||
}
|
||||
|
||||
var _ event.Aggregate = (*SaltyUser)(nil)
|
||||
|
||||
// ApplyEvent applies the event to the aggrigate state
|
||||
func (a *SaltyUser) ApplyEvent(lis ...event.Event) {
|
||||
for _, e := range lis {
|
||||
switch e := e.(type) {
|
||||
case *UserRegistered:
|
||||
a.Name = e.Name
|
||||
a.Pubkey = e.Pubkey
|
||||
a.Inbox = e.EventMeta().EventID
|
||||
a.SetStreamID(a.streamID())
|
||||
default:
|
||||
log.Printf("unknown event %T", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *SaltyUser) streamID() string {
|
||||
return fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(a.Name))))
|
||||
}
|
||||
|
||||
func (a *SaltyUser) OnUserRegister(name string, pubkey *keys.EdX25519PublicKey) error {
|
||||
event.Raise(a, &UserRegistered{Name: name, Pubkey: pubkey})
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserRegistered struct {
|
||||
Name string
|
||||
Pubkey *keys.EdX25519PublicKey
|
||||
|
||||
eventMeta event.Meta
|
||||
}
|
||||
|
||||
var _ event.Event = (*UserRegistered)(nil)
|
||||
|
||||
func (e *UserRegistered) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
|
||||
func (e *UserRegistered) SetEventMeta(m event.Meta) {
|
||||
if e != nil {
|
||||
e.eventMeta = m
|
||||
}
|
||||
}
|
||||
func (e *UserRegistered) MarshalBinary() (text []byte, err error) {
|
||||
var b bytes.Buffer
|
||||
b.WriteString(e.Name)
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(e.Pubkey.String())
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
func (e *UserRegistered) UnmarshalBinary(b []byte) error {
|
||||
name, pub, ok := bytes.Cut(b, []byte{'\t'})
|
||||
if !ok {
|
||||
return fmt.Errorf("parse error")
|
||||
}
|
||||
|
||||
var err error
|
||||
e.Name = string(name)
|
||||
e.Pubkey, err = keys.NewEdX25519PublicKeyFromID(keys.ID(pub))
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -215,7 +215,6 @@ func (e *eventLog) Read(ctx context.Context, pos, count int64) (event.Events, er
|
||||
}
|
||||
|
||||
start, count := math.PagerBox(first, last, pos, count)
|
||||
span.AddEvent(fmt.Sprint("reading", first, last, pos, count, start))
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
7
pkg/es/es.graphqls
Normal file
7
pkg/es/es.graphqls
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
type Meta @goModel(model: "github.com/sour-is/ev/pkg/es/event.Meta") {
|
||||
eventID: String! @goField(name: "getEventID")
|
||||
streamID: String!
|
||||
created: Time!
|
||||
position: Int!
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/sour-is/ev/internal/logz"
|
||||
"github.com/sour-is/ev/pkg/locker"
|
||||
)
|
||||
|
||||
@@ -66,41 +67,72 @@ func (u *UnknownEvent) MarshalBinary() ([]byte, error) {
|
||||
|
||||
// Register a type container for Unmarshalling values into. The type must implement Event and not be a nil value.
|
||||
func Register(ctx context.Context, lis ...Event) error {
|
||||
_, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
for _, e := range lis {
|
||||
if err := ctx.Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
if e == nil {
|
||||
return fmt.Errorf("can't register event.Event of type=%T with value=%v", e, e)
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(e)
|
||||
|
||||
if value.IsNil() {
|
||||
return fmt.Errorf("can't register event.Event of type=%T with value=%v", e, e)
|
||||
}
|
||||
|
||||
value = reflect.Indirect(value)
|
||||
|
||||
name := TypeOf(e)
|
||||
typ := value.Type()
|
||||
|
||||
if err := eventTypes.Modify(ctx, func(c *config) error {
|
||||
c.eventTypes[name] = typ
|
||||
return nil
|
||||
}); err != nil {
|
||||
err := RegisterName(ctx, name, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func RegisterName(ctx context.Context, name string, e Event) error {
|
||||
_, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if e == nil {
|
||||
err := fmt.Errorf("can't register event.Event of type=%T with value=%v", e, e)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(e)
|
||||
|
||||
if value.IsNil() {
|
||||
err := fmt.Errorf("can't register event.Event of type=%T with value=%v", e, e)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
value = reflect.Indirect(value)
|
||||
|
||||
typ := value.Type()
|
||||
|
||||
span.AddEvent("register: " + name)
|
||||
|
||||
if err := eventTypes.Modify(ctx, func(c *config) error {
|
||||
_, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
c.eventTypes[name] = typ
|
||||
return nil
|
||||
}); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func GetContainer(ctx context.Context, s string) Event {
|
||||
_, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var e Event
|
||||
|
||||
eventTypes.Modify(ctx, func(c *config) error {
|
||||
_, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
typ, ok := c.eventTypes[s]
|
||||
if !ok {
|
||||
return fmt.Errorf("not defined")
|
||||
err := fmt.Errorf("not defined: %s", s)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
newType := reflect.New(typ)
|
||||
newInterface := newType.Interface()
|
||||
@@ -108,7 +140,9 @@ func GetContainer(ctx context.Context, s string) Event {
|
||||
e = iface
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed")
|
||||
err := fmt.Errorf("failed")
|
||||
span.RecordError(err)
|
||||
return err
|
||||
})
|
||||
if e == nil {
|
||||
e = &UnknownEvent{eventType: s}
|
||||
@@ -142,13 +176,19 @@ func MarshalBinary(e Event) (txt []byte, err error) {
|
||||
}
|
||||
|
||||
func UnmarshalBinary(ctx context.Context, txt []byte, pos uint64) (e Event, err error) {
|
||||
_, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
sp := bytes.SplitN(txt, []byte{'\t'}, 4)
|
||||
if len(sp) != 4 {
|
||||
return nil, fmt.Errorf("invalid format. expected=4, got=%d", len(sp))
|
||||
err = fmt.Errorf("invalid format. expected=4, got=%d", len(sp))
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := Meta{}
|
||||
if err = m.EventID.UnmarshalText(sp[0]); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -157,8 +197,10 @@ func UnmarshalBinary(ctx context.Context, txt []byte, pos uint64) (e Event, err
|
||||
|
||||
eventType := string(sp[2])
|
||||
e = GetContainer(ctx, eventType)
|
||||
span.AddEvent(fmt.Sprintf("%s == %T", eventType, e))
|
||||
|
||||
if err = e.UnmarshalBinary(sp[3]); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
36
pkg/gql/common.graphqls
Normal file
36
pkg/gql/common.graphqls
Normal file
@@ -0,0 +1,36 @@
|
||||
scalar Time
|
||||
scalar Map
|
||||
|
||||
type Connection @goModel(model: "github.com/sour-is/ev/pkg/gql.Connection") {
|
||||
paging: PageInfo!
|
||||
edges: [Edge!]!
|
||||
}
|
||||
input PageInput @goModel(model: "github.com/sour-is/ev/pkg/gql.PageInput") {
|
||||
idx: Int = 0
|
||||
count: Int = 30
|
||||
}
|
||||
type PageInfo @goModel(model: "github.com/sour-is/ev/pkg/gql.PageInfo") {
|
||||
next: Boolean!
|
||||
prev: Boolean!
|
||||
|
||||
begin: Int!
|
||||
end: Int!
|
||||
}
|
||||
interface Edge @goModel(model: "github.com/sour-is/ev/pkg/gql.Edge"){
|
||||
id: ID!
|
||||
}
|
||||
|
||||
directive @goModel(
|
||||
model: String
|
||||
models: [String!]
|
||||
) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
|
||||
|
||||
directive @goField(
|
||||
forceResolver: Boolean
|
||||
name: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
||||
|
||||
directive @goTag(
|
||||
key: String!
|
||||
value: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
||||
56
pkg/gql/connection.go
Normal file
56
pkg/gql/connection.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
type Edge interface {
|
||||
IsEdge()
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
Paging *PageInfo `json:"paging"`
|
||||
Edges []Edge `json:"edges"`
|
||||
}
|
||||
|
||||
type PostEvent struct {
|
||||
ID string `json:"id"`
|
||||
Payload string `json:"payload"`
|
||||
Tags []string `json:"tags"`
|
||||
Meta *event.Meta `json:"meta"`
|
||||
}
|
||||
|
||||
func (PostEvent) IsEdge() {}
|
||||
|
||||
func (e *PostEvent) PayloadJSON(ctx context.Context) (m map[string]interface{}, err error) {
|
||||
err = json.Unmarshal([]byte(e.Payload), &m)
|
||||
return
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
Next bool `json:"next"`
|
||||
Prev bool `json:"prev"`
|
||||
Begin uint64 `json:"begin"`
|
||||
End uint64 `json:"end"`
|
||||
}
|
||||
|
||||
type PageInput struct {
|
||||
Idx *int64 `json:"idx"`
|
||||
Count *int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (p *PageInput) GetIdx(v int64) int64 {
|
||||
if p == nil || p.Idx == nil {
|
||||
return v
|
||||
}
|
||||
return *p.Idx
|
||||
}
|
||||
func (p *PageInput) GetCount(v int64) int64 {
|
||||
if p == nil || p.Count == nil {
|
||||
return v
|
||||
}
|
||||
return *p.Count
|
||||
}
|
||||
14
pkg/gql/context.go
Normal file
14
pkg/gql/context.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package gql
|
||||
|
||||
import "context"
|
||||
|
||||
func ToContext[K comparable, V any](ctx context.Context, key K, value V) context.Context {
|
||||
return context.WithValue(ctx, key, value)
|
||||
}
|
||||
func FromContext[K comparable, V any](ctx context.Context, key K) V {
|
||||
var empty V
|
||||
if v, ok := ctx.Value(key).(V); ok {
|
||||
return v
|
||||
}
|
||||
return empty
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
package msgbus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/sour-is/ev/internal/logz"
|
||||
"github.com/sour-is/ev/pkg/domain"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
baseURL string
|
||||
es *es.EventStore
|
||||
}
|
||||
|
||||
func New(ctx context.Context, es *es.EventStore, baseURL string) (*service, error) {
|
||||
ctx, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if err := event.Register(ctx, &PostEvent{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &service{baseURL, es}, nil
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
s.websocket(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/.well-known/salty") {
|
||||
s.getUser(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
s.get(w, r)
|
||||
case http.MethodPost, http.MethodPut:
|
||||
s.post(w, r)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) get(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
name, _, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
var pos, count int64 = 0, es.AllEvents
|
||||
qry := r.URL.Query()
|
||||
|
||||
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 1 {
|
||||
pos = i - 1
|
||||
}
|
||||
if i, err := strconv.ParseInt(qry.Get("pos"), 10, 64); err == nil {
|
||||
pos = i
|
||||
}
|
||||
if i, err := strconv.ParseInt(qry.Get("n"), 10, 64); err == nil {
|
||||
count = i
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("GET topic=", name, " idx=", pos, " n=", count))
|
||||
events, err := s.es.Read(ctx, "post-"+name, pos, count)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
if err = encodeJSON(w, first, events...); err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i := range events {
|
||||
fmt.Fprintln(w, events[i])
|
||||
}
|
||||
}
|
||||
func (s *service) getUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
addr := "saltyuser-" + strings.TrimPrefix(r.URL.Path, "/.well-known/salty/")
|
||||
addr = strings.TrimSuffix(addr, ".json")
|
||||
|
||||
span.AddEvent(fmt.Sprint("find ", addr))
|
||||
a, err := es.Update(ctx, s.es, addr, func(ctx context.Context, agg *domain.SaltyUser) error { return nil })
|
||||
switch {
|
||||
case errors.Is(err, event.ErrShouldExist):
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(
|
||||
struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Key string `json:"key"`
|
||||
}{
|
||||
Endpoint: path.Join(s.baseURL, a.Inbox.String()),
|
||||
Key: a.Pubkey.ID().String(),
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
}
|
||||
func (s *service) post(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
name, tags, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
events := event.NewEvents(&PostEvent{
|
||||
Payload: b,
|
||||
Tags: fields(tags),
|
||||
})
|
||||
|
||||
_, err = s.es.Append(ctx, "post-"+name, events)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if first == event.NilEvent {
|
||||
first = events.First()
|
||||
}
|
||||
|
||||
m := events.First().EventMeta()
|
||||
span.AddEvent(fmt.Sprint("POST topic=", name, " tags=", tags, " idx=", m.Position, " id=", m.EventID))
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err = encodeJSON(w, first, events...); err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
span.AddEvent("finish response")
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
fmt.Fprintf(w, "OK %d %s", m.Position, m.EventID)
|
||||
}
|
||||
func (s *service) websocket(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx, span := logz.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
name, _, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
var pos int64 = 0
|
||||
qry := r.URL.Query()
|
||||
|
||||
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 0 {
|
||||
pos = i - 1
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("WS topic=", name, " idx=", pos))
|
||||
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
c.SetCloseHandler(func(code int, text string) error {
|
||||
cancel()
|
||||
return nil
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
mt, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("recv: %d %s", mt, message))
|
||||
}
|
||||
}()
|
||||
|
||||
es := s.es.EventStream()
|
||||
if es == nil {
|
||||
span.AddEvent("EventStore does not implement streaming")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sub, err := es.Subscribe(ctx, "post-"+name, pos)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
span.AddEvent("stop ws")
|
||||
sub.Close(ctx)
|
||||
}()
|
||||
|
||||
span.AddEvent("start ws")
|
||||
for sub.Recv(ctx) {
|
||||
events, err := sub.Events(ctx)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
span.AddEvent(fmt.Sprint("got events ", len(events)))
|
||||
for i := range events {
|
||||
e, ok := events[i].(*PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
span.AddEvent(fmt.Sprint("send", i, e.String()))
|
||||
|
||||
var b bytes.Buffer
|
||||
if err = encodeJSON(&b, first, e); err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
|
||||
err = c.WriteMessage(websocket.TextMessage, b.Bytes())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PostEvent struct {
|
||||
Payload []byte
|
||||
Tags []string
|
||||
|
||||
eventMeta event.Meta
|
||||
}
|
||||
|
||||
func (e *PostEvent) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
func (e *PostEvent) SetEventMeta(eventMeta event.Meta) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.eventMeta = eventMeta
|
||||
}
|
||||
func (e *PostEvent) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
func (e *PostEvent) UnmarshalBinary(b []byte) error {
|
||||
return json.Unmarshal(b, e)
|
||||
}
|
||||
|
||||
func (e *PostEvent) String() string {
|
||||
var b bytes.Buffer
|
||||
|
||||
// b.WriteString(e.eventMeta.StreamID)
|
||||
// b.WriteRune('@')
|
||||
b.WriteString(strconv.FormatUint(e.eventMeta.Position, 10))
|
||||
b.WriteRune('\t')
|
||||
|
||||
b.WriteString(e.eventMeta.EventID.String())
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(string(e.Payload))
|
||||
if len(e.Tags) > 0 {
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(strings.Join(e.Tags, ","))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
func fields(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, "/")
|
||||
}
|
||||
|
||||
func encodeJSON(w io.Writer, first event.Event, events ...event.Event) error {
|
||||
out := make([]struct {
|
||||
ID uint64 `json:"id"`
|
||||
Payload []byte `json:"payload"`
|
||||
Created string `json:"created"`
|
||||
Tags []string `json:"tags"`
|
||||
Topic struct {
|
||||
Name string `json:"name"`
|
||||
TTL uint64 `json:"ttl"`
|
||||
Seq uint64 `json:"seq"`
|
||||
Created string `json:"created"`
|
||||
} `json:"topic"`
|
||||
}, len(events))
|
||||
|
||||
for i := range events {
|
||||
e, ok := events[i].(*PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[i].ID = e.EventMeta().Position
|
||||
out[i].Created = e.EventMeta().Created().Format(time.RFC3339Nano)
|
||||
out[i].Payload = e.Payload
|
||||
out[i].Tags = e.Tags
|
||||
out[i].Topic.Name = strings.TrimPrefix(e.EventMeta().StreamID, "post-")
|
||||
out[i].Topic.Created = first.EventMeta().Created().Format(time.RFC3339Nano)
|
||||
out[i].Topic.Seq = e.EventMeta().Position
|
||||
}
|
||||
|
||||
if len(out) == 1 {
|
||||
return json.NewEncoder(w).Encode(out[0])
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8/>
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
|
||||
<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
|
||||
integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
|
||||
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
|
||||
integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"
|
||||
integrity="{{ .jsSRI }}" crossorigin="anonymous"></script>
|
||||
<title>{{.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<style type="text/css">
|
||||
html { font-family: "Open Sans", sans-serif; overflow: hidden; }
|
||||
body { margin: 0; background: #172a3a; }
|
||||
</style>
|
||||
<div id="root"/>
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', function (event) {
|
||||
const root = document.getElementById('root');
|
||||
root.classList.add('playgroundIn');
|
||||
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:'
|
||||
GraphQLPlayground.init(root, {
|
||||
endpoint: location.protocol + '//' + location.host + '{{.endpoint}}',
|
||||
subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}',
|
||||
shareEnabled: true,
|
||||
settings: {
|
||||
'request.credentials': 'same-origin'
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
func Handler(title string, endpoint string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
err := page.Execute(w, map[string]string{
|
||||
"title": title,
|
||||
"endpoint": endpoint,
|
||||
"version": "1.7.26",
|
||||
"cssSRI": "sha256-dKnNLEFwKSVFpkpjRWe+o/jQDM6n/JsvQ0J3l5Dk3fc=",
|
||||
"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
|
||||
"jsSRI": "sha256-SG9YAy4eywTcLckwij7V4oSCG3hOdV1m+2e1XuNxIgk=",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user