refactor: push commands in to cmd and ev to root as library

This commit is contained in:
Jon Lundy 2023-01-09 11:30:02 -07:00
parent 250395d6b3
commit 4fc9c78dae
Signed by untrusted user who does not match committer: xuu
GPG Key ID: C63E6D61F3035024
58 changed files with 765 additions and 376 deletions

View File

@ -1,11 +1,11 @@
root = "." root = "./cmd/ev"
testdata_dir = "data" testdata_dir = "data"
tmp_dir = "tmp" tmp_dir = "tmp"
[build] [build]
args_bin = [] args_bin = []
bin = "./tmp/main" bin = "./tmp/main"
cmd = "go build -o ./tmp/main ." cmd = "go build -o ./tmp/main ./cmd/ev"
delay = 1000 delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"] exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = [] exclude_file = []

2
.gitignore vendored
View File

@ -5,4 +5,4 @@ data/
local.mk local.mk
logzio.yml logzio.yml
tmp/ tmp/
ev /ev

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
local.mk:d632b22a2291637331e5613d35536c69e696447ce407d7320b4c5ab0922b47a9

View File

@ -9,10 +9,10 @@ air: gen
ifeq (, $(shell which air)) ifeq (, $(shell which air))
go install github.com/cosmtrek/air@latest go install github.com/cosmtrek/air@latest
endif endif
air air ./cmd/ev
run: run:
go run . go run ./cmd/ev
test: test:
go test -cover -race ./... go test -cover -race ./...
@ -25,6 +25,8 @@ GQLS:=$(GQLS) $(wildcard app/*/*.graphqls)
GQLS:=$(GQLS) $(wildcard app/*/*.go) GQLS:=$(GQLS) $(wildcard app/*/*.go)
GQLSRC=internal/graph/generated/generated.go GQLSRC=internal/graph/generated/generated.go
clean:
rm -f "$(GQLSRC)"
gen: gql gen: gql
gql: $(GQLSRC) gql: $(GQLSRC)
$(GQLSRC): $(GQLS) $(GQLSRC): $(GQLS)

3
app/README.md Normal file
View File

@ -0,0 +1,3 @@
# App examples
These applications are to demonstrate how the EV library can be used.

View File

@ -17,14 +17,14 @@ import (
"go.opentelemetry.io/otel/metric/unit" "go.opentelemetry.io/otel/metric/unit"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/gql" "github.com/sour-is/ev/pkg/gql"
) )
type service struct { type service struct {
es *es.EventStore es *ev.EventStore
m_gql_posts syncint64.Counter m_gql_posts syncint64.Counter
m_gql_post_added syncint64.Counter m_gql_post_added syncint64.Counter
@ -38,7 +38,7 @@ type MsgbusResolver interface {
IsResolver() IsResolver()
} }
func New(ctx context.Context, es *es.EventStore) (*service, error) { func New(ctx context.Context, es *ev.EventStore) (*service, error) {
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
defer span.End() defer span.End()
@ -243,7 +243,7 @@ func (s *service) get(w http.ResponseWriter, r *http.Request) {
first = lis[0] first = lis[0]
} }
var pos, count int64 = 0, es.AllEvents var pos, count int64 = 0, ev.AllEvents
qry := r.URL.Query() qry := r.URL.Query()
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 1 { if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 1 {

View File

@ -19,8 +19,8 @@ import (
contentnegotiation "gitlab.com/jamietanna/content-negotiation-go" contentnegotiation "gitlab.com/jamietanna/content-negotiation-go"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
) )
@ -150,7 +150,7 @@ func (s *service) getPending(w http.ResponseWriter, r *http.Request, peerID stri
return return
} }
info, err := es.Upsert(ctx, s.es, aggInfo, func(ctx context.Context, agg *Info) error { info, err := ev.Upsert(ctx, s.es, aggInfo, func(ctx context.Context, agg *Info) error {
return agg.OnUpsert() // initialize if not exists return agg.OnUpsert() // initialize if not exists
}) })
if err != nil { if err != nil {

View File

@ -9,8 +9,8 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/set" "github.com/sour-is/ev/pkg/set"
) )
@ -151,7 +151,7 @@ func (s *service) cleanRequests(ctx context.Context, now time.Time) error {
for { for {
events, err := s.es.Read(ctx, queueRequests, startPosition, 1000) // read 1000 from the top each loop. events, err := s.es.Read(ctx, queueRequests, startPosition, 1000) // read 1000 from the top each loop.
if err != nil && !errors.Is(err, es.ErrNotFound) { if err != nil && !errors.Is(err, ev.ErrNotFound) {
span.RecordError(err) span.RecordError(err)
return err return err
} }

View File

@ -6,8 +6,8 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/locker" "github.com/sour-is/ev/pkg/locker"
"go.uber.org/multierr" "go.uber.org/multierr"
@ -24,7 +24,7 @@ func aggRequest(id string) string { return "pf-request-" + id }
func aggPeer(id string) string { return "pf-peer-" + id } func aggPeer(id string) string { return "pf-peer-" + id }
type service struct { type service struct {
es *es.EventStore es *ev.EventStore
statusURL string statusURL string
state *locker.Locked[state] state *locker.Locked[state]
@ -37,7 +37,7 @@ type state struct {
requests map[string]*Request requests map[string]*Request
} }
func New(ctx context.Context, es *es.EventStore, statusURL string) (*service, error) { func New(ctx context.Context, es *ev.EventStore, statusURL string) (*service, error) {
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
defer span.End() defer span.End()

View File

@ -19,8 +19,8 @@ import (
"go.opentelemetry.io/otel/metric/unit" "go.opentelemetry.io/otel/metric/unit"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/gql" "github.com/sour-is/ev/pkg/gql"
) )
@ -31,7 +31,7 @@ type DNSResolver interface {
type service struct { type service struct {
baseURL string baseURL string
es *es.EventStore es *ev.EventStore
dns DNSResolver dns DNSResolver
m_create_user syncint64.Counter m_create_user syncint64.Counter
@ -54,7 +54,7 @@ type SaltyResolver interface {
IsResolver() IsResolver()
} }
func New(ctx context.Context, es *es.EventStore, baseURL string) (*service, error) { func New(ctx context.Context, es *ev.EventStore, baseURL string) (*service, error) {
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
defer span.End() defer span.End()
@ -111,23 +111,23 @@ func New(ctx context.Context, es *es.EventStore, baseURL string) (*service, erro
return svc, errs return svc, errs
} }
func (s *service) IsResolver() {}
func (s *service) BaseURL() string { func (s *service) BaseURL() string {
if s == nil { if s == nil {
return "http://missing.context/" return "http://missing.context/"
} }
return s.baseURL return s.baseURL
} }
func (s *service) RegisterHTTP(mux *http.ServeMux) {
mux.Handle("/.well-known/salty/", lg.Htrace(s, "lookup")) func (s *service) RegisterHTTP(mux *http.ServeMux) {}
}
func (s *service) RegisterAPIv1(mux *http.ServeMux) { func (s *service) RegisterAPIv1(mux *http.ServeMux) {
mux.HandleFunc("/ping", s.apiv1) mux.HandleFunc("/ping", s.apiv1)
mux.HandleFunc("/register", s.apiv1) mux.HandleFunc("/register", s.apiv1)
mux.HandleFunc("/lookup/", s.apiv1) mux.HandleFunc("/lookup/", s.apiv1)
mux.HandleFunc("/send", s.apiv1) mux.HandleFunc("/send", s.apiv1)
} }
func (s *service) RegisterWellKnown(mux *http.ServeMux) {
mux.Handle("/salty/", lg.Htrace(s, "lookup"))
}
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
@ -140,7 +140,7 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
addr = strings.TrimSuffix(addr, ".json") addr = strings.TrimSuffix(addr, ".json")
span.AddEvent(fmt.Sprint("find ", addr)) span.AddEvent(fmt.Sprint("find ", addr))
a, err := es.Update(ctx, s.es, addr, func(ctx context.Context, agg *SaltyUser) error { return nil }) a, err := ev.Update(ctx, s.es, addr, func(ctx context.Context, agg *SaltyUser) error { return nil })
switch { switch {
case errors.Is(err, event.ErrShouldExist): case errors.Is(err, event.ErrShouldExist):
span.RecordError(err) span.RecordError(err)
@ -168,6 +168,16 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
span.RecordError(err) span.RecordError(err)
} }
} }
func (s *service) IsResolver() {}
func (s *service) GetMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(gql.ToContext(r.Context(), saltyKey, s))
next.ServeHTTP(w, r)
})
}
}
func (s *service) CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error) { func (s *service) CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error) {
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
defer span.End() defer span.End()
@ -191,11 +201,11 @@ func (s *service) createSaltyUser(ctx context.Context, streamID, pub string) (*S
return nil, err return nil, err
} }
a, err := es.Create(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error { a, err := ev.Create(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error {
return agg.OnUserRegister(key) return agg.OnUserRegister(key)
}) })
switch { switch {
case errors.Is(err, es.ErrShouldNotExist): case errors.Is(err, ev.ErrShouldNotExist):
span.RecordError(err) span.RecordError(err)
return nil, fmt.Errorf("user exists: %w", err) return nil, fmt.Errorf("user exists: %w", err)
@ -217,9 +227,9 @@ func (s *service) SaltyUser(ctx context.Context, nick string) (*SaltyUser, error
streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick)))) streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
span.AddEvent(streamID) span.AddEvent(streamID)
a, err := es.Update(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error { return nil }) a, err := ev.Update(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error { return nil })
switch { switch {
case errors.Is(err, es.ErrShouldExist): case errors.Is(err, ev.ErrShouldExist):
span.RecordError(err) span.RecordError(err)
return nil, fmt.Errorf("user not found") return nil, fmt.Errorf("user not found")
@ -230,14 +240,6 @@ func (s *service) SaltyUser(ctx context.Context, nick string) (*SaltyUser, error
return a, err return a, err
} }
func (s *service) GetMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(gql.ToContext(r.Context(), saltyKey, s))
next.ServeHTTP(w, r)
})
}
}
func (s *service) apiv1(w http.ResponseWriter, r *http.Request) { func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()

View File

@ -0,0 +1 @@
package webfinger

3
cmd/README.md Normal file
View File

@ -0,0 +1,3 @@
# Cmd
These are examples that can be built using EV. Because they are modular the apps can be mixed an matched by including the different source files linked from `cmd/ev`.

32
cmd/ev/app.msgbus.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"context"
"fmt"
"github.com/sour-is/ev"
"github.com/sour-is/ev/app/msgbus"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/service"
"github.com/sour-is/ev/pkg/slice"
)
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable Msgbus")
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
if !ok {
return fmt.Errorf("*es.EventStore not found in services")
}
msgbus, err := msgbus.New(ctx, eventstore)
if err != nil {
span.RecordError(err)
return err
}
svc.Add(msgbus)
return nil
})

43
cmd/ev/app.peerfinder.go Normal file
View File

@ -0,0 +1,43 @@
package main
import (
"context"
"fmt"
"github.com/sour-is/ev"
"github.com/sour-is/ev/app/peerfinder"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/env"
"github.com/sour-is/ev/pkg/es/driver/projecter"
"github.com/sour-is/ev/pkg/service"
"github.com/sour-is/ev/pkg/slice"
)
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable Peers")
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
if !ok {
return fmt.Errorf("*es.EventStore not found in services")
}
eventstore.Option(projecter.New(ctx, peerfinder.Projector))
peers, err := peerfinder.New(ctx, eventstore, env.Secret("PEER_STATUS", "").Secret())
if err != nil {
span.RecordError(err)
return err
}
svc.RunOnce(ctx, peers.RefreshJob)
svc.NewCron("0,15,30,45", peers.RefreshJob)
svc.RunOnce(ctx, peers.CleanJob)
svc.NewCron("0 1", peers.CleanJob)
svc.OnStart(peers.Run)
svc.OnStop(peers.Stop)
svc.Add(peers)
return nil
})

46
cmd/ev/app.salty.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/sour-is/ev"
"github.com/sour-is/ev/app/salty"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/env"
"github.com/sour-is/ev/pkg/service"
"github.com/sour-is/ev/pkg/slice"
)
var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable Salty")
eventstore, ok := slice.Find[*ev.EventStore](svc.Services...)
if !ok {
return fmt.Errorf("*es.EventStore not found in services")
}
addr := "localhost"
if ht, ok := slice.Find[*http.Server](svc.Services...); ok {
addr = ht.Addr
}
base, err := url.JoinPath(env.Default("SALTY_BASE_URL", "http://"+addr), "inbox")
if err != nil {
span.RecordError(err)
return err
}
salty, err := salty.New(ctx, eventstore, base)
if err != nil {
span.RecordError(err)
return err
}
svc.Add(salty)
return nil
})

37
cmd/ev/main.go Normal file
View File

@ -0,0 +1,37 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/service"
)
var apps service.Apps
var appName, version = service.AppName()
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
go func() {
<-ctx.Done()
defer cancel() // restore interrupt function
}()
svc := &service.Harness{}
ctx, stop := lg.Init(ctx, appName)
svc.OnStop(stop)
svc.Add(lg.NewHTTP(ctx))
svc.Setup(ctx, apps.Apps()...)
// Run application
if err := svc.Run(ctx, appName, version); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}

54
cmd/ev/svc.es.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"context"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/env"
"github.com/sour-is/ev/pkg/es"
diskstore "github.com/sour-is/ev/pkg/es/driver/disk-store"
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
"github.com/sour-is/ev/pkg/es/driver/projecter"
resolvelinks "github.com/sour-is/ev/pkg/es/driver/resolve-links"
"github.com/sour-is/ev/pkg/es/driver/streamer"
"github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/service"
"go.uber.org/multierr"
)
var _ = apps.Register(10, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
// setup eventstore
err := multierr.Combine(
ev.Init(ctx),
event.Init(ctx),
diskstore.Init(ctx),
memstore.Init(ctx),
)
if err != nil {
span.RecordError(err)
return err
}
eventstore, err := ev.Open(
ctx,
env.Default("EV_DATA", "mem:"),
resolvelinks.New(),
streamer.New(ctx),
projecter.New(
ctx,
projecter.DefaultProjection,
),
)
if err != nil {
span.RecordError(err)
return err
}
svc.Add(eventstore)
svc.Add(&es.EventStore{EventStore: eventstore})
return nil
})

30
cmd/ev/svc.gql.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"context"
"net/http"
"github.com/sour-is/ev/app/gql"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/gql/resolver"
"github.com/sour-is/ev/pkg/mux"
"github.com/sour-is/ev/pkg/service"
"github.com/sour-is/ev/pkg/slice"
)
var _ = apps.Register(90, func(ctx context.Context, svc *service.Harness) error {
ctx, span := lg.Span(ctx)
defer span.End()
span.AddEvent("Enable GraphQL")
gql, err := resolver.New(ctx, &gql.Resolver{}, slice.FilterType[resolver.IsResolver](svc.Services...)...)
if err != nil {
span.RecordError(err)
return err
}
svc.Add(gql, mux.RegisterHTTP(func(mux *http.ServeMux) {
mux.Handle("/", http.RedirectHandler("/playground", http.StatusTemporaryRedirect))
}))
return nil
})

41
cmd/ev/svc.http.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"context"
"log"
"net/http"
"strings"
"github.com/rs/cors"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/env"
"github.com/sour-is/ev/pkg/mux"
"github.com/sour-is/ev/pkg/service"
"github.com/sour-is/ev/pkg/slice"
)
var _ = apps.Register(20, func(ctx context.Context, svc *service.Harness) error {
s := &http.Server{}
svc.Add(s)
mux := mux.New()
s.Handler = cors.AllowAll().Handler(mux)
s.Addr = env.Default("EV_HTTP", ":8080")
if strings.HasPrefix(s.Addr, ":") {
s.Addr = "[::]" + s.Addr
}
svc.OnStart(func(ctx context.Context) error {
_, span := lg.Span(ctx)
defer span.End()
log.Print("Listen on ", s.Addr)
span.AddEvent("begin listen and serve on " + s.Addr)
mux.Add(slice.FilterType[interface{ RegisterHTTP(*http.ServeMux) }](svc.Services...)...)
return s.ListenAndServe()
})
svc.OnStop(s.Shutdown)
return nil
})

1
cmd/msgbus/app.msgbus.go Symbolic link
View File

@ -0,0 +1 @@
../ev/app.msgbus.go

1
cmd/msgbus/main.go Symbolic link
View File

@ -0,0 +1 @@
../ev/main.go

1
cmd/msgbus/svc.es.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.es.go

1
cmd/msgbus/svc.gql.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.gql.go

1
cmd/msgbus/svc.http.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.http.go

1
cmd/peers/app.peerfinder.go Symbolic link
View File

@ -0,0 +1 @@
../ev/app.peerfinder.go

1
cmd/peers/main.go Symbolic link
View File

@ -0,0 +1 @@
../ev/main.go

1
cmd/peers/svc.es.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.es.go

1
cmd/peers/svc.http.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.http.go

1
cmd/salty/app.msgbus.go Symbolic link
View File

@ -0,0 +1 @@
../ev/app.msgbus.go

1
cmd/salty/app.salty.go Symbolic link
View File

@ -0,0 +1 @@
../ev/app.salty.go

1
cmd/salty/main.go Symbolic link
View File

@ -0,0 +1 @@
../ev/main.go

1
cmd/salty/svc.es.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.es.go

1
cmd/salty/svc.http.go Symbolic link
View File

@ -0,0 +1 @@
../ev/svc.http.go

View File

@ -1,5 +1,5 @@
// package es implements an event store and drivers for extending its functionality. // package es implements an event store and drivers for extending its functionality.
package es package ev
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package es_test package ev_test
import ( import (
"context" "context"
@ -11,8 +11,8 @@ import (
"github.com/matryer/is" "github.com/matryer/is"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/sour-is/ev"
"github.com/sour-is/ev/app/peerfinder" "github.com/sour-is/ev/app/peerfinder"
"github.com/sour-is/ev/pkg/es"
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store" memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
"github.com/sour-is/ev/pkg/es/driver/projecter" "github.com/sour-is/ev/pkg/es/driver/projecter"
resolvelinks "github.com/sour-is/ev/pkg/es/driver/resolve-links" resolvelinks "github.com/sour-is/ev/pkg/es/driver/resolve-links"
@ -79,17 +79,17 @@ func TestES(t *testing.T) {
is.NoErr(err) is.NoErr(err)
{ {
store, err := es.Open(ctx, "mem") store, err := ev.Open(ctx, "mem")
is.True(errors.Is(err, es.ErrNoDriver)) is.True(errors.Is(err, ev.ErrNoDriver))
is.True(store.EventStream() == nil) is.True(store.EventStream() == nil)
} }
{ {
_, err := es.Open(ctx, "bogo:") _, err := ev.Open(ctx, "bogo:")
is.True(errors.Is(err, es.ErrNoDriver)) is.True(errors.Is(err, ev.ErrNoDriver))
} }
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx)) store, err := ev.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
is.NoErr(err) is.NoErr(err)
thing := &Thing{Name: "time"} thing := &Thing{Name: "time"}
@ -135,10 +135,10 @@ func TestESOperations(t *testing.T) {
is := is.New(t) is := is.New(t)
ctx := context.Background() ctx := context.Background()
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx)) store, err := ev.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
is.NoErr(err) is.NoErr(err)
thing, err := es.Create(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error { thing, err := ev.Create(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
return agg.OnSetValue("foo") return agg.OnSetValue("foo")
}) })
@ -146,7 +146,7 @@ func TestESOperations(t *testing.T) {
is.Equal(thing.Version(), uint64(1)) is.Equal(thing.Version(), uint64(1))
is.Equal(thing.Value, "foo") is.Equal(thing.Value, "foo")
thing, err = es.Update(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error { thing, err = ev.Update(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
return agg.OnSetValue("bar") return agg.OnSetValue("bar")
}) })
@ -154,7 +154,7 @@ func TestESOperations(t *testing.T) {
is.Equal(thing.Version(), uint64(2)) is.Equal(thing.Version(), uint64(2))
is.Equal(thing.Value, "bar") is.Equal(thing.Value, "bar")
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error { thing, err = ev.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
return agg.OnSetValue("bin") return agg.OnSetValue("bin")
}) })
@ -162,7 +162,7 @@ func TestESOperations(t *testing.T) {
is.Equal(thing.Version(), uint64(1)) is.Equal(thing.Version(), uint64(1))
is.Equal(thing.Value, "bin") is.Equal(thing.Value, "bin")
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error { thing, err = ev.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
return agg.OnSetValue("baz") return agg.OnSetValue("baz")
}) })
@ -178,8 +178,8 @@ func TestUnwrap(t *testing.T) {
err := errors.New("foo") err := errors.New("foo")
werr := fmt.Errorf("wrap: %w", err) werr := fmt.Errorf("wrap: %w", err)
is.Equal(es.Unwrap(werr), err) is.Equal(ev.Unwrap(werr), err)
is.Equal(es.Unwrap("test"), "") is.Equal(ev.Unwrap("test"), "")
} }
func TestUnwrapProjector(t *testing.T) { func TestUnwrapProjector(t *testing.T) {
@ -188,7 +188,7 @@ func TestUnwrapProjector(t *testing.T) {
ctx, stop := context.WithCancel(context.Background()) ctx, stop := context.WithCancel(context.Background())
defer stop() defer stop()
es, err := es.Open( es, err := ev.Open(
ctx, ctx,
"mem:", "mem:",
resolvelinks.New(), resolvelinks.New(),
@ -211,7 +211,7 @@ func TestMain(m *testing.M) {
defer stop() defer stop()
err := multierr.Combine( err := multierr.Combine(
es.Init(ctx), ev.Init(ctx),
event.Init(ctx), event.Init(ctx),
memstore.Init(ctx), memstore.Init(ctx),
) )

View File

@ -14,6 +14,10 @@ model:
filename: internal/graph/model/models_gen.go filename: internal/graph/model/models_gen.go
package: model package: model
resolver:
filename: internal/graph/resolver/resolver.go
package: resolver
models: models:
ID: ID:
model: model:

View File

@ -1,40 +0,0 @@
package main
import (
"net/http"
"github.com/rs/cors"
)
type mux struct {
*http.ServeMux
api *http.ServeMux
}
func httpMux(fns ...interface{ RegisterHTTP(*http.ServeMux) }) http.Handler {
mux := newMux()
for _, fn := range fns {
fn.RegisterHTTP(mux.ServeMux)
if fn, ok := fn.(interface{ RegisterAPIv1(*http.ServeMux) }); ok {
fn.RegisterAPIv1(mux.api)
}
}
return cors.AllowAll().Handler(mux)
}
func newMux() *mux {
mux := &mux{
api: http.NewServeMux(),
ServeMux: http.NewServeMux(),
}
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", mux.api))
return mux
}
type RegisterHTTP func(*http.ServeMux)
func (fn RegisterHTTP) RegisterHTTP(mux *http.ServeMux) {
fn(mux)
}

View File

@ -0,0 +1,63 @@
package resolver
// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
import (
"context"
"github.com/sour-is/ev/app/msgbus"
"github.com/sour-is/ev/app/salty"
"github.com/sour-is/ev/internal/graph/generated"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/gql"
)
type Resolver struct{}
// // foo
func (r *mutationResolver) TruncateStream(ctx context.Context, streamID string, index int64) (bool, error) {
panic("not implemented")
}
// // foo
func (r *mutationResolver) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) {
panic("not implemented")
}
// // foo
func (r *queryResolver) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
panic("not implemented")
}
// // foo
func (r *queryResolver) Posts(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
panic("not implemented")
}
// // foo
func (r *queryResolver) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error) {
panic("not implemented")
}
// // foo
func (r *subscriptionResolver) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *es.GQLEvent, error) {
panic("not implemented")
}
// // foo
func (r *subscriptionResolver) PostAdded(ctx context.Context, streamID string, after int64) (<-chan *msgbus.PostEvent, error) {
panic("not implemented")
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
// Subscription returns generated.SubscriptionResolver implementation.
func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type subscriptionResolver struct{ *Resolver }

View File

@ -9,7 +9,7 @@ import (
"go.uber.org/multierr" "go.uber.org/multierr"
) )
func Init(ctx context.Context, name string) (context.Context, func() error) { func Init(ctx context.Context, name string) (context.Context, func(context.Context) error) {
ctx, span := Span(ctx) ctx, span := Span(ctx)
defer span.End() defer span.End()
@ -21,7 +21,7 @@ func Init(ctx context.Context, name string) (context.Context, func() error) {
reverse(stop[:]) reverse(stop[:])
return ctx, func() error { return ctx, func(context.Context) error {
log.Println("flushing logs...") log.Println("flushing logs...")
errs := make([]error, len(stop)) errs := make([]error, len(stop))
for i, fn := range stop { for i, fn := range stop {

245
main.go
View File

@ -1,245 +0,0 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"runtime/debug"
"strings"
"time"
"go.opentelemetry.io/otel/attribute"
"go.uber.org/multierr"
"golang.org/x/sync/errgroup"
"github.com/sour-is/ev/app/gql"
"github.com/sour-is/ev/app/msgbus"
"github.com/sour-is/ev/app/peerfinder"
"github.com/sour-is/ev/app/salty"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/cron"
"github.com/sour-is/ev/pkg/es"
diskstore "github.com/sour-is/ev/pkg/es/driver/disk-store"
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
"github.com/sour-is/ev/pkg/es/driver/projecter"
resolvelinks "github.com/sour-is/ev/pkg/es/driver/resolve-links"
"github.com/sour-is/ev/pkg/es/driver/streamer"
"github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/gql/resolver"
"github.com/sour-is/ev/pkg/set"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
go func() {
<-ctx.Done()
defer cancel() // restore interrupt function
}()
// Initialize logger
ctx, stop := lg.Init(ctx, appName)
defer stop()
// Run application
if err := run(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
stop := &stopFns{}
cron := cron.New(cron.DefaultGranularity)
{
ctx, span := lg.Span(ctx)
log.Println(appName, version)
span.SetAttributes(
attribute.String("app", appName),
attribute.String("version", version),
)
err := multierr.Combine(
es.Init(ctx),
event.Init(ctx),
diskstore.Init(ctx),
memstore.Init(ctx),
)
if err != nil {
span.RecordError(err)
return err
}
es, err := es.Open(
ctx,
env("EV_DATA", "mem:"),
resolvelinks.New(),
streamer.New(ctx),
projecter.New(
ctx,
projecter.DefaultProjection,
),
)
if err != nil {
span.RecordError(err)
return err
}
s := http.Server{
Addr: env("EV_HTTP", ":8080"),
}
if strings.HasPrefix(s.Addr, ":") {
s.Addr = "[::]" + s.Addr
}
enable := set.New(strings.Fields(env("EV_ENABLE", "salty msgbus gql peers"))...)
var svcs []interface{ RegisterHTTP(*http.ServeMux) }
var res []resolver.IsResolver
res = append(res, es)
if enable.Has("salty") {
span.AddEvent("Enable Salty")
base, err := url.JoinPath(env("EV_BASE_URL", "http://"+s.Addr), "inbox")
if err != nil {
span.RecordError(err)
return err
}
salty, err := salty.New(ctx, es, base)
if err != nil {
span.RecordError(err)
return err
}
svcs = append(svcs, salty)
res = append(res, salty)
}
if enable.Has("msgbus") {
span.AddEvent("Enable Msgbus")
msgbus, err := msgbus.New(ctx, es)
if err != nil {
span.RecordError(err)
return err
}
svcs = append(svcs, msgbus)
res = append(res, msgbus)
}
if enable.Has("peers") {
span.AddEvent("Enable Peers")
es.Option(projecter.New(ctx, peerfinder.Projector))
peers, err := peerfinder.New(ctx, es, env("PEER_STATUS", ""))
if err != nil {
span.RecordError(err)
return err
}
svcs = append(svcs, peers)
cron.Once(ctx, peers.RefreshJob)
cron.NewJob("0,15,30,45", peers.RefreshJob)
cron.Once(ctx, peers.CleanJob)
cron.NewJob("0 1", peers.CleanJob)
g.Go(func() error {
return peers.Run(ctx)
})
stop.add(peers.Stop)
}
if enable.Has("gql") {
span.AddEvent("Enable GraphQL")
gql, err := resolver.New(ctx, &gql.Resolver{}, res...)
if err != nil {
span.RecordError(err)
return err
}
svcs = append(svcs, gql)
}
svcs = append(svcs, lg.NewHTTP(ctx), RegisterHTTP(func(mux *http.ServeMux) {
mux.Handle("/", http.RedirectHandler("/playground", http.StatusTemporaryRedirect))
}))
s.Handler = httpMux(svcs...)
log.Print("Listen on ", s.Addr)
span.AddEvent("begin listen and serve on " + s.Addr)
Mup, err := lg.Meter(ctx).SyncInt64().UpDownCounter("up")
if err != nil {
return err
}
Mup.Add(ctx, 1)
g.Go(s.ListenAndServe)
stop.add(s.Shutdown)
span.End()
}
g.Go(func() error {
<-ctx.Done()
// shutdown jobs
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return stop.stop(ctx)
})
g.Go(func() error {
return cron.Run(ctx)
})
if err := g.Wait(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
func env(name, defaultValue string) string {
name = strings.TrimSpace(name)
defaultValue = strings.TrimSpace(defaultValue)
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
log.Println("#", name, "=", v)
return v
}
log.Println("#", name, "=", defaultValue, "(default)")
return defaultValue
}
var appName, version = func() (string, string) {
if info, ok := debug.ReadBuildInfo(); ok {
_, name, _ := strings.Cut(info.Main.Path, "/")
name = strings.Replace(name, "-", ".", -1)
name = strings.Replace(name, "/", "-", -1)
return name, info.Main.Version
}
return "sour.is-ev", "(devel)"
}()
type application interface {
Setup(context.Context) error
}
type stopFns struct {
fns []func(context.Context) error
}
func (s *stopFns) add(fn func(context.Context) error) {
s.fns = append(s.fns, fn)
}
func (s *stopFns) stop(ctx context.Context) error {
g, _ := errgroup.WithContext(ctx)
for i := range s.fns {
fn := s.fns[i]
g.Go(func() error {
return fn(ctx)
})
}
return g.Wait()
}

3
pkg/README.md Normal file
View File

@ -0,0 +1,3 @@
# Pkg Tools
This is a collection of modules that provide simple reusable functions.

View File

@ -60,7 +60,7 @@ func parseInto(c string, s *set.BoundSet[int8]) *set.BoundSet[int8] {
// 24hour time. Any of the values may be -1 as an "any" match, so passing in // 24hour time. Any of the values may be -1 as an "any" match, so passing in
// a day of -1, the event occurs every day; passing in a second value of -1, the // a day of -1, the event occurs every day; passing in a second value of -1, the
// event will fire every second that the other parameters match. // event will fire every second that the other parameters match.
func (c *cron) NewJob(expr string, task task) { func (c *cron) NewCron(expr string, task func(context.Context, time.Time) error) {
sp := append(strings.Fields(expr), make([]string, 5)...)[:5] sp := append(strings.Fields(expr), make([]string, 5)...)[:5]
job := job{ job := job{
@ -73,7 +73,7 @@ func (c *cron) NewJob(expr string, task task) {
} }
c.jobs = append(c.jobs, job) c.jobs = append(c.jobs, job)
} }
func (c *cron) Once(ctx context.Context, once task) { func (c *cron) RunOnce(ctx context.Context, once func(context.Context, time.Time) error) {
c.state.Modify(ctx, func(ctx context.Context, state *state) error { c.state.Modify(ctx, func(ctx context.Context, state *state) error {
state.queue = append(state.queue, once) state.queue = append(state.queue, once)
return nil return nil

40
pkg/env/env.go vendored Normal file
View File

@ -0,0 +1,40 @@
package env
import (
"log"
"os"
"strings"
)
func Default(name, defaultValue string) string {
name = strings.TrimSpace(name)
defaultValue = strings.TrimSpace(defaultValue)
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
log.Println("# ", name, "=", v)
return v
}
log.Println("# ", name, "=", defaultValue, "(default)")
return defaultValue
}
type secret string
func (s secret) String() string {
if s == "" {
return "(nil)"
}
return "***"
}
func (s secret) Secret() string {
return string(s)
}
func Secret(name, defaultValue string) secret {
name = strings.TrimSpace(name)
defaultValue = strings.TrimSpace(defaultValue)
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
log.Println("# ", name, "=", secret(v))
return secret(v)
}
log.Println("# ", name, "=", secret(defaultValue), "(default)")
return secret(defaultValue)
}

View File

@ -16,9 +16,9 @@ import (
"go.opentelemetry.io/otel/metric/instrument/syncint64" "go.opentelemetry.io/otel/metric/instrument/syncint64"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/cache" "github.com/sour-is/ev/pkg/cache"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver" "github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/locker" "github.com/sour-is/ev/pkg/locker"
@ -41,8 +41,8 @@ type diskStore struct {
m_disk_write syncint64.Counter m_disk_write syncint64.Counter
} }
const AppendOnly = es.AppendOnly const AppendOnly = ev.AppendOnly
const AllEvents = es.AllEvents const AllEvents = ev.AllEvents
func Init(ctx context.Context) error { func Init(ctx context.Context) error {
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
@ -65,7 +65,7 @@ func Init(ctx context.Context) error {
d.m_disk_write, err = m.SyncInt64().Counter("disk_write") d.m_disk_write, err = m.SyncInt64().Counter("disk_write")
errs = multierr.Append(errs, err) errs = multierr.Append(errs, err)
es.Register(ctx, "file", d) ev.Register(ctx, "file", d)
return errs return errs
} }
@ -204,7 +204,7 @@ func (e *eventLog) Append(ctx context.Context, events event.Events, version uint
} }
if version != AppendOnly && version != last { if version != AppendOnly && version != last {
err = fmt.Errorf("%w: current version wrong %d != %d", es.ErrWrongVersion, version, last) err = fmt.Errorf("%w: current version wrong %d != %d", ev.ErrWrongVersion, version, last)
span.RecordError(err) span.RecordError(err)
return err return err
} }
@ -411,7 +411,7 @@ func readStream(ctx context.Context, stream *wal.Log, index uint64) (event.Event
b, err = stream.Read(index) b, err = stream.Read(index)
if err != nil { if err != nil {
if errors.Is(err, wal.ErrNotFound) || errors.Is(err, wal.ErrOutOfRange) { if errors.Is(err, wal.ErrNotFound) || errors.Is(err, wal.ErrOutOfRange) {
err = fmt.Errorf("%w: empty", es.ErrNotFound) err = fmt.Errorf("%w: empty", ev.ErrNotFound)
} }
span.RecordError(err) span.RecordError(err)
@ -444,7 +444,7 @@ func readStreamN(ctx context.Context, stream *wal.Log, index ...uint64) (event.E
b, err = stream.Read(idx) b, err = stream.Read(idx)
if err != nil { if err != nil {
if errors.Is(err, wal.ErrNotFound) || errors.Is(err, wal.ErrOutOfRange) { if errors.Is(err, wal.ErrNotFound) || errors.Is(err, wal.ErrOutOfRange) {
err = fmt.Errorf("%w: empty", es.ErrNotFound) err = fmt.Errorf("%w: empty", ev.ErrNotFound)
} }
span.RecordError(err) span.RecordError(err)

View File

@ -5,8 +5,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver" "github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/locker" "github.com/sour-is/ev/pkg/locker"
@ -24,14 +24,14 @@ type memstore struct {
state *locker.Locked[state] state *locker.Locked[state]
} }
const AppendOnly = es.AppendOnly const AppendOnly = ev.AppendOnly
const AllEvents = es.AllEvents const AllEvents = ev.AllEvents
func Init(ctx context.Context) error { func Init(ctx context.Context) error {
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
defer span.End() defer span.End()
return es.Register(ctx, "mem", &memstore{}) return ev.Register(ctx, "mem", &memstore{})
} }
var _ driver.Driver = (*memstore)(nil) var _ driver.Driver = (*memstore)(nil)
@ -84,7 +84,7 @@ func (m *eventLog) Append(ctx context.Context, events event.Events, version uint
last := uint64(len(*stream)) last := uint64(len(*stream))
if version != AppendOnly && version != last { if version != AppendOnly && version != last {
return fmt.Errorf("%w: current version wrong %d != %d", es.ErrWrongVersion, version, last) return fmt.Errorf("%w: current version wrong %d != %d", ev.ErrWrongVersion, version, last)
} }
for i := range events { for i := range events {

View File

@ -5,8 +5,8 @@ import (
"context" "context"
"strings" "strings"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver" "github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
) )
@ -19,7 +19,7 @@ type projector struct {
func New(_ context.Context, fns ...func(event.Event) []event.Event) *projector { func New(_ context.Context, fns ...func(event.Event) []event.Event) *projector {
return &projector{fns: fns} return &projector{fns: fns}
} }
func (p *projector) Apply(e *es.EventStore) { func (p *projector) Apply(e *ev.EventStore) {
up := e.Driver up := e.Driver
for up != nil { for up != nil {
@ -29,7 +29,7 @@ func (p *projector) Apply(e *es.EventStore) {
return return
} }
up = es.Unwrap(up) up = ev.Unwrap(up)
} }
p.up = e.Driver p.up = e.Driver
@ -112,7 +112,7 @@ func (w *wrapper) Append(ctx context.Context, events event.Events, version uint6
span.RecordError(err) span.RecordError(err)
continue continue
} }
_, err = l.Append(ctx, event.NewEvents(e), es.AppendOnly) _, err = l.Append(ctx, event.NewEvents(e), ev.AppendOnly)
span.RecordError(err) span.RecordError(err)
} }
}() }()

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/matryer/is" "github.com/matryer/is"
"github.com/sour-is/ev/pkg/es" "github.com/sour-is/ev"
"github.com/sour-is/ev/pkg/es/driver" "github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/driver/projecter" "github.com/sour-is/ev/pkg/es/driver/projecter"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
@ -112,10 +112,10 @@ func TestProjecter(t *testing.T) {
return mockEL, nil return mockEL, nil
} }
es.Init(ctx) ev.Init(ctx)
es.Register(ctx, "mock", mock) ev.Register(ctx, "mock", mock)
es, err := es.Open( es, err := ev.Open(
ctx, ctx,
"mock:", "mock:",
projecter.New(ctx, projecter.DefaultProjection), projecter.New(ctx, projecter.DefaultProjection),

View File

@ -4,8 +4,8 @@ import (
"context" "context"
"errors" "errors"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver" "github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
) )
@ -18,7 +18,7 @@ func New() *resolvelinks {
return &resolvelinks{} return &resolvelinks{}
} }
func (r *resolvelinks) Apply(es *es.EventStore) { func (r *resolvelinks) Apply(es *ev.EventStore) {
r.up = es.Driver r.up = es.Driver
es.Driver = r es.Driver = r
} }
@ -77,7 +77,7 @@ func (w *wrapper) Read(ctx context.Context, after int64, count int64) (event.Eve
} }
ptr := ptrs[streamID] ptr := ptrs[streamID]
lis, err := d.ReadN(ctx, ids...) lis, err := d.ReadN(ctx, ids...)
if err != nil && !errors.Is(err, es.ErrNotFound) { if err != nil && !errors.Is(err, ev.ErrNotFound) {
return nil, err return nil, err
} }

View File

@ -9,8 +9,8 @@ import (
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es"
"github.com/sour-is/ev/pkg/es/driver" "github.com/sour-is/ev/pkg/es/driver"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/locker" "github.com/sour-is/ev/pkg/locker"
@ -32,9 +32,9 @@ func New(ctx context.Context) *streamer {
return &streamer{state: locker.New(&state{subscribers: map[string][]*subscription{}})} return &streamer{state: locker.New(&state{subscribers: map[string][]*subscription{}})}
} }
var _ es.Option = (*streamer)(nil) var _ ev.Option = (*streamer)(nil)
func (s *streamer) Apply(e *es.EventStore) { func (s *streamer) Apply(e *ev.EventStore) {
s.up = e.Driver s.up = e.Driver
e.Driver = s e.Driver = s
} }
@ -72,7 +72,7 @@ func (s *streamer) Subscribe(ctx context.Context, streamID string, start int64)
sub := &subscription{topic: streamID, events: events} sub := &subscription{topic: streamID, events: events}
sub.position = locker.New(&position{ sub.position = locker.New(&position{
idx: start, idx: start,
size: es.AllEvents, size: ev.AllEvents,
}) })
sub.unsub = s.delete(streamID, sub) sub.unsub = s.delete(streamID, sub)
@ -232,7 +232,7 @@ func (s *subscription) Recv(ctx context.Context) <-chan bool {
_, span := lg.Span(ctx) _, span := lg.Span(ctx)
defer span.End() defer span.End()
if position.size == es.AllEvents { if position.size == ev.AllEvents {
return nil return nil
} }
if position.size == 0 { if position.size == 0 {

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/sour-is/ev"
"github.com/sour-is/ev/internal/lg" "github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/es/event" "github.com/sour-is/ev/pkg/es/event"
"github.com/sour-is/ev/pkg/gql" "github.com/sour-is/ev/pkg/gql"
@ -23,13 +24,17 @@ type contextKey struct {
var esKey = contextKey{"event-store"} var esKey = contextKey{"event-store"}
type EventStore struct {
*ev.EventStore
}
func (es *EventStore) IsResolver() {} func (es *EventStore) IsResolver() {}
func (es *EventStore) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) { func (es *EventStore) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
ctx, span := lg.Span(ctx) ctx, span := lg.Span(ctx)
defer span.End() defer span.End()
lis, err := es.Read(ctx, streamID, paging.GetIdx(0), paging.GetCount(30)) lis, err := es.Read(ctx, streamID, paging.GetIdx(0), paging.GetCount(30))
if err != nil && !errors.Is(err, ErrNotFound) { if err != nil && !errors.Is(err, ev.ErrNotFound) {
span.RecordError(err) span.RecordError(err)
return nil, err return nil, err
} }

View File

@ -52,12 +52,14 @@ outer:
rs := reflect.ValueOf(resolvers[i]) rs := reflect.ValueOf(resolvers[i])
if field.IsNil() && rs.Type().Implements(field.Type()) { if field.IsNil() && rs.Type().Implements(field.Type()) {
// log.Print("found ", field.Type().Name())
span.AddEvent(fmt.Sprint("found ", field.Type().Name())) span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
field.Set(rs) field.Set(rs)
continue outer continue outer
} }
} }
// log.Print(fmt.Sprint("default ", field.Type().Name()))
span.AddEvent(fmt.Sprint("default ", field.Type().Name())) span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
field.Set(noop) field.Set(noop)
} }

45
pkg/mux/httpmux.go Normal file
View File

@ -0,0 +1,45 @@
package mux
import (
"net/http"
)
type mux struct {
*http.ServeMux
api *http.ServeMux
wellknown *http.ServeMux
}
func (mux *mux) Add(fns ...interface{ RegisterHTTP(*http.ServeMux) }) {
for _, fn := range fns {
// log.Printf("HTTP: %T", fn)
fn.RegisterHTTP(mux.ServeMux)
if fn, ok := fn.(interface{ RegisterAPIv1(*http.ServeMux) }); ok {
// log.Printf("APIv1: %T", fn)
fn.RegisterAPIv1(mux.api)
}
if fn, ok := fn.(interface{ RegisterWellKnown(*http.ServeMux) }); ok {
// log.Printf("APIv1: %T", fn)
fn.RegisterWellKnown(mux.wellknown)
}
}
}
func New() *mux {
mux := &mux{
api: http.NewServeMux(),
wellknown: http.NewServeMux(),
ServeMux: http.NewServeMux(),
}
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", mux.api))
mux.Handle("/.well-known/", http.StripPrefix("/.well-known/", mux.api))
return mux
}
type RegisterHTTP func(*http.ServeMux)
func (fn RegisterHTTP) RegisterHTTP(mux *http.ServeMux) {
fn(mux)
}

View File

@ -1,4 +1,4 @@
package main package mux_test
import ( import (
"net/http" "net/http"
@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/matryer/is" "github.com/matryer/is"
"github.com/sour-is/ev/pkg/mux"
) )
type mockHTTP struct { type mockHTTP struct {
@ -15,7 +16,6 @@ type mockHTTP struct {
func (m *mockHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m *mockHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.onServeHTTP() m.onServeHTTP()
} }
func (h *mockHTTP) RegisterHTTP(mux *http.ServeMux) { func (h *mockHTTP) RegisterHTTP(mux *http.ServeMux) {
mux.Handle("/", h) mux.Handle("/", h)
} }
@ -28,7 +28,8 @@ func TestHttpMux(t *testing.T) {
called := false called := false
mux := httpMux(&mockHTTP{func() { called = true }}) mux := mux.New()
mux.Add(&mockHTTP{func() { called = true }})
is.True(mux != nil) is.True(mux != nil)

169
pkg/service/service.go Normal file
View File

@ -0,0 +1,169 @@
package service
import (
"context"
"log"
"net/http"
"runtime/debug"
"sort"
"strings"
"time"
"github.com/sour-is/ev/internal/lg"
"github.com/sour-is/ev/pkg/cron"
"go.opentelemetry.io/otel/attribute"
"go.uber.org/multierr"
"golang.org/x/sync/errgroup"
)
type crontab interface {
NewCron(expr string, task func(context.Context, time.Time) error)
RunOnce(ctx context.Context, once func(context.Context, time.Time) error)
}
type Harness struct {
crontab
Services []any
onStart []func(context.Context) error
onStop []func(context.Context) error
}
func (s *Harness) Setup(ctx context.Context, apps ...application) error {
ctx, span := lg.Span(ctx)
defer span.End()
// setup crontab
c := cron.New(cron.DefaultGranularity)
s.OnStart(c.Run)
s.crontab = c
var err error
for _, app := range apps {
err = multierr.Append(err, app(ctx, s))
}
span.RecordError(err)
return err
}
func (s *Harness) OnStart(fn func(context.Context) error) {
s.onStart = append(s.onStart, fn)
}
func (s *Harness) OnStop(fn func(context.Context) error) {
s.onStop = append(s.onStop, fn)
}
func (s *Harness) Add(svcs ...any) {
s.Services = append(s.Services, svcs...)
}
func (s *Harness) stop(ctx context.Context) error {
g, _ := errgroup.WithContext(ctx)
for i := range s.onStop {
fn := s.onStop[i]
g.Go(func() error {
if err := fn(ctx); err != nil && err != http.ErrServerClosed {
return err
}
return nil
})
}
return g.Wait()
}
func (s *Harness) Run(ctx context.Context, appName, version string) error {
{
ctx, span := lg.Span(ctx)
log.Println(appName, version)
span.SetAttributes(
attribute.String("app", appName),
attribute.String("version", version),
)
Mup, err := lg.Meter(ctx).SyncInt64().UpDownCounter("up")
if err != nil {
return err
}
Mup.Add(ctx, 1)
span.End()
}
g, _ := errgroup.WithContext(ctx)
g.Go(func() error {
<-ctx.Done()
// shutdown jobs
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return s.stop(ctx)
})
for i := range s.onStart {
fn := s.onStart[i]
g.Go(func() error { return fn(ctx) })
}
return g.Wait()
}
type application func(context.Context, *Harness) error // Len is the number of elements in the collection.
type appscore struct {
score int
application
}
type Apps []appscore
func (a *Apps) Apps() []application {
sort.Sort(a)
lis := make([]application, len(*a))
for i, app := range *a {
lis[i] = app.application
}
return lis
}
// Len is the number of elements in the collection.
func (a *Apps) Len() int {
if a == nil {
return 0
}
return len(*a)
}
// Less reports whether the element with index i
func (a *Apps) Less(i int, j int) bool {
if a == nil {
return false
}
return (*a)[i].score < (*a)[j].score
}
// Swap swaps the elements with indexes i and j.
func (a *Apps) Swap(i int, j int) {
if a == nil {
return
}
(*a)[i], (*a)[j] = (*a)[j], (*a)[i]
}
func (a *Apps) Register(score int, app application) (none struct{}) {
if a == nil {
return
}
*a = append(*a, appscore{score, app})
return
}
func AppName() (string, string) {
if info, ok := debug.ReadBuildInfo(); ok {
_, name, _ := strings.Cut(info.Main.Path, "/")
name = strings.Replace(name, "-", ".", -1)
name = strings.Replace(name, "/", "-", -1)
return name, info.Main.Version
}
return "sour.is-app", "(devel)"
}

35
pkg/slice/slice.go Normal file
View File

@ -0,0 +1,35 @@
package slice
// FilterType returns a subset that matches the type.
func FilterType[T any](in ...any) []T {
lis := make([]T, 0, len(in))
for _, u := range in {
if t, ok := u.(T); ok {
lis = append(lis, t)
}
}
return lis
}
// Find returns the first of type found. or false if not found.
func Find[T any](in ...any) (T, bool) {
return First(FilterType[T](in...)...)
}
// First returns the first element in a slice.
func First[T any](in ...T) (T, bool) {
if len(in) == 0 {
var zero T
return zero, false
}
return in[0], true
}
// Map applys func to each element s and returns results as slice.
func Map[T, U any](s []T, f func(T) U) []U {
r := make([]U, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}