refactor: moving items around into related files/packages
This commit is contained in:
@@ -28,7 +28,7 @@ type EventLogWithUpdate interface {
|
||||
}
|
||||
|
||||
type Subscription interface {
|
||||
Recv(context.Context) bool
|
||||
Recv(context.Context) <-chan bool
|
||||
Events(context.Context) (event.Events, error)
|
||||
Close(context.Context) error
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package streamer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -67,6 +68,7 @@ func (s *streamer) Subscribe(ctx context.Context, streamID string, start int64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sub := &subscription{topic: streamID, events: events}
|
||||
sub.position = locker.New(&position{
|
||||
idx: start,
|
||||
@@ -86,12 +88,14 @@ func (s *streamer) Send(ctx context.Context, streamID string, events event.Event
|
||||
return s.state.Modify(ctx, func(ctx context.Context, state *state) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.AddEvent(fmt.Sprint("subscribers=", len(state.subscribers[streamID])))
|
||||
|
||||
for _, sub := range state.subscribers[streamID] {
|
||||
err := sub.position.Modify(ctx, func(ctx context.Context, position *position) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("streamID", streamID),
|
||||
attribute.Int64("actualPosition", int64(events.Last().EventMeta().ActualPosition)),
|
||||
@@ -106,6 +110,7 @@ func (s *streamer) Send(ctx context.Context, streamID string, events event.Event
|
||||
position.link = trace.LinkFromContext(ctx, attribute.String("src", "event"))
|
||||
position.wait = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -210,56 +215,65 @@ type subscription struct {
|
||||
|
||||
events driver.EventLog
|
||||
unsub func(context.Context) error
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (s *subscription) Recv(ctx context.Context) bool {
|
||||
func (s *subscription) Recv(ctx context.Context) <-chan bool {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var wait func(context.Context) bool
|
||||
done := make(chan bool)
|
||||
|
||||
err := s.position.Modify(ctx, func(ctx context.Context, position *position) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
go func() {
|
||||
var wait func(context.Context) bool
|
||||
defer close(done)
|
||||
|
||||
if position.size == es.AllEvents {
|
||||
return nil
|
||||
}
|
||||
if position.size == 0 {
|
||||
position.wait = make(chan struct{})
|
||||
wait = func(ctx context.Context) bool {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
err := s.position.Modify(ctx, func(ctx context.Context, position *position) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
select {
|
||||
case <-position.wait:
|
||||
if position.link.SpanContext.IsValid() {
|
||||
_, span := lg.Span(ctx, trace.WithLinks(position.link))
|
||||
span.AddEvent("recv event")
|
||||
span.End()
|
||||
position.link = trace.Link{}
|
||||
if position.size == es.AllEvents {
|
||||
return nil
|
||||
}
|
||||
if position.size == 0 {
|
||||
position.wait = make(chan struct{})
|
||||
wait = func(ctx context.Context) bool {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
select {
|
||||
case <-position.wait:
|
||||
if position.link.SpanContext.IsValid() {
|
||||
_, span := lg.Span(ctx, trace.WithLinks(position.link))
|
||||
span.AddEvent("recv event")
|
||||
span.End()
|
||||
position.link = trace.Link{}
|
||||
}
|
||||
return true
|
||||
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
position.idx += position.size
|
||||
position.size = 0
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
done <- false
|
||||
return
|
||||
}
|
||||
position.idx += position.size
|
||||
position.size = 0
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if wait != nil {
|
||||
return wait(ctx)
|
||||
}
|
||||
if wait != nil {
|
||||
done <- wait(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
return true
|
||||
done <- true
|
||||
}()
|
||||
|
||||
return done
|
||||
}
|
||||
func (s *subscription) Events(ctx context.Context) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
@@ -293,5 +307,12 @@ func (s *subscription) Close(ctx context.Context) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return s.unsub(ctx)
|
||||
if s == nil || s.unsub == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
s.once.Do(func() { err = s.unsub(ctx) })
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ type contextKey struct {
|
||||
|
||||
var esKey = contextKey{"event-store"}
|
||||
|
||||
func (es *EventStore) IsResolver() {}
|
||||
func (es *EventStore) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
@@ -51,10 +52,10 @@ func (es *EventStore) Events(ctx context.Context, streamID string, paging *gql.P
|
||||
|
||||
return &gql.Connection{
|
||||
Paging: &gql.PageInfo{
|
||||
Next: lis.Last().EventMeta().Position < last,
|
||||
Prev: lis.First().EventMeta().Position > first,
|
||||
Begin: lis.First().EventMeta().Position,
|
||||
End: lis.Last().EventMeta().Position,
|
||||
Next: lis.Last().EventMeta().ActualPosition < last,
|
||||
Prev: lis.First().EventMeta().ActualPosition > first,
|
||||
Begin: lis.First().EventMeta().ActualPosition,
|
||||
End: lis.Last().EventMeta().ActualPosition,
|
||||
},
|
||||
Edges: edges,
|
||||
}, nil
|
||||
@@ -91,7 +92,7 @@ func (e *EventStore) EventAdded(ctx context.Context, streamID string, after int6
|
||||
}()
|
||||
}
|
||||
|
||||
for sub.Recv(ctx) {
|
||||
for <-sub.Recv(ctx) {
|
||||
events, err := sub.Events(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
@@ -120,7 +121,6 @@ func (es *EventStore) TruncateStream(ctx context.Context, streamID string, index
|
||||
err := es.Truncate(ctx, streamID, index)
|
||||
return err == nil, err
|
||||
}
|
||||
func (*EventStore) RegisterHTTP(*http.ServeMux) {}
|
||||
func (e *EventStore) GetMiddleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
120
pkg/gql/graphiql/playground.go
Normal file
120
pkg/gql/graphiql/playground.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package graphiql
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{.title}}</title>
|
||||
<style>
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#graphiql {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/react@{{.reactVersion}}/umd/react.production.min.js"
|
||||
integrity="{{.reactSRI}}"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/react-dom@{{.reactVersion}}/umd/react-dom.production.min.js"
|
||||
integrity="{{.reactDOMSRI}}"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.css"
|
||||
x-integrity="{{.cssSRI}}"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="graphiql">Loading...</div>
|
||||
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/graphiql@{{.version}}/graphiql.min.js"
|
||||
x-integrity="{{.jsSRI}}"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<script>
|
||||
{{- if .endpointIsAbsolute}}
|
||||
const url = {{.endpoint}};
|
||||
const subscriptionUrl = {{.subscriptionEndpoint}};
|
||||
{{- else}}
|
||||
const url = location.protocol + '//' + location.host + {{.endpoint}};
|
||||
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:';
|
||||
const subscriptionUrl = wsProto + '//' + location.host + {{.endpoint}};
|
||||
{{- end}}
|
||||
|
||||
const fetcher = GraphiQL.createFetcher({ url, subscriptionUrl });
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: fetcher,
|
||||
isHeadersEditorEnabled: true,
|
||||
shouldPersistHeaders: true
|
||||
}),
|
||||
document.getElementById('graphiql'),
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
// Handler responsible for setting up the playground
|
||||
func Handler(title string, endpoint string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html; charset=UTF-8")
|
||||
err := page.Execute(w, map[string]interface{}{
|
||||
"title": title,
|
||||
"endpoint": endpoint,
|
||||
"endpointIsAbsolute": endpointHasScheme(endpoint),
|
||||
"subscriptionEndpoint": getSubscriptionEndpoint(endpoint),
|
||||
"version": "2.0.13",
|
||||
"reactVersion": "17.0.2",
|
||||
"cssSRI": "sha256-qKvndYgkAMQOBoa1SZF9NlbIig+kQ3Fk4f8wlrEqBLw=",
|
||||
"jsSRI": "sha256-dExtzxjgqXfOgQ94xw079jAjd4dPAFrO2Qz6I3Yd9Ko=",
|
||||
"reactSRI": "sha256-Ipu/TQ50iCCVZBUsZyNJfxrDk0E2yhaEIz0vqI+kFG8=",
|
||||
"reactDOMSRI": "sha256-nbMykgB6tsOFJ7OdVmPpdqMFVk4ZsqWocT6issAPUF0=",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endpointHasScheme checks if the endpoint has a scheme.
|
||||
func endpointHasScheme(endpoint string) bool {
|
||||
u, err := url.Parse(endpoint)
|
||||
return err == nil && u.Scheme != ""
|
||||
}
|
||||
|
||||
// getSubscriptionEndpoint returns the subscription endpoint for the given
|
||||
// endpoint if it is parsable as a URL, or an empty string.
|
||||
func getSubscriptionEndpoint(endpoint string) string {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "https":
|
||||
u.Scheme = "wss"
|
||||
default:
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
|
||||
return u.String()
|
||||
}
|
||||
62
pkg/gql/playground/playground.go
Normal file
62
pkg/gql/playground/playground.go
Normal file
@@ -0,0 +1,62 @@
|
||||
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.28",
|
||||
"cssSRI": "sha256-dKnNLEFwKSVFpkpjRWe+o/jQDM6n/JsvQ0J3l5Dk3fc=",
|
||||
"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
|
||||
"jsSRI": "sha256-VVwEZwxs4qS5W7E+/9nXINYgr/BJRWKOi/rTMUdmmWg=",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
141
pkg/gql/resolver/resolver.go
Normal file
141
pkg/gql/resolver/resolver.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||
"github.com/99designs/gqlgen/graphql/handler/lru"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/ravilushqa/otelgqlgen"
|
||||
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/gql/graphiql"
|
||||
"github.com/sour-is/ev/pkg/gql/playground"
|
||||
)
|
||||
|
||||
type BaseResolver interface {
|
||||
ExecutableSchema() graphql.ExecutableSchema
|
||||
BaseResolver() IsResolver
|
||||
}
|
||||
|
||||
type Resolver[T BaseResolver] struct {
|
||||
res T
|
||||
}
|
||||
type IsResolver interface {
|
||||
IsResolver()
|
||||
}
|
||||
|
||||
func New[T BaseResolver](ctx context.Context, base T, resolvers ...IsResolver) (*Resolver[T], error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
v := reflect.ValueOf(base)
|
||||
v = reflect.Indirect(v)
|
||||
noop := reflect.ValueOf(base.BaseResolver())
|
||||
|
||||
outer:
|
||||
for _, idx := range reflect.VisibleFields(v.Type()) {
|
||||
field := v.FieldByIndex(idx.Index)
|
||||
|
||||
for i := range resolvers {
|
||||
rs := reflect.ValueOf(resolvers[i])
|
||||
|
||||
if field.IsNil() && rs.Type().Implements(field.Type()) {
|
||||
span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
|
||||
field.Set(rs)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
|
||||
field.Set(noop)
|
||||
}
|
||||
|
||||
return &Resolver[T]{base}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver[T]) Resolver() T {
|
||||
return r.res
|
||||
}
|
||||
|
||||
// ChainMiddlewares will check all embeded resolvers for a GetMiddleware func and add to handler.
|
||||
func (r *Resolver[T]) ChainMiddlewares(h http.Handler) http.Handler {
|
||||
v := reflect.ValueOf(r) // Get reflected value of *Resolver
|
||||
v = reflect.Indirect(v) // Get the pointed value (returns a zero value on nil)
|
||||
n := v.NumField() // Get number of fields to iterate over.
|
||||
for i := 0; i < n; i++ {
|
||||
f := v.Field(i)
|
||||
if !f.CanInterface() { // Skip non-interface types.
|
||||
continue
|
||||
}
|
||||
if iface, ok := f.Interface().(interface {
|
||||
GetMiddleware() func(http.Handler) http.Handler
|
||||
}); ok {
|
||||
h = iface.GetMiddleware()(h) // Append only items that fulfill the interface.
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (r *Resolver[T]) RegisterHTTP(mux *http.ServeMux) {
|
||||
gql := NewServer(r.res.ExecutableSchema())
|
||||
gql.SetRecoverFunc(NoopRecover)
|
||||
gql.Use(otelgqlgen.Middleware())
|
||||
mux.Handle("/graphiql", graphiql.Handler("GraphiQL playground", "/gql"))
|
||||
mux.Handle("/gql", lg.Htrace(r.ChainMiddlewares(gql), "gql"))
|
||||
mux.Handle("/playground", playground.Handler("GraphQL playground", "/gql"))
|
||||
}
|
||||
|
||||
func NoopRecover(ctx context.Context, err interface{}) error {
|
||||
if err, ok := err.(string); ok && err == "not implemented" {
|
||||
return gqlerror.Errorf("not implemented")
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
debug.PrintStack()
|
||||
|
||||
return gqlerror.Errorf("internal system error")
|
||||
}
|
||||
|
||||
func NewServer(es graphql.ExecutableSchema) *handler.Server {
|
||||
srv := handler.New(es)
|
||||
|
||||
srv.AddTransport(transport.Websocket{
|
||||
Upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
switch r.Header.Get("Origin") {
|
||||
case "https://ev.sour.is", "https://www.graphqlbin.com", "http://localhost:8080":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
KeepAlivePingInterval: 10 * time.Second,
|
||||
})
|
||||
srv.AddTransport(transport.Options{})
|
||||
srv.AddTransport(transport.GET{})
|
||||
srv.AddTransport(transport.POST{})
|
||||
srv.AddTransport(transport.MultipartForm{})
|
||||
|
||||
srv.SetQueryCache(lru.New(1000))
|
||||
|
||||
srv.Use(extension.Introspection{})
|
||||
srv.Use(extension.AutomaticPersistedQuery{
|
||||
Cache: lru.New(100),
|
||||
})
|
||||
|
||||
return srv
|
||||
}
|
||||
Reference in New Issue
Block a user