refactor: push commands in to cmd and ev to root as library
This commit is contained in:
3
pkg/README.md
Normal file
3
pkg/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Pkg Tools
|
||||
|
||||
This is a collection of modules that provide simple reusable functions.
|
||||
@@ -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
|
||||
// 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.
|
||||
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]
|
||||
|
||||
job := job{
|
||||
@@ -73,7 +73,7 @@ func (c *cron) NewJob(expr string, task task) {
|
||||
}
|
||||
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 {
|
||||
state.queue = append(state.queue, once)
|
||||
return nil
|
||||
|
||||
40
pkg/env/env.go
vendored
Normal file
40
pkg/env/env.go
vendored
Normal 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)
|
||||
}
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"github.com/sour-is/ev"
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"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/event"
|
||||
"github.com/sour-is/ev/pkg/locker"
|
||||
@@ -41,8 +41,8 @@ type diskStore struct {
|
||||
m_disk_write syncint64.Counter
|
||||
}
|
||||
|
||||
const AppendOnly = es.AppendOnly
|
||||
const AllEvents = es.AllEvents
|
||||
const AppendOnly = ev.AppendOnly
|
||||
const AllEvents = ev.AllEvents
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
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")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
es.Register(ctx, "file", d)
|
||||
ev.Register(ctx, "file", d)
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func (e *eventLog) Append(ctx context.Context, events event.Events, version uint
|
||||
}
|
||||
|
||||
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)
|
||||
return err
|
||||
}
|
||||
@@ -411,7 +411,7 @@ func readStream(ctx context.Context, stream *wal.Log, index uint64) (event.Event
|
||||
b, err = stream.Read(index)
|
||||
if err != nil {
|
||||
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)
|
||||
@@ -444,7 +444,7 @@ func readStreamN(ctx context.Context, stream *wal.Log, index ...uint64) (event.E
|
||||
b, err = stream.Read(idx)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sour-is/ev"
|
||||
"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/event"
|
||||
"github.com/sour-is/ev/pkg/locker"
|
||||
@@ -24,14 +24,14 @@ type memstore struct {
|
||||
state *locker.Locked[state]
|
||||
}
|
||||
|
||||
const AppendOnly = es.AppendOnly
|
||||
const AllEvents = es.AllEvents
|
||||
const AppendOnly = ev.AppendOnly
|
||||
const AllEvents = ev.AllEvents
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return es.Register(ctx, "mem", &memstore{})
|
||||
return ev.Register(ctx, "mem", &memstore{})
|
||||
}
|
||||
|
||||
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))
|
||||
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 {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/sour-is/ev"
|
||||
"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/event"
|
||||
)
|
||||
@@ -19,7 +19,7 @@ type projector struct {
|
||||
func New(_ context.Context, fns ...func(event.Event) []event.Event) *projector {
|
||||
return &projector{fns: fns}
|
||||
}
|
||||
func (p *projector) Apply(e *es.EventStore) {
|
||||
func (p *projector) Apply(e *ev.EventStore) {
|
||||
|
||||
up := e.Driver
|
||||
for up != nil {
|
||||
@@ -29,7 +29,7 @@ func (p *projector) Apply(e *es.EventStore) {
|
||||
return
|
||||
}
|
||||
|
||||
up = es.Unwrap(up)
|
||||
up = ev.Unwrap(up)
|
||||
}
|
||||
|
||||
p.up = e.Driver
|
||||
@@ -112,7 +112,7 @@ func (w *wrapper) Append(ctx context.Context, events event.Events, version uint6
|
||||
span.RecordError(err)
|
||||
continue
|
||||
}
|
||||
_, err = l.Append(ctx, event.NewEvents(e), es.AppendOnly)
|
||||
_, err = l.Append(ctx, event.NewEvents(e), ev.AppendOnly)
|
||||
span.RecordError(err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"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/projecter"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
@@ -112,10 +112,10 @@ func TestProjecter(t *testing.T) {
|
||||
return mockEL, nil
|
||||
}
|
||||
|
||||
es.Init(ctx)
|
||||
es.Register(ctx, "mock", mock)
|
||||
ev.Init(ctx)
|
||||
ev.Register(ctx, "mock", mock)
|
||||
|
||||
es, err := es.Open(
|
||||
es, err := ev.Open(
|
||||
ctx,
|
||||
"mock:",
|
||||
projecter.New(ctx, projecter.DefaultProjection),
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/sour-is/ev"
|
||||
"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/event"
|
||||
)
|
||||
@@ -18,7 +18,7 @@ func New() *resolvelinks {
|
||||
return &resolvelinks{}
|
||||
}
|
||||
|
||||
func (r *resolvelinks) Apply(es *es.EventStore) {
|
||||
func (r *resolvelinks) Apply(es *ev.EventStore) {
|
||||
r.up = es.Driver
|
||||
es.Driver = r
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (w *wrapper) Read(ctx context.Context, after int64, count int64) (event.Eve
|
||||
}
|
||||
ptr := ptrs[streamID]
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/sour-is/ev"
|
||||
"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/event"
|
||||
"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{}})}
|
||||
}
|
||||
|
||||
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
|
||||
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.position = locker.New(&position{
|
||||
idx: start,
|
||||
size: es.AllEvents,
|
||||
size: ev.AllEvents,
|
||||
})
|
||||
sub.unsub = s.delete(streamID, sub)
|
||||
|
||||
@@ -232,7 +232,7 @@ func (s *subscription) Recv(ctx context.Context) <-chan bool {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if position.size == es.AllEvents {
|
||||
if position.size == ev.AllEvents {
|
||||
return nil
|
||||
}
|
||||
if position.size == 0 {
|
||||
|
||||
415
pkg/es/es.go
415
pkg/es/es.go
@@ -1,415 +0,0 @@
|
||||
// package es implements an event store and drivers for extending its functionality.
|
||||
package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/es/driver"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
"github.com/sour-is/ev/pkg/locker"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
drivers map[string]driver.Driver
|
||||
}
|
||||
|
||||
const AppendOnly = ^uint64(0)
|
||||
const AllEvents = int64(AppendOnly >> 1)
|
||||
|
||||
var (
|
||||
drivers = locker.New(&config{drivers: make(map[string]driver.Driver)})
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
m := lg.Meter(ctx)
|
||||
|
||||
var err, errs error
|
||||
Mes_open, err = m.SyncInt64().Counter("es_open")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
Mes_read, err = m.SyncInt64().Counter("es_read")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
Mes_load, err = m.SyncInt64().Counter("es_load")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
Mes_save, err = m.SyncInt64().Counter("es_save")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
Mes_append, err = m.SyncInt64().Counter("es_append")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func Register(ctx context.Context, name string, d driver.Driver) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return drivers.Modify(ctx, func(ctx context.Context, c *config) error {
|
||||
if _, set := c.drivers[name]; set {
|
||||
return fmt.Errorf("driver %s already set", name)
|
||||
}
|
||||
c.drivers[name] = d
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type EventStore struct {
|
||||
driver.Driver
|
||||
}
|
||||
|
||||
var (
|
||||
Mes_open syncint64.Counter
|
||||
Mes_read syncint64.Counter
|
||||
Mes_load syncint64.Counter
|
||||
Mes_save syncint64.Counter
|
||||
Mes_append syncint64.Counter
|
||||
)
|
||||
|
||||
func Open(ctx context.Context, dsn string, options ...Option) (*EventStore, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(attribute.String("dsn", dsn))
|
||||
|
||||
name, _, ok := strings.Cut(dsn, ":")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: no scheme", ErrNoDriver)
|
||||
}
|
||||
|
||||
var d driver.Driver
|
||||
c, err := drivers.Copy(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d, ok = c.drivers[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %s not registered", ErrNoDriver, name)
|
||||
}
|
||||
|
||||
conn, err := d.Open(ctx, dsn)
|
||||
|
||||
es := &EventStore{Driver: conn}
|
||||
es.Option(options...)
|
||||
|
||||
Mes_open.Add(ctx, 1)
|
||||
|
||||
return es, err
|
||||
}
|
||||
|
||||
func (es *EventStore) Option(options ...Option) {
|
||||
for _, o := range options {
|
||||
o.Apply(es)
|
||||
}
|
||||
}
|
||||
|
||||
type Option interface {
|
||||
Apply(*EventStore)
|
||||
}
|
||||
|
||||
func (es *EventStore) Save(ctx context.Context, agg event.Aggregate) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
events := agg.Events(true)
|
||||
if len(events) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("agg.type", event.TypeOf(agg)),
|
||||
attribute.String("agg.streamID", agg.StreamID()),
|
||||
attribute.Int64("agg.version", int64(agg.StreamVersion())),
|
||||
)
|
||||
|
||||
l, err := es.EventLog(ctx, agg.StreamID())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count, err := l.Append(ctx, events, agg.StreamVersion())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
Mes_save.Add(ctx, int64(count))
|
||||
|
||||
agg.Commit()
|
||||
return count, err
|
||||
}
|
||||
func (es *EventStore) Load(ctx context.Context, agg event.Aggregate) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("agg.type", event.TypeOf(agg)),
|
||||
attribute.String("agg.streamID", agg.StreamID()),
|
||||
)
|
||||
|
||||
l, err := es.Driver.EventLog(ctx, agg.StreamID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
events, err := l.Read(ctx, 0, AllEvents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Mes_load.Add(ctx, events.Count())
|
||||
event.Append(agg, events...)
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int64("agg.version", int64(agg.StreamVersion())),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
func (es *EventStore) Read(ctx context.Context, streamID string, after, count int64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("streamID", streamID),
|
||||
attribute.Int64("after", after),
|
||||
attribute.Int64("count", count),
|
||||
)
|
||||
|
||||
l, err := es.Driver.EventLog(ctx, streamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events, err := l.Read(ctx, after, count)
|
||||
Mes_read.Add(ctx, events.Count())
|
||||
|
||||
return events, err
|
||||
}
|
||||
func (es *EventStore) ReadN(ctx context.Context, streamID string, index ...uint64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
lis := make([]int64, len(index))
|
||||
for i, j := range index {
|
||||
lis[i] = int64(j)
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("streamID", streamID),
|
||||
attribute.Int64Slice("index", lis),
|
||||
)
|
||||
|
||||
l, err := es.Driver.EventLog(ctx, streamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events, err := l.ReadN(ctx, index...)
|
||||
Mes_read.Add(ctx, events.Count())
|
||||
|
||||
return events, err
|
||||
}
|
||||
func (es *EventStore) Append(ctx context.Context, streamID string, events event.Events) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
Mes_append.Add(ctx, 1)
|
||||
span.SetAttributes(
|
||||
attribute.String("ev.streamID", streamID),
|
||||
)
|
||||
|
||||
l, err := es.Driver.EventLog(ctx, streamID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return l.Append(ctx, events, AppendOnly)
|
||||
}
|
||||
func (es *EventStore) FirstIndex(ctx context.Context, streamID string) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("ev.streamID", streamID),
|
||||
)
|
||||
|
||||
l, err := es.Driver.EventLog(ctx, streamID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return l.FirstIndex(ctx)
|
||||
}
|
||||
func (es *EventStore) LastIndex(ctx context.Context, streamID string) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("ev.streamID", streamID),
|
||||
)
|
||||
|
||||
l, err := es.Driver.EventLog(ctx, streamID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return l.LastIndex(ctx)
|
||||
}
|
||||
func (es *EventStore) EventStream() driver.EventStream {
|
||||
if es == nil {
|
||||
return nil
|
||||
}
|
||||
d := es.Driver
|
||||
for d != nil {
|
||||
if d, ok := d.(driver.EventStream); ok {
|
||||
return d
|
||||
}
|
||||
|
||||
d = Unwrap(d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (es *EventStore) Truncate(ctx context.Context, streamID string, index int64) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
up, err := es.Driver.EventLog(ctx, streamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for up != nil {
|
||||
if up, ok := up.(driver.EventLogWithTruncate); ok {
|
||||
err = up.Truncate(ctx, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
up = Unwrap(up)
|
||||
}
|
||||
|
||||
return ErrNoDriver
|
||||
}
|
||||
|
||||
func Unwrap[T any](t T) T {
|
||||
if unwrap, ok := any(t).(interface{ Unwrap() T }); ok {
|
||||
return unwrap.Unwrap()
|
||||
} else {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
}
|
||||
|
||||
var ErrNoDriver = errors.New("no driver")
|
||||
var ErrWrongVersion = errors.New("wrong version")
|
||||
var ErrShouldExist = event.ErrShouldExist
|
||||
var ErrShouldNotExist = event.ErrShouldNotExist
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
type PA[T any] interface {
|
||||
event.Aggregate
|
||||
*T
|
||||
}
|
||||
type PE[T any] interface {
|
||||
event.Event
|
||||
*T
|
||||
}
|
||||
|
||||
// Create uses fn to create a new aggregate and store in db.
|
||||
func Create[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
agg = new(A)
|
||||
agg.SetStreamID(streamID)
|
||||
span.SetAttributes(
|
||||
attribute.String("agg.streamID", streamID),
|
||||
)
|
||||
|
||||
if err = es.Load(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = event.NotExists(agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = fn(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var i uint64
|
||||
if i, err = es.Save(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("wrote events = ", i))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Update uses fn to update an exsisting aggregate and store in db.
|
||||
func Update[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
agg = new(A)
|
||||
agg.SetStreamID(streamID)
|
||||
span.SetAttributes(
|
||||
attribute.String("agg.streamID", streamID),
|
||||
)
|
||||
|
||||
if err = es.Load(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = event.ShouldExist(agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = fn(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = es.Save(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Update uses fn to update an exsisting aggregate and store in db.
|
||||
func Upsert[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
agg = new(A)
|
||||
agg.SetStreamID(streamID)
|
||||
span.SetAttributes(
|
||||
attribute.String("agg.streamID", streamID),
|
||||
)
|
||||
|
||||
if err = es.Load(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = fn(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = event.ShouldExist(agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = es.Save(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
package es_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
_ event.Event = (*ValueSet)(nil)
|
||||
_ event.Aggregate = (*Thing)(nil)
|
||||
)
|
||||
|
||||
type Thing struct {
|
||||
Name string
|
||||
Value string
|
||||
|
||||
event.AggregateRoot
|
||||
}
|
||||
|
||||
func (a *Thing) ApplyEvent(lis ...event.Event) {
|
||||
for _, e := range lis {
|
||||
switch e := e.(type) {
|
||||
case *ValueSet:
|
||||
a.Value = e.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
func (a *Thing) OnSetValue(value string) error {
|
||||
event.Raise(a, &ValueSet{Value: value})
|
||||
return nil
|
||||
}
|
||||
|
||||
type ValueSet struct {
|
||||
Value string
|
||||
|
||||
eventMeta event.Meta
|
||||
}
|
||||
|
||||
func (e *ValueSet) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
func (e *ValueSet) SetEventMeta(eventMeta event.Meta) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.eventMeta = eventMeta
|
||||
}
|
||||
func (e *ValueSet) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
func (e *ValueSet) UnmarshalBinary(b []byte) error {
|
||||
return json.Unmarshal(b, e)
|
||||
}
|
||||
|
||||
func TestES(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err := event.Register(ctx, &ValueSet{})
|
||||
is.NoErr(err)
|
||||
|
||||
{
|
||||
store, err := es.Open(ctx, "mem")
|
||||
is.True(errors.Is(err, es.ErrNoDriver))
|
||||
is.True(store.EventStream() == nil)
|
||||
}
|
||||
|
||||
{
|
||||
_, err := es.Open(ctx, "bogo:")
|
||||
is.True(errors.Is(err, es.ErrNoDriver))
|
||||
}
|
||||
|
||||
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
||||
is.NoErr(err)
|
||||
|
||||
thing := &Thing{Name: "time"}
|
||||
err = store.Load(ctx, thing)
|
||||
is.NoErr(err)
|
||||
|
||||
t.Log(thing.StreamVersion(), thing.Name, thing.Value)
|
||||
|
||||
err = thing.OnSetValue(time.Now().String())
|
||||
is.NoErr(err)
|
||||
|
||||
thing.SetStreamID("thing-time")
|
||||
i, err := store.Save(ctx, thing)
|
||||
is.NoErr(err)
|
||||
|
||||
t.Log(thing.StreamVersion(), thing.Name, thing.Value)
|
||||
t.Log("Wrote: ", i)
|
||||
|
||||
i, err = store.Append(ctx, "thing-time", event.NewEvents(&ValueSet{Value: "xxx"}))
|
||||
is.NoErr(err)
|
||||
is.Equal(i, uint64(1))
|
||||
|
||||
events, err := store.Read(ctx, "thing-time", -1, -11)
|
||||
is.NoErr(err)
|
||||
|
||||
for i, e := range events {
|
||||
t.Logf("event %d %d - %v\n", i, e.EventMeta().Position, e)
|
||||
}
|
||||
|
||||
first, err := store.FirstIndex(ctx, "thing-time")
|
||||
is.NoErr(err)
|
||||
is.Equal(first, uint64(1))
|
||||
|
||||
last, err := store.LastIndex(ctx, "thing-time")
|
||||
is.NoErr(err)
|
||||
is.Equal(last, uint64(2))
|
||||
|
||||
stream := store.EventStream()
|
||||
is.True(stream != nil)
|
||||
}
|
||||
|
||||
func TestESOperations(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
||||
is.NoErr(err)
|
||||
|
||||
thing, err := es.Create(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
||||
return agg.OnSetValue("foo")
|
||||
})
|
||||
|
||||
is.NoErr(err)
|
||||
is.Equal(thing.Version(), uint64(1))
|
||||
is.Equal(thing.Value, "foo")
|
||||
|
||||
thing, err = es.Update(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
||||
return agg.OnSetValue("bar")
|
||||
})
|
||||
|
||||
is.NoErr(err)
|
||||
is.Equal(thing.Version(), uint64(2))
|
||||
is.Equal(thing.Value, "bar")
|
||||
|
||||
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
||||
return agg.OnSetValue("bin")
|
||||
})
|
||||
|
||||
is.NoErr(err)
|
||||
is.Equal(thing.Version(), uint64(1))
|
||||
is.Equal(thing.Value, "bin")
|
||||
|
||||
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
||||
return agg.OnSetValue("baz")
|
||||
})
|
||||
|
||||
is.NoErr(err)
|
||||
is.Equal(thing.Version(), uint64(2))
|
||||
is.Equal(thing.Value, "baz")
|
||||
|
||||
}
|
||||
|
||||
func TestUnwrap(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
err := errors.New("foo")
|
||||
werr := fmt.Errorf("wrap: %w", err)
|
||||
|
||||
is.Equal(es.Unwrap(werr), err)
|
||||
is.Equal(es.Unwrap("test"), "")
|
||||
}
|
||||
|
||||
func TestUnwrapProjector(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
defer stop()
|
||||
|
||||
es, err := es.Open(
|
||||
ctx,
|
||||
"mem:",
|
||||
resolvelinks.New(),
|
||||
streamer.New(ctx),
|
||||
projecter.New(
|
||||
ctx,
|
||||
projecter.DefaultProjection,
|
||||
peerfinder.Projector,
|
||||
),
|
||||
)
|
||||
is.NoErr(err)
|
||||
|
||||
stream := es.EventStream()
|
||||
is.True(stream != nil)
|
||||
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
defer stop()
|
||||
|
||||
err := multierr.Combine(
|
||||
es.Init(ctx),
|
||||
event.Init(ctx),
|
||||
memstore.Init(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
m.Run()
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sour-is/ev"
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
"github.com/sour-is/ev/pkg/gql"
|
||||
@@ -23,13 +24,17 @@ type contextKey struct {
|
||||
|
||||
var esKey = contextKey{"event-store"}
|
||||
|
||||
type EventStore struct {
|
||||
*ev.EventStore
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -52,12 +52,14 @@ outer:
|
||||
rs := reflect.ValueOf(resolvers[i])
|
||||
|
||||
if field.IsNil() && rs.Type().Implements(field.Type()) {
|
||||
// log.Print("found ", field.Type().Name())
|
||||
span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
|
||||
field.Set(rs)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
// log.Print(fmt.Sprint("default ", field.Type().Name()))
|
||||
span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
|
||||
field.Set(noop)
|
||||
}
|
||||
|
||||
45
pkg/mux/httpmux.go
Normal file
45
pkg/mux/httpmux.go
Normal 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)
|
||||
}
|
||||
41
pkg/mux/httpmux_test.go
Normal file
41
pkg/mux/httpmux_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package mux_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"github.com/sour-is/ev/pkg/mux"
|
||||
)
|
||||
|
||||
type mockHTTP struct {
|
||||
onServeHTTP func()
|
||||
}
|
||||
|
||||
func (m *mockHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
m.onServeHTTP()
|
||||
}
|
||||
func (h *mockHTTP) RegisterHTTP(mux *http.ServeMux) {
|
||||
mux.Handle("/", h)
|
||||
}
|
||||
func (h *mockHTTP) RegisterAPIv1(mux *http.ServeMux) {
|
||||
mux.Handle("/ping", h)
|
||||
}
|
||||
|
||||
func TestHttpMux(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
called := false
|
||||
|
||||
mux := mux.New()
|
||||
mux.Add(&mockHTTP{func() { called = true }})
|
||||
|
||||
is.True(mux != nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/ping", nil)
|
||||
mux.ServeHTTP(w, r)
|
||||
|
||||
is.True(called)
|
||||
}
|
||||
169
pkg/service/service.go
Normal file
169
pkg/service/service.go
Normal 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
35
pkg/slice/slice.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user