initial commit
This commit is contained in:
		
						commit
						1010657a02
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					.vscode/
 | 
				
			||||||
 | 
					data/
 | 
				
			||||||
							
								
								
									
										17
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					module github.com/sour-is/ev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/tidwall/wal v1.1.7
 | 
				
			||||||
 | 
						golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/matryer/is v1.4.0
 | 
				
			||||||
 | 
						github.com/oklog/ulid/v2 v2.1.0
 | 
				
			||||||
 | 
						github.com/tidwall/gjson v1.10.2 // indirect
 | 
				
			||||||
 | 
						github.com/tidwall/match v1.1.1 // indirect
 | 
				
			||||||
 | 
						github.com/tidwall/pretty v1.2.0 // indirect
 | 
				
			||||||
 | 
						github.com/tidwall/tinylru v1.1.0 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										17
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
 | 
				
			||||||
 | 
					github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 | 
				
			||||||
 | 
					github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
 | 
				
			||||||
 | 
					github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
 | 
				
			||||||
 | 
					github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
 | 
				
			||||||
 | 
					github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
 | 
				
			||||||
 | 
					github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 | 
				
			||||||
 | 
					github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 | 
				
			||||||
 | 
					github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 | 
				
			||||||
 | 
					github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 | 
				
			||||||
 | 
					github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 | 
				
			||||||
 | 
					github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I=
 | 
				
			||||||
 | 
					github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8=
 | 
				
			||||||
 | 
					github.com/tidwall/wal v1.1.7 h1:emc1TRjIVsdKKSnpwGBAcsAGg0767SvUk8+ygx7Bb+4=
 | 
				
			||||||
 | 
					github.com/tidwall/wal v1.1.7/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
							
								
								
									
										185
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,185 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/sync/errgroup"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/es"
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/es/driver"
 | 
				
			||||||
 | 
						ds_driver "github.com/sour-is/ev/pkg/es/driver/disk-store"
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/es/event"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
						ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							<-ctx.Done()
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := run(ctx); err != nil {
 | 
				
			||||||
 | 
							log.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func run(ctx context.Context) error {
 | 
				
			||||||
 | 
						event.Register(&PostEvent{})
 | 
				
			||||||
 | 
						ds_driver.Init(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						es, err := es.Open(ctx, "file:data")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						svc := &service{
 | 
				
			||||||
 | 
							es: es,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s := http.Server{
 | 
				
			||||||
 | 
							Addr: ":8080",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						http.HandleFunc("/event/", svc.event)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Print("Listen on ", s.Addr)
 | 
				
			||||||
 | 
						g, ctx := errgroup.WithContext(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.Go(s.ListenAndServe)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.Go(func() error {
 | 
				
			||||||
 | 
							<-ctx.Done()
 | 
				
			||||||
 | 
							ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
							defer cancel()
 | 
				
			||||||
 | 
							log.Print("shutdown http")
 | 
				
			||||||
 | 
							return s.Shutdown(ctx)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return g.Wait()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type service struct {
 | 
				
			||||||
 | 
						es driver.EventStore
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *service) event(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						ctx := r.Context()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var name, tags string
 | 
				
			||||||
 | 
						if strings.HasPrefix(r.URL.Path, "/event/") {
 | 
				
			||||||
 | 
							name = strings.TrimPrefix(r.URL.Path, "/event/")
 | 
				
			||||||
 | 
							name, tags, _ = strings.Cut(name, "/")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusNotFound)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if r.Method == http.MethodGet {
 | 
				
			||||||
 | 
							if name == "" {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusNotFound)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var pos, count int64 = -1, -99
 | 
				
			||||||
 | 
							qry := r.URL.Query()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if i, err := strconv.ParseInt(qry.Get("pos"), 10, 64); err == nil {
 | 
				
			||||||
 | 
								pos = i
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if i, err := strconv.ParseInt(qry.Get("n"), 10, 64); err == nil {
 | 
				
			||||||
 | 
								count = i
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							log.Print("name=", name, ", pos=", pos, ", n=", count)
 | 
				
			||||||
 | 
							events, err := s.es.Read(ctx, "post-"+name, pos, count)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Print(err)
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for i := range events {
 | 
				
			||||||
 | 
								fmt.Fprintln(w, events[i])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Print(err)
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						r.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if name == "" {
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusNotFound)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Print(name, tags)
 | 
				
			||||||
 | 
						events := event.NewEvents(&PostEvent{
 | 
				
			||||||
 | 
							Payload: b,
 | 
				
			||||||
 | 
							Tags:    strings.Split(tags, "/"),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						_, err = s.es.Append(r.Context(), "post-"+name, events)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Print(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						m := events.First().EventMeta()
 | 
				
			||||||
 | 
						w.WriteHeader(http.StatusAccepted)
 | 
				
			||||||
 | 
						fmt.Fprintf(w, "OK %d %s", m.Position, m.EventID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PostEvent struct {
 | 
				
			||||||
 | 
						Payload []byte
 | 
				
			||||||
 | 
						Tags    []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						eventMeta event.Meta
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *PostEvent) EventMeta() event.Meta {
 | 
				
			||||||
 | 
						if e == nil {
 | 
				
			||||||
 | 
							return event.Meta{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return e.eventMeta
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (e *PostEvent) SetEventMeta(eventMeta event.Meta) {
 | 
				
			||||||
 | 
						if e == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						e.eventMeta = eventMeta
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (e *PostEvent) String() string {
 | 
				
			||||||
 | 
						var b bytes.Buffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// b.WriteString(e.eventMeta.StreamID)
 | 
				
			||||||
 | 
						// b.WriteRune('@')
 | 
				
			||||||
 | 
						b.WriteString(strconv.FormatUint(e.eventMeta.Position, 10))
 | 
				
			||||||
 | 
						b.WriteRune('\t')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b.WriteString(e.eventMeta.EventID.String())
 | 
				
			||||||
 | 
						b.WriteRune('\t')
 | 
				
			||||||
 | 
						b.WriteString(string(e.Payload))
 | 
				
			||||||
 | 
						if len(e.Tags) > 0 {
 | 
				
			||||||
 | 
							b.WriteRune('\t')
 | 
				
			||||||
 | 
							b.WriteString(strings.Join(e.Tags, ","))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										232
									
								
								pkg/es/driver/disk-store/disk-store.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								pkg/es/driver/disk-store/disk-store.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,232 @@
 | 
				
			|||||||
 | 
					package ds_driver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/tidwall/wal"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"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/math"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type diskStore struct {
 | 
				
			||||||
 | 
						path string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ driver.Driver = (*diskStore)(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Init(ctx context.Context) {
 | 
				
			||||||
 | 
						es.Register(ctx, "file", &diskStore{})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (diskStore) Open(dsn string) (driver.EventStore, error) {
 | 
				
			||||||
 | 
						scheme, path, ok := strings.Cut(dsn, ":")
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("expected scheme")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if scheme != "file" {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("expeted scheme=file, got=%s", scheme)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := os.Stat(path); os.IsNotExist(err) {
 | 
				
			||||||
 | 
							err = os.MkdirAll(path, 0700)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &diskStore{path: path}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (es *diskStore) Save(ctx context.Context, agg event.Aggregate) (uint64, error) {
 | 
				
			||||||
 | 
						l, err := es.readLog(agg.StreamID())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var last uint64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if last, err = l.w.LastIndex(); err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if agg.StreamVersion() != last {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("current version wrong %d != %d", agg.StreamVersion(), last)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						events := agg.Events(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var b []byte
 | 
				
			||||||
 | 
						batch := &wal.Batch{}
 | 
				
			||||||
 | 
						for _, e := range events {
 | 
				
			||||||
 | 
							b, err = event.MarshalText(e)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							batch.Write(e.EventMeta().Position, b)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = l.w.WriteBatch(batch)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						agg.Commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return uint64(len(events)), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (es *diskStore) Append(ctx context.Context, streamID string, events event.Events) (uint64, error) {
 | 
				
			||||||
 | 
						event.SetStreamID(streamID, events...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						l, err := es.readLog(streamID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var last uint64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if last, err = l.w.LastIndex(); err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var b []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						batch := &wal.Batch{}
 | 
				
			||||||
 | 
						for i, e := range events {
 | 
				
			||||||
 | 
							b, err = event.MarshalText(e)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							pos := last + uint64(i) + 1
 | 
				
			||||||
 | 
							event.SetPosition(e, pos)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							batch.Write(pos, b)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = l.w.WriteBatch(batch)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return uint64(len(events)), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (es *diskStore) Load(ctx context.Context, agg event.Aggregate) error {
 | 
				
			||||||
 | 
						l, err := es.readLog(agg.StreamID())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var i, first, last uint64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if first, err = l.w.FirstIndex(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if last, err = l.w.LastIndex(); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if first == 0 || last == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var b []byte
 | 
				
			||||||
 | 
						events := make([]event.Event, last-i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i = 0; first+i <= last; i++ {
 | 
				
			||||||
 | 
							b, err = l.w.Read(first + i)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							events[i], err = event.UnmarshalText(b, first+i)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						event.Append(agg, events...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (es *diskStore) Read(ctx context.Context, streamID string, pos, count int64) (event.Events, error) {
 | 
				
			||||||
 | 
						l, err := es.readLog(streamID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var first, last, start uint64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if first, err = l.w.FirstIndex(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if last, err = l.w.LastIndex(); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if first == 0 || last == 0 {
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case pos >= 0:
 | 
				
			||||||
 | 
							start = first + uint64(pos)
 | 
				
			||||||
 | 
							if pos == 0 && count < 0 {
 | 
				
			||||||
 | 
								count = -count // if pos=0 assume forward count.
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case pos < 0:
 | 
				
			||||||
 | 
							start = uint64(int64(last) + pos + 1)
 | 
				
			||||||
 | 
							if pos == -1 && count > 0 {
 | 
				
			||||||
 | 
								count = -count // if pos=-1 assume backward count.
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						events := make([]event.Event, math.Abs(count))
 | 
				
			||||||
 | 
						for i := range events {
 | 
				
			||||||
 | 
							var b []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							b, err = l.w.Read(start)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return events, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							events[i], err = event.UnmarshalText(b, start)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return events, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if count > 0 {
 | 
				
			||||||
 | 
								start += 1
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								start -= 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if start < first || start > last {
 | 
				
			||||||
 | 
								events = events[:i+1]
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						event.SetStreamID(streamID, events...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return events, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (es *diskStore) readLog(name string) (*eventLog, error) {
 | 
				
			||||||
 | 
						return newEventLog(name, filepath.Join(es.path, name))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type eventLog struct {
 | 
				
			||||||
 | 
						name string
 | 
				
			||||||
 | 
						path string
 | 
				
			||||||
 | 
						w    *wal.Log
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newEventLog(name, path string) (*eventLog, error) {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						el := &eventLog{name: name, path: path}
 | 
				
			||||||
 | 
						el.w, err = wal.Open(path, wal.DefaultOptions)
 | 
				
			||||||
 | 
						return el, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								pkg/es/driver/driver.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								pkg/es/driver/driver.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					package driver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/es/event"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Driver interface {
 | 
				
			||||||
 | 
						Open(string) (EventStore, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type EventStore interface {
 | 
				
			||||||
 | 
						Save(ctx context.Context, agg event.Aggregate) (uint64, error)
 | 
				
			||||||
 | 
						Load(ctx context.Context, agg event.Aggregate) error
 | 
				
			||||||
 | 
						Read(ctx context.Context, streamID string, pos, count int64) (event.Events, error)
 | 
				
			||||||
 | 
						Append(ctx context.Context, streamID string, events event.Events) (uint64, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								pkg/es/es.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								pkg/es/es.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					package es
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/es/driver"
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/locker"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type config struct {
 | 
				
			||||||
 | 
						drivers map[string]driver.Driver
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						Config = locker.New(&config{drivers: make(map[string]driver.Driver)})
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Register(ctx context.Context, name string, d driver.Driver) {
 | 
				
			||||||
 | 
						Config.Modify(ctx, func(c *config) error {
 | 
				
			||||||
 | 
							if _, set := c.drivers[name]; set {
 | 
				
			||||||
 | 
								return fmt.Errorf("driver %s already set", name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							c.drivers[name] = d
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Open(ctx context.Context, dsn string) (driver.EventStore, error) {
 | 
				
			||||||
 | 
						name, _, ok := strings.Cut(dsn, ":")
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("%w: no scheme", ErrNoDriver)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var d driver.Driver
 | 
				
			||||||
 | 
						Config.Modify(ctx,func(c *config) error {
 | 
				
			||||||
 | 
							var ok bool
 | 
				
			||||||
 | 
							d, ok = c.drivers[name]
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return fmt.Errorf("%w: %s not registered", ErrNoDriver, name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return d.Open(dsn)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var ErrNoDriver = errors.New("no driver")
 | 
				
			||||||
							
								
								
									
										101
									
								
								pkg/es/es_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								pkg/es/es_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,101 @@
 | 
				
			|||||||
 | 
					package es_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/es"
 | 
				
			||||||
 | 
						ds_driver "github.com/sour-is/ev/pkg/es/driver/disk-store"
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/es/event"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestES(t *testing.T) {
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						event.Register(&ValueSet{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ds_driver.Init(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						es, err := es.Open(ctx, "file:data")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						thing := &Thing{Name: "time"}
 | 
				
			||||||
 | 
						err = es.Load(ctx, thing)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						t.Log(thing.StreamVersion(), thing.Name, thing.Value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = thing.OnSetValue(time.Now().String())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						i, err := es.Save(ctx, thing)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Log(thing.StreamVersion(), thing.Name, thing.Value)
 | 
				
			||||||
 | 
						t.Log("Wrote: ", i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						events, err := es.Read(ctx, "thing-time", -1, -11)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, e := range events {
 | 
				
			||||||
 | 
							t.Logf("event %d %d - %v\n", i, e.EventMeta().Position, e)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Thing struct {
 | 
				
			||||||
 | 
						Name  string
 | 
				
			||||||
 | 
						Value string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						event.AggregateRoot
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *Thing) StreamID() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("thing-%s", a.Name)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						_ event.Event     = (*ValueSet)(nil)
 | 
				
			||||||
 | 
						_ event.Aggregate = (*Thing)(nil)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										124
									
								
								pkg/es/event/aggregate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								pkg/es/event/aggregate.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
				
			|||||||
 | 
					package event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Aggregate interface {
 | 
				
			||||||
 | 
						// ApplyEvent  applies the event to the aggrigate state
 | 
				
			||||||
 | 
						ApplyEvent(...Event)
 | 
				
			||||||
 | 
						StreamID() string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						AggregateRootInterface
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Raise adds new uncommitted events
 | 
				
			||||||
 | 
					func Raise(a Aggregate, lis ...Event) {
 | 
				
			||||||
 | 
						lis = NewEvents(lis...)
 | 
				
			||||||
 | 
						SetStreamID(a.StreamID(), lis...)
 | 
				
			||||||
 | 
						a.raise(lis...)
 | 
				
			||||||
 | 
						a.ApplyEvent(lis...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Append adds new committed events
 | 
				
			||||||
 | 
					func Append(a Aggregate, lis ...Event) {
 | 
				
			||||||
 | 
						a.append(lis...)
 | 
				
			||||||
 | 
						a.ApplyEvent(lis...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CheckVersion returns an error if the version does not match.
 | 
				
			||||||
 | 
					func CheckVersion(a Aggregate, version uint64) error {
 | 
				
			||||||
 | 
						if version != uint64(a.StreamVersion()) {
 | 
				
			||||||
 | 
							return fmt.Errorf("version wrong, got (proposed) %d != (expected) %d", version, a.StreamVersion())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NotExists returns error if there are no events present.
 | 
				
			||||||
 | 
					func NotExists(a Aggregate) error {
 | 
				
			||||||
 | 
						if a.StreamVersion() != 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("%w, got version == %d", ErrShouldNotExist, a.StreamVersion())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AggregateRootInterface interface {
 | 
				
			||||||
 | 
						// Events returns the aggrigate events
 | 
				
			||||||
 | 
						// pass true for only uncommitted events
 | 
				
			||||||
 | 
						Events(bool) Events
 | 
				
			||||||
 | 
						// StreamVersion returns last commit events
 | 
				
			||||||
 | 
						StreamVersion() uint64
 | 
				
			||||||
 | 
						// Version returns the current aggrigate version. (committed + uncommitted)
 | 
				
			||||||
 | 
						Version() uint64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						raise(lis ...Event)
 | 
				
			||||||
 | 
						append(lis ...Event)
 | 
				
			||||||
 | 
						Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ AggregateRootInterface = &AggregateRoot{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AggregateRoot struct {
 | 
				
			||||||
 | 
						events        Events
 | 
				
			||||||
 | 
						streamVersion uint64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mu sync.RWMutex
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *AggregateRoot) Commit() {
 | 
				
			||||||
 | 
						a.streamVersion = uint64(len(a.events))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *AggregateRoot) StreamVersion() uint64 {
 | 
				
			||||||
 | 
						return a.streamVersion
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (a *AggregateRoot) Events(new bool) Events {
 | 
				
			||||||
 | 
						a.mu.RLock()
 | 
				
			||||||
 | 
						defer a.mu.RUnlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						events := a.events
 | 
				
			||||||
 | 
						if new {
 | 
				
			||||||
 | 
							events = events[a.streamVersion:]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lis := make(Events, len(events))
 | 
				
			||||||
 | 
						copy(lis, events)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return lis
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (a *AggregateRoot) Version() uint64 {
 | 
				
			||||||
 | 
						return uint64(len(a.events))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//lint:ignore U1000 is called by embeded interface
 | 
				
			||||||
 | 
					func (a *AggregateRoot) raise(lis ...Event) { //nolint
 | 
				
			||||||
 | 
						a.mu.Lock()
 | 
				
			||||||
 | 
						defer a.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						a.posStartAt(lis...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						a.events = append(a.events, lis...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//lint:ignore U1000 is called by embeded interface
 | 
				
			||||||
 | 
					func (a *AggregateRoot) append(lis ...Event) {
 | 
				
			||||||
 | 
						a.mu.Lock()
 | 
				
			||||||
 | 
						defer a.mu.Unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						a.posStartAt(lis...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						a.events = append(a.events, lis...)
 | 
				
			||||||
 | 
						a.streamVersion += uint64(len(lis))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a *AggregateRoot) posStartAt(lis ...Event) {
 | 
				
			||||||
 | 
						for i, e := range lis {
 | 
				
			||||||
 | 
							m := e.EventMeta()
 | 
				
			||||||
 | 
							m.Position = a.streamVersion + uint64(i) + 1
 | 
				
			||||||
 | 
							e.SetEventMeta(m)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var ErrShouldNotExist = errors.New("should not exist")
 | 
				
			||||||
							
								
								
									
										129
									
								
								pkg/es/event/events.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								pkg/es/event/events.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					package event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ulid "github.com/oklog/ulid/v2"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var pool = sync.Pool{
 | 
				
			||||||
 | 
						New: func() interface{} { return ulid.Monotonic(rand.Reader, 0) },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getULID() ulid.ULID {
 | 
				
			||||||
 | 
						var entropy io.Reader = rand.Reader
 | 
				
			||||||
 | 
						if e, ok := pool.Get().(io.Reader); ok {
 | 
				
			||||||
 | 
							entropy = e
 | 
				
			||||||
 | 
							defer pool.Put(e)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ulid.MustNew(ulid.Now(), entropy)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Event interface {
 | 
				
			||||||
 | 
						EventMeta() Meta
 | 
				
			||||||
 | 
						SetEventMeta(Meta)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Events is a list of events
 | 
				
			||||||
 | 
					type Events []Event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewEvents(lis ...Event) Events {
 | 
				
			||||||
 | 
						for i, e := range lis {
 | 
				
			||||||
 | 
							meta := e.EventMeta()
 | 
				
			||||||
 | 
							meta.Position = uint64(i)
 | 
				
			||||||
 | 
							meta.EventID = getULID()
 | 
				
			||||||
 | 
							e.SetEventMeta(meta)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lis
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (lis Events) StreamID() string {
 | 
				
			||||||
 | 
						if len(lis) == 0 {
 | 
				
			||||||
 | 
							return ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lis.First().EventMeta().StreamID
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (lis Events) SetStreamID(streamID string) {
 | 
				
			||||||
 | 
						SetStreamID(streamID, lis...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (lis Events) First() Event {
 | 
				
			||||||
 | 
						if len(lis) == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lis[0]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (lis Events) Rest() Events {
 | 
				
			||||||
 | 
						if len(lis) == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return lis[1:]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (lis Events) MarshalText() ([]byte, error) {
 | 
				
			||||||
 | 
						b := &bytes.Buffer{}
 | 
				
			||||||
 | 
						for i := range lis {
 | 
				
			||||||
 | 
							txt, err := MarshalText(lis[i])
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							b.Write(txt)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return b.Bytes(), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TypeOf(e Event) string {
 | 
				
			||||||
 | 
						if ie, ok := e.(interface{ UnwrapEvent() Event }); ok {
 | 
				
			||||||
 | 
							e = ie.UnwrapEvent()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if e, ok := e.(interface{ EventType() string }); ok {
 | 
				
			||||||
 | 
							return e.EventType()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Default to printed representation for unnamed types
 | 
				
			||||||
 | 
						return strings.Trim(fmt.Sprintf("%T", e), "*")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type streamID string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s streamID) StreamID() string {
 | 
				
			||||||
 | 
						return string(s)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func StreamID(e Event) streamID {
 | 
				
			||||||
 | 
						return streamID(e.EventMeta().StreamID)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func SetStreamID(id string, lis ...Event) {
 | 
				
			||||||
 | 
						for _, e := range lis {
 | 
				
			||||||
 | 
							meta := e.EventMeta()
 | 
				
			||||||
 | 
							meta.StreamID = id
 | 
				
			||||||
 | 
							e.SetEventMeta(meta)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func EventID(e Event) ulid.ULID {
 | 
				
			||||||
 | 
						return e.EventMeta().EventID
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func SetEventID(e Event, id ulid.ULID) {
 | 
				
			||||||
 | 
						meta := e.EventMeta()
 | 
				
			||||||
 | 
						meta.EventID = id
 | 
				
			||||||
 | 
						e.SetEventMeta(meta)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func SetPosition(e Event, i uint64) {
 | 
				
			||||||
 | 
						meta := e.EventMeta()
 | 
				
			||||||
 | 
						meta.Position = i
 | 
				
			||||||
 | 
						e.SetEventMeta(meta)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type Meta struct {
 | 
				
			||||||
 | 
						EventID  ulid.ULID
 | 
				
			||||||
 | 
						StreamID string
 | 
				
			||||||
 | 
						Position uint64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m Meta) Time() time.Time {
 | 
				
			||||||
 | 
						return ulid.Time(m.EventID.Time())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										59
									
								
								pkg/es/event/events_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								pkg/es/event/events_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					package event_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/matryer/is"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/sour-is/ev/pkg/es/event"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DummyEvent struct {
 | 
				
			||||||
 | 
						Value string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						eventMeta event.Meta
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e *DummyEvent) EventMeta() event.Meta {
 | 
				
			||||||
 | 
						if e == nil {
 | 
				
			||||||
 | 
							return event.Meta{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return e.eventMeta
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (e *DummyEvent) SetEventMeta(eventMeta event.Meta) {
 | 
				
			||||||
 | 
						if e == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						e.eventMeta = eventMeta
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestEventEncode(t *testing.T) {
 | 
				
			||||||
 | 
						is := is.New(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						event.Register(&DummyEvent{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var lis event.Events = event.NewEvents(
 | 
				
			||||||
 | 
							&DummyEvent{Value: "testA"},
 | 
				
			||||||
 | 
							&DummyEvent{Value: "testB"},
 | 
				
			||||||
 | 
							&DummyEvent{Value: "testC"},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						lis.SetStreamID("test")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						blis, err := event.EncodeEvents(lis...)
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, b := range blis {
 | 
				
			||||||
 | 
							sp := bytes.SplitN(b, []byte{'\t'}, 4)
 | 
				
			||||||
 | 
							is.Equal(len(sp), 4)
 | 
				
			||||||
 | 
							is.Equal(string(sp[1]), "test")
 | 
				
			||||||
 | 
							is.Equal(string(sp[2]), "event_test.DummyEvent")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						chk, err := event.DecodeEvents(blis...)
 | 
				
			||||||
 | 
						is.NoErr(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := range chk {
 | 
				
			||||||
 | 
							is.Equal(lis[i], chk[i])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										201
									
								
								pkg/es/event/reflect.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								pkg/es/event/reflect.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,201 @@
 | 
				
			|||||||
 | 
					package event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						eventTypes sync.Map
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type UnknownEvent struct {
 | 
				
			||||||
 | 
						eventType string
 | 
				
			||||||
 | 
						values    map[string]json.RawMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						eventMeta Meta
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ Event = (*UnknownEvent)(nil)
 | 
				
			||||||
 | 
					var _ json.Marshaler = (*UnknownEvent)(nil)
 | 
				
			||||||
 | 
					var _ json.Unmarshaler = (*UnknownEvent)(nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewUnknownEventFromValues(eventType string, meta Meta, values url.Values) *UnknownEvent {
 | 
				
			||||||
 | 
						jsonValues := make(map[string]json.RawMessage, len(values))
 | 
				
			||||||
 | 
						for k, v := range values {
 | 
				
			||||||
 | 
							switch len(v) {
 | 
				
			||||||
 | 
							case 0:
 | 
				
			||||||
 | 
								jsonValues[k] = []byte("null")
 | 
				
			||||||
 | 
							case 1:
 | 
				
			||||||
 | 
								jsonValues[k] = embedJSON(v[0])
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								parts := make([][]byte, len(v))
 | 
				
			||||||
 | 
								for i := range v {
 | 
				
			||||||
 | 
									parts[i] = embedJSON(v[i])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								jsonValues[k] = append([]byte("["), bytes.Join(parts, []byte(","))...)
 | 
				
			||||||
 | 
								jsonValues[k] = append(jsonValues[k], ']')
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &UnknownEvent{eventType: eventType, eventMeta: meta, values: jsonValues}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func NewUnknownEventFromRaw(eventType string, meta Meta, values map[string]json.RawMessage) *UnknownEvent {
 | 
				
			||||||
 | 
						return &UnknownEvent{eventType: eventType, eventMeta: meta, values: values}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (u UnknownEvent) EventMeta() Meta   { return u.eventMeta }
 | 
				
			||||||
 | 
					func (u UnknownEvent) EventType() string { return u.eventType }
 | 
				
			||||||
 | 
					func (u *UnknownEvent) SetEventMeta(em Meta) {
 | 
				
			||||||
 | 
						u.eventMeta = em
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (u *UnknownEvent) UnmarshalJSON(b []byte) error {
 | 
				
			||||||
 | 
						return json.Unmarshal(b, &u.values)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (u *UnknownEvent) MarshalJSON() ([]byte, error) {
 | 
				
			||||||
 | 
						return json.Marshal(u.values)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Register a type container for Unmarshalling values into. The type must implement Event and not be a nil value.
 | 
				
			||||||
 | 
					func Register(lis ...Event) {
 | 
				
			||||||
 | 
						for _, e := range lis {
 | 
				
			||||||
 | 
							if e == nil {
 | 
				
			||||||
 | 
								panic(fmt.Sprintf("can't register event.Event of type=%T with value=%v", e, e))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							value := reflect.ValueOf(e)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if value.IsNil() {
 | 
				
			||||||
 | 
								panic(fmt.Sprintf("can't register event.Event of type=%T with value=%v", e, e))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							value = reflect.Indirect(value)
 | 
				
			||||||
 | 
							typ := value.Type()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							eventTypes.LoadOrStore(TypeOf(e), typ)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func GetContainer(s string) Event {
 | 
				
			||||||
 | 
						if typ, ok := eventTypes.Load(s); ok {
 | 
				
			||||||
 | 
							if typ, ok := typ.(reflect.Type); ok {
 | 
				
			||||||
 | 
								newType := reflect.New(typ)
 | 
				
			||||||
 | 
								newInterface := newType.Interface()
 | 
				
			||||||
 | 
								if typ, ok := newInterface.(Event); ok {
 | 
				
			||||||
 | 
									return typ
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &UnknownEvent{eventType: s}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func MarshalText(e Event) (txt []byte, err error) {
 | 
				
			||||||
 | 
						b := &bytes.Buffer{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err = writeMarshaler(b, e.EventMeta().EventID); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.WriteRune('\t')
 | 
				
			||||||
 | 
						if _, err = b.WriteString(e.EventMeta().StreamID); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.WriteRune('\t')
 | 
				
			||||||
 | 
						if _, err = b.WriteString(TypeOf(e)); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b.WriteRune('\t')
 | 
				
			||||||
 | 
						if enc, ok := e.(encoding.TextMarshaler); ok {
 | 
				
			||||||
 | 
							if txt, err = enc.MarshalText(); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if txt, err = json.Marshal(e); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err = b.Write(txt)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return b.Bytes(), err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UnmarshalText(txt []byte, pos uint64) (e Event, err error) {
 | 
				
			||||||
 | 
						sp := bytes.SplitN(txt, []byte{'\t'}, 4)
 | 
				
			||||||
 | 
						if len(sp) != 4 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("invalid format. expected=4, got=%d", len(sp))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						m := Meta{}
 | 
				
			||||||
 | 
						if err = m.EventID.UnmarshalText(sp[0]); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						m.StreamID = string(sp[1])
 | 
				
			||||||
 | 
						m.Position = pos
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						eventType := string(sp[2])
 | 
				
			||||||
 | 
						e = GetContainer(eventType)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if enc, ok := e.(encoding.TextUnmarshaler); ok {
 | 
				
			||||||
 | 
							if err = enc.UnmarshalText(sp[3]); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if err = json.Unmarshal(sp[3], e); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						e.SetEventMeta(m)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return e, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func writeMarshaler(out io.Writer, in encoding.TextMarshaler) (int, error) {
 | 
				
			||||||
 | 
						if b, err := in.MarshalText(); err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return out.Write(b)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DecodeEvents unmarshals the byte list into Events.
 | 
				
			||||||
 | 
					func DecodeEvents(lis ...[]byte) (Events, error) {
 | 
				
			||||||
 | 
						elis := make([]Event, len(lis))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						for i, txt := range lis {
 | 
				
			||||||
 | 
							elis[i], err = UnmarshalText(txt, uint64(i))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return elis, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func EncodeEvents(events ...Event) (lis [][]byte, err error) {
 | 
				
			||||||
 | 
						lis = make([][]byte, len(events))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, txt := range events {
 | 
				
			||||||
 | 
							lis[i], err = MarshalText(txt)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return lis, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func embedJSON(s string) json.RawMessage {
 | 
				
			||||||
 | 
						if len(s) > 1 && s[0] == '{' && s[len(s)-1] == '}' {
 | 
				
			||||||
 | 
							return []byte(s)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(s) > 1 && s[0] == '[' && s[len(s)-1] == ']' {
 | 
				
			||||||
 | 
							return []byte(s)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return []byte(fmt.Sprintf(`"%s"`, strings.Replace(s, `"`, `\"`, -1)))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								pkg/locker/locker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pkg/locker/locker.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					package locker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Locked[T any] struct {
 | 
				
			||||||
 | 
						state chan *T
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func New[T any](initial *T) *Locked[T] {
 | 
				
			||||||
 | 
						s := &Locked[T]{}
 | 
				
			||||||
 | 
						s.state = make(chan *T, 1)
 | 
				
			||||||
 | 
						s.state <- initial
 | 
				
			||||||
 | 
						return s
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *Locked[T]) Modify(ctx context.Context, fn func(*T) error) error {
 | 
				
			||||||
 | 
						if ctx.Err() != nil {
 | 
				
			||||||
 | 
							return ctx.Err()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
						case state := <-s.state:
 | 
				
			||||||
 | 
							defer func() { s.state <- state }()
 | 
				
			||||||
 | 
							return fn(state)
 | 
				
			||||||
 | 
						case <-ctx.Done():
 | 
				
			||||||
 | 
							return ctx.Err()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								pkg/math/math.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								pkg/math/math.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					package math
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type signed interface {
 | 
				
			||||||
 | 
						~int | ~int8 | ~int16 | ~int32 | ~int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type unsigned interface {
 | 
				
			||||||
 | 
						~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type integer interface {
 | 
				
			||||||
 | 
						signed | unsigned
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type float interface {
 | 
				
			||||||
 | 
						~float32 | ~float64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					type ordered interface {
 | 
				
			||||||
 | 
						integer | float | ~string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Abs[T signed](i T) T {
 | 
				
			||||||
 | 
						if i > 0 {
 | 
				
			||||||
 | 
							return i
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return -i
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func Max[T ordered](i, j T) T {
 | 
				
			||||||
 | 
						if i > j {
 | 
				
			||||||
 | 
							return i
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return j
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func Min[T ordered](i, j T) T {
 | 
				
			||||||
 | 
						if i < j {
 | 
				
			||||||
 | 
							return i
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return j
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user