Compare commits
7 Commits
main
...
inprogress
Author | SHA1 | Date | |
---|---|---|---|
|
2732178507 | ||
|
092a4d59f1 | ||
|
be8a318ca3 | ||
|
fb72d4bc8c | ||
|
7c48812057 | ||
|
47aff6b106 | ||
|
ba3c302dc4 |
37
.air.toml
Normal file
37
.air.toml
Normal file
|
@ -0,0 +1,37 @@
|
|||
root = "."
|
||||
testdata_dir = "data"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
kill_delay = "1s"
|
||||
log = "build-errors.log"
|
||||
send_interrupt = true
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
14
.drone.yml
14
.drone.yml
|
@ -1,14 +0,0 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.21.1
|
||||
commands:
|
||||
- go test -v -race -skip '^TestE2E|TestMain' ./...
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
|
@ -1,25 +0,0 @@
|
|||
name: Go Bump
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.21.1
|
||||
|
||||
- run: go install github.com/psanetra/git-semver/cli@master
|
||||
|
||||
- run: git tag v$(cli next --stable=false) && git push --tags || echo no change
|
||||
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
@ -1,29 +0,0 @@
|
|||
name: Go Bump
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Deploy to external repository
|
||||
uses: https://git.sour.is/actions/github-action-push-to-another-repository@main
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
|
||||
with:
|
||||
# GitHub Action output files
|
||||
source-directory: .
|
||||
destination-github-username: sour-is
|
||||
destination-repository-name: ev
|
||||
user-email: jon@xuu.cc
|
||||
# It defaults to `main`
|
||||
target-branch: "main"
|
||||
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
@ -1,33 +0,0 @@
|
|||
name: Go Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
||||
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
||||
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||
|
||||
- name: List files in the repository
|
||||
run: |
|
||||
ls ${{ gitea.workspace }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.21.1
|
||||
|
||||
- name: Test
|
||||
run: go test --race -v ./...
|
||||
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
25
.github/workflows/go.yml
vendored
25
.github/workflows/go.yml
vendored
|
@ -1,25 +0,0 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.21.1
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test --race -v ./...
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -5,8 +5,3 @@ data/
|
|||
local.mk
|
||||
logzio.yml
|
||||
tmp/
|
||||
/build
|
||||
/ev
|
||||
acct.yml
|
||||
.DS_Store
|
||||
/*.yaml
|
||||
|
|
BIN
.gitsecret/keys/pubring.kbx
Normal file
BIN
.gitsecret/keys/pubring.kbx
Normal file
Binary file not shown.
BIN
.gitsecret/keys/pubring.kbx~
Normal file
BIN
.gitsecret/keys/pubring.kbx~
Normal file
Binary file not shown.
BIN
.gitsecret/keys/trustdb.gpg
Normal file
BIN
.gitsecret/keys/trustdb.gpg
Normal file
Binary file not shown.
1
.gitsecret/paths/mapping.cfg
Normal file
1
.gitsecret/paths/mapping.cfg
Normal file
|
@ -0,0 +1 @@
|
|||
local.mk:d632b22a2291637331e5613d35536c69e696447ce407d7320b4c5ab0922b47a9
|
11
LICENSE
11
LICENSE
|
@ -1,11 +0,0 @@
|
|||
Copyright 2023 Jon Lundy <me@sour.is>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
44
Makefile
Normal file
44
Makefile
Normal file
|
@ -0,0 +1,44 @@
|
|||
export PATH:=$(shell go env GOPATH)/bin:$(PATH)
|
||||
export EV_DATA=mem:
|
||||
export EV_HTTP=:8080
|
||||
export EV_TRACE_SAMPLE=always
|
||||
export EV_TRACE_ENDPOINT=localhost:4318
|
||||
-include local.mk
|
||||
|
||||
air: gen
|
||||
ifeq (, $(shell which air))
|
||||
go install github.com/cosmtrek/air@latest
|
||||
endif
|
||||
air
|
||||
|
||||
run:
|
||||
go run .
|
||||
|
||||
test:
|
||||
go test -cover -race ./...
|
||||
|
||||
|
||||
GQLS=gqlgen.yml
|
||||
GQLS:=$(GQLS) $(wildcard api/gql_ev/*.go)
|
||||
GQLS:=$(GQLS) $(wildcard pkg/*/*.graphqls)
|
||||
GQLS:=$(GQLS) $(wildcard app/*/*.graphqls)
|
||||
GQLS:=$(GQLS) $(wildcard app/*/*.go)
|
||||
GQLSRC=internal/graph/generated/generated.go
|
||||
|
||||
gen: gql
|
||||
gql: $(GQLSRC)
|
||||
$(GQLSRC): $(GQLS)
|
||||
ifeq (, $(shell which gqlgen))
|
||||
go install github.com/99designs/gqlgen@latest
|
||||
endif
|
||||
gqlgen
|
||||
|
||||
|
||||
EV_HOST?=localhost:8080
|
||||
load:
|
||||
watch -n .1 "http POST $(EV_HOST)/inbox/asdf/test a=b one=1 two:='{\"v\":2}' | jq"
|
||||
|
||||
bi:
|
||||
go build .
|
||||
sudo mv ev /usr/local/bin/
|
||||
sudo systemctl restart ev
|
|
@ -3,5 +3,3 @@
|
|||
This project is learnings in building an eventstore and applications around it.
|
||||
|
||||
Feel free to explore.
|
||||
|
||||
For examples of use of this see <go.sour.is/tools>
|
62
app/gql/playground/playground.go
Normal file
62
app/gql/playground/playground.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package playground
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8/>
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
|
||||
<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
|
||||
integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
|
||||
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
|
||||
integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"
|
||||
integrity="{{ .jsSRI }}" crossorigin="anonymous"></script>
|
||||
<title>{{.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<style type="text/css">
|
||||
html { font-family: "Open Sans", sans-serif; overflow: hidden; }
|
||||
body { margin: 0; background: #172a3a; }
|
||||
</style>
|
||||
<div id="root"/>
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', function (event) {
|
||||
const root = document.getElementById('root');
|
||||
root.classList.add('playgroundIn');
|
||||
const wsProto = location.protocol == 'https:' ? 'wss:' : 'ws:'
|
||||
GraphQLPlayground.init(root, {
|
||||
endpoint: location.protocol + '//' + location.host + '{{.endpoint}}',
|
||||
subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}',
|
||||
shareEnabled: true,
|
||||
settings: {
|
||||
'request.credentials': 'same-origin'
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
func Handler(title string, endpoint string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
err := page.Execute(w, map[string]string{
|
||||
"title": title,
|
||||
"endpoint": endpoint,
|
||||
"version": "1.7.26",
|
||||
"cssSRI": "sha256-dKnNLEFwKSVFpkpjRWe+o/jQDM6n/JsvQ0J3l5Dk3fc=",
|
||||
"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
|
||||
"jsSRI": "sha256-SG9YAy4eywTcLckwij7V4oSCG3hOdV1m+2e1XuNxIgk=",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
132
app/gql/resolver.go
Normal file
132
app/gql/resolver.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package gql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/ravilushqa/otelgqlgen"
|
||||
"github.com/sour-is/ev/app/gql/playground"
|
||||
"github.com/sour-is/ev/app/msgbus"
|
||||
"github.com/sour-is/ev/app/salty"
|
||||
"github.com/sour-is/ev/internal/graph/generated"
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
"github.com/sour-is/ev/pkg/gql"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
msgbus.MsgbusResolver
|
||||
salty.SaltyResolver
|
||||
es.EventResolver
|
||||
}
|
||||
|
||||
func New(ctx context.Context, resolvers ...interface{ RegisterHTTP(*http.ServeMux) }) (*Resolver, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
r := &Resolver{}
|
||||
v := reflect.ValueOf(r)
|
||||
v = reflect.Indirect(v)
|
||||
noop := reflect.ValueOf(&noop{})
|
||||
|
||||
outer:
|
||||
for _, idx := range reflect.VisibleFields(v.Type()) {
|
||||
field := v.FieldByIndex(idx.Index)
|
||||
|
||||
for i := range resolvers {
|
||||
rs := reflect.ValueOf(resolvers[i])
|
||||
|
||||
if field.IsNil() && rs.Type().Implements(field.Type()) {
|
||||
span.AddEvent(fmt.Sprint("found ", field.Type().Name()))
|
||||
field.Set(rs)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("default ", field.Type().Name()))
|
||||
field.Set(noop)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Query() generated.QueryResolver { return r }
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Mutation() generated.MutationResolver { return r }
|
||||
|
||||
// Subscription returns generated.SubscriptionResolver implementation.
|
||||
func (r *Resolver) Subscription() generated.SubscriptionResolver { return r }
|
||||
|
||||
// ChainMiddlewares will check all embeded resolvers for a GetMiddleware func and add to handler.
|
||||
func (r *Resolver) ChainMiddlewares(h http.Handler) http.Handler {
|
||||
v := reflect.ValueOf(r) // Get reflected value of *Resolver
|
||||
v = reflect.Indirect(v) // Get the pointed value (returns a zero value on nil)
|
||||
n := v.NumField() // Get number of fields to iterate over.
|
||||
for i := 0; i < n; i++ {
|
||||
f := v.Field(i)
|
||||
if !f.CanInterface() { // Skip non-interface types.
|
||||
continue
|
||||
}
|
||||
if iface, ok := f.Interface().(interface {
|
||||
GetMiddleware() func(http.Handler) http.Handler
|
||||
}); ok {
|
||||
h = iface.GetMiddleware()(h) // Append only items that fulfill the interface.
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (r *Resolver) RegisterHTTP(mux *http.ServeMux) {
|
||||
gql := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: r}))
|
||||
gql.SetRecoverFunc(NoopRecover)
|
||||
gql.Use(otelgqlgen.Middleware())
|
||||
mux.Handle("/", playground.Handler("GraphQL playground", "/gql"))
|
||||
mux.Handle("/gql", lg.Htrace(r.ChainMiddlewares(gql), "gql"))
|
||||
}
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func NoopRecover(ctx context.Context, err interface{}) error {
|
||||
if err, ok := err.(string); ok && err == "not implemented" {
|
||||
return gqlerror.Errorf("not implemented")
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
debug.PrintStack()
|
||||
|
||||
return gqlerror.Errorf("internal system error")
|
||||
}
|
||||
|
||||
var _ msgbus.MsgbusResolver = (*noop)(nil)
|
||||
var _ salty.SaltyResolver = (*noop)(nil)
|
||||
var _ es.EventResolver = (*noop)(nil)
|
||||
|
||||
func (*noop) CreateSaltyUser(ctx context.Context, nick string, pubkey string) (*salty.SaltyUser, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) Posts(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) SaltyUser(ctx context.Context, nick string) (*salty.SaltyUser, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) PostAdded(ctx context.Context, streamID string, after int64) (<-chan *msgbus.PostEvent, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (*noop) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *es.GQLEvent, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (*noop) RegisterHTTP(*http.ServeMux) {}
|
16
app/msgbus/msgbus.graphqls
Normal file
16
app/msgbus/msgbus.graphqls
Normal file
|
@ -0,0 +1,16 @@
|
|||
extend type Query {
|
||||
posts(streamID: String! paging: PageInput): Connection!
|
||||
}
|
||||
extend type Subscription {
|
||||
"""after == 0 start from begining, after == -1 start from end"""
|
||||
postAdded(streamID: String! after: Int! = -1): PostEvent
|
||||
}
|
||||
type PostEvent implements Edge @goModel(model: "github.com/sour-is/ev/app/msgbus.PostEvent") {
|
||||
id: ID!
|
||||
|
||||
payload: String!
|
||||
payloadJSON: Map!
|
||||
tags: [String!]!
|
||||
|
||||
meta: Meta!
|
||||
}
|
573
app/msgbus/service.go
Normal file
573
app/msgbus/service.go
Normal file
|
@ -0,0 +1,573 @@
|
|||
package msgbus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
"github.com/sour-is/ev/pkg/gql"
|
||||
"go.opentelemetry.io/otel/metric/instrument"
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||
"go.opentelemetry.io/otel/metric/unit"
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
es *es.EventStore
|
||||
|
||||
m_gql_posts syncint64.Counter
|
||||
m_gql_post_added syncint64.Counter
|
||||
m_gql_post_added_event syncint64.Counter
|
||||
m_req_time syncint64.Histogram
|
||||
}
|
||||
|
||||
type MsgbusResolver interface {
|
||||
Posts(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error)
|
||||
PostAdded(ctx context.Context, streamID string, after int64) (<-chan *PostEvent, error)
|
||||
RegisterHTTP(mux *http.ServeMux)
|
||||
}
|
||||
|
||||
func New(ctx context.Context, es *es.EventStore) (*service, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if err := event.Register(ctx, &PostEvent{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := event.RegisterName(ctx, "domain.PostEvent", &PostEvent{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := lg.Meter(ctx)
|
||||
|
||||
svc := &service{es: es}
|
||||
|
||||
var err, errs error
|
||||
svc.m_gql_posts, err = m.SyncInt64().Counter("msgbus_posts",
|
||||
instrument.WithDescription("msgbus graphql posts requests"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_gql_post_added, err = m.SyncInt64().Counter("msgbus_post_added",
|
||||
instrument.WithDescription("msgbus graphql post added subcription requests"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_gql_post_added_event, err = m.SyncInt64().Counter("msgbus_post_event",
|
||||
instrument.WithDescription("msgbus graphql post added subscription events"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_req_time, err = m.SyncInt64().Histogram("msgbus_request_time",
|
||||
instrument.WithDescription("msgbus graphql post added subscription events"),
|
||||
instrument.WithUnit(unit.Unit("ns")),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
span.RecordError(err)
|
||||
|
||||
return svc, errs
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||
mux.Handle("/inbox/", lg.Htrace(http.StripPrefix("/inbox/", s), "inbox"))
|
||||
}
|
||||
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
s.websocket(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
s.get(w, r)
|
||||
case http.MethodPost, http.MethodPut:
|
||||
s.post(w, r)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// Posts is the resolver for the events field.
|
||||
func (s *service) Posts(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s.m_gql_posts.Add(ctx, 1)
|
||||
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
lis, err := s.es.Read(ctx, streamID, paging.GetIdx(0), paging.GetCount(30))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edges := make([]gql.Edge, 0, len(lis))
|
||||
for i := range lis {
|
||||
span.AddEvent(fmt.Sprint("post ", i, " of ", len(lis)))
|
||||
e := lis[i]
|
||||
|
||||
post, ok := e.(*PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
edges = append(edges, post)
|
||||
}
|
||||
|
||||
var first, last uint64
|
||||
if first, err = s.es.FirstIndex(ctx, streamID); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
if last, err = s.es.LastIndex(ctx, streamID); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gql.Connection{
|
||||
Paging: &gql.PageInfo{
|
||||
Next: lis.Last().EventMeta().Position < last,
|
||||
Prev: lis.First().EventMeta().Position > first,
|
||||
Begin: lis.First().EventMeta().Position,
|
||||
End: lis.Last().EventMeta().Position,
|
||||
},
|
||||
Edges: edges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *service) PostAdded(ctx context.Context, streamID string, after int64) (<-chan *PostEvent, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
r.m_gql_post_added.Add(ctx, 1)
|
||||
|
||||
es := r.es.EventStream()
|
||||
if es == nil {
|
||||
return nil, fmt.Errorf("EventStore does not implement streaming")
|
||||
}
|
||||
|
||||
sub, err := es.Subscribe(ctx, streamID, after)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan *PostEvent)
|
||||
|
||||
go func() {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
{
|
||||
ctx, span := lg.Fork(ctx)
|
||||
defer func() {
|
||||
defer span.End()
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
err := sub.Close(ctx)
|
||||
span.RecordError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
for sub.Recv(ctx) {
|
||||
events, err := sub.Events(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
break
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("received %d events", len(events)))
|
||||
r.m_gql_post_added_event.Add(ctx, int64(len(events)))
|
||||
|
||||
for _, e := range events {
|
||||
if p, ok := e.(*PostEvent); ok {
|
||||
select {
|
||||
case ch <- p:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (s *service) get(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
name, _, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
var pos, count int64 = 0, es.AllEvents
|
||||
qry := r.URL.Query()
|
||||
|
||||
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 1 {
|
||||
pos = i - 1
|
||||
}
|
||||
if i, err := strconv.ParseInt(qry.Get("pos"), 10, 64); err == nil {
|
||||
pos = i
|
||||
}
|
||||
if i, err := strconv.ParseInt(qry.Get("n"), 10, 64); err == nil {
|
||||
count = i
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("GET topic=", name, " idx=", pos, " n=", count))
|
||||
events, err := s.es.Read(ctx, "post-"+name, pos, count)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
if err = encodeJSON(w, first, events...); err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i := range events {
|
||||
fmt.Fprintln(w, events[i])
|
||||
}
|
||||
}
|
||||
func (s *service) post(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
name, tags, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
events := event.NewEvents(&PostEvent{
|
||||
payload: b,
|
||||
tags: fields(tags),
|
||||
})
|
||||
|
||||
_, err = s.es.Append(ctx, "post-"+name, events)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if first == event.NilEvent {
|
||||
first = events.First()
|
||||
}
|
||||
|
||||
m := events.First().EventMeta()
|
||||
span.AddEvent(fmt.Sprint("POST topic=", name, " tags=", tags, " idx=", m.Position, " id=", m.EventID))
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
if strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err = encodeJSON(w, first, events...); err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
span.AddEvent("finish response")
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
fmt.Fprintf(w, "OK %d %s", m.Position, m.EventID)
|
||||
}
|
||||
func (s *service) websocket(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
name, _, _ := strings.Cut(r.URL.Path, "/")
|
||||
if name == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var first event.Event = event.NilEvent
|
||||
if lis, err := s.es.Read(ctx, "post-"+name, 0, 1); err == nil && len(lis) > 0 {
|
||||
first = lis[0]
|
||||
}
|
||||
|
||||
var pos int64 = 0
|
||||
qry := r.URL.Query()
|
||||
|
||||
if i, err := strconv.ParseInt(qry.Get("index"), 10, 64); err == nil && i > 0 {
|
||||
pos = i - 1
|
||||
}
|
||||
|
||||
span.AddEvent(fmt.Sprint("WS topic=", name, " idx=", pos))
|
||||
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
c.SetCloseHandler(func(code int, text string) error {
|
||||
cancel()
|
||||
return nil
|
||||
})
|
||||
go func() {
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
mt, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("recv: %d %s", mt, message))
|
||||
}
|
||||
}()
|
||||
|
||||
es := s.es.EventStream()
|
||||
if es == nil {
|
||||
span.AddEvent("EventStore does not implement streaming")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sub, err := es.Subscribe(ctx, "post-"+name, pos)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return
|
||||
}
|
||||
{
|
||||
ctx, span := lg.Fork(ctx)
|
||||
defer func() {
|
||||
defer span.End()
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
err := sub.Close(ctx)
|
||||
span.RecordError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
span.AddEvent("start ws")
|
||||
for sub.Recv(ctx) {
|
||||
events, err := sub.Events(ctx)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
span.AddEvent(fmt.Sprint("got events ", len(events)))
|
||||
for i := range events {
|
||||
e, ok := events[i].(*PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
span.AddEvent(fmt.Sprint("send", i, e.String()))
|
||||
|
||||
var b bytes.Buffer
|
||||
if err = encodeJSON(&b, first, e); err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
|
||||
err = c.WriteMessage(websocket.TextMessage, b.Bytes())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PostEvent struct {
|
||||
payload []byte
|
||||
tags []string
|
||||
|
||||
eventMeta event.Meta
|
||||
}
|
||||
|
||||
func (e *PostEvent) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
func (e *PostEvent) SetEventMeta(eventMeta event.Meta) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.eventMeta = eventMeta
|
||||
}
|
||||
func (e *PostEvent) Values() any {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return struct {
|
||||
Payload []byte
|
||||
Tags []string
|
||||
}{
|
||||
Payload: e.payload,
|
||||
Tags: e.tags,
|
||||
}
|
||||
}
|
||||
func (e *PostEvent) MarshalBinary() ([]byte, error) {
|
||||
j := e.Values()
|
||||
return json.Marshal(&j)
|
||||
}
|
||||
func (e *PostEvent) UnmarshalBinary(b []byte) error {
|
||||
j := struct {
|
||||
Payload []byte
|
||||
Tags []string
|
||||
}{}
|
||||
err := json.Unmarshal(b, &j)
|
||||
e.payload = j.Payload
|
||||
e.tags = j.Tags
|
||||
|
||||
return err
|
||||
}
|
||||
func (e *PostEvent) MarshalJSON() ([]byte, error) { return e.MarshalBinary() }
|
||||
func (e *PostEvent) UnmarshalJSON(b []byte) error { return e.UnmarshalBinary(b) }
|
||||
|
||||
func (e *PostEvent) ID() string { return e.eventMeta.GetEventID() }
|
||||
func (e *PostEvent) Tags() []string { return e.tags }
|
||||
func (e *PostEvent) Payload() string { return string(e.payload) }
|
||||
func (e *PostEvent) PayloadJSON(ctx context.Context) (m map[string]interface{}, err error) {
|
||||
err = json.Unmarshal([]byte(e.payload), &m)
|
||||
return
|
||||
}
|
||||
func (e *PostEvent) Meta() *event.Meta { return &e.eventMeta }
|
||||
func (e *PostEvent) IsEdge() {}
|
||||
|
||||
func (e *PostEvent) String() string {
|
||||
var b bytes.Buffer
|
||||
|
||||
// b.WriteString(e.eventMeta.StreamID)
|
||||
// b.WriteRune('@')
|
||||
b.WriteString(strconv.FormatUint(e.eventMeta.Position, 10))
|
||||
b.WriteRune('\t')
|
||||
|
||||
b.WriteString(e.eventMeta.EventID.String())
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(string(e.payload))
|
||||
if len(e.tags) > 0 {
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(strings.Join(e.tags, ","))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func fields(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, "/")
|
||||
}
|
||||
|
||||
func encodeJSON(w io.Writer, first event.Event, events ...event.Event) error {
|
||||
out := make([]struct {
|
||||
ID uint64 `json:"id"`
|
||||
Payload []byte `json:"payload"`
|
||||
Created string `json:"created"`
|
||||
Tags []string `json:"tags"`
|
||||
Topic struct {
|
||||
Name string `json:"name"`
|
||||
TTL uint64 `json:"ttl"`
|
||||
Seq uint64 `json:"seq"`
|
||||
Created string `json:"created"`
|
||||
} `json:"topic"`
|
||||
}, len(events))
|
||||
|
||||
for i := range events {
|
||||
e, ok := events[i].(*PostEvent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[i].ID = e.EventMeta().Position
|
||||
out[i].Created = e.EventMeta().Created().Format(time.RFC3339Nano)
|
||||
out[i].Payload = e.payload
|
||||
out[i].Tags = e.tags
|
||||
out[i].Topic.Name = strings.TrimPrefix(e.EventMeta().StreamID, "post-")
|
||||
out[i].Topic.Created = first.EventMeta().Created().Format(time.RFC3339Nano)
|
||||
out[i].Topic.Seq = e.EventMeta().Position
|
||||
}
|
||||
|
||||
if len(out) == 1 {
|
||||
return json.NewEncoder(w).Encode(out[0])
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(out)
|
||||
}
|
25
app/msgbus/service_test.go
Normal file
25
app/msgbus/service_test.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package msgbus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
m := &PostEvent{}
|
||||
s := `{"Payload":"QkVHSU4gU0FMVFBBQ0sgRU5DUllQVEVEIE1FU1NBR0UuIGtlRElETVFXWXZWUjU4QiBGVGZUZURRTkhzT2ZuZWMgWUV1dkNYSTNoVDBYZzZKIDJxeXBJcmdsT3ZlZjlqciA0dHVQaWpJRVgxdlpoTEkgUTVKTzNvYVY5cnNna01BIEFOeTJwYjg2Qkd6N0JGMCA4MXJuZk9OV2RRM0VldFAgSmU0ZFlHeUI4NkRydkVrIGNqcFpoajNmcEJUcDdiZiBpMktwRDJQM1kzNVJBQU8gWmIyZGZtOVpneHZNSVJ2IDJsVVRCWTQxVEtZNkJhTyB2NGVIeXF1MENjQkR4dW8gSEZIekxJd3BBb3ZoRGt1IGFJdXRZYzdhZ3puMUxvNCBZQWFyUDZxVVVtTVlrQXAgYkdSYTZLZWVOa3ZzTDdMIHFoMWd6WUlnS2l6cW51eCB1SVQ0QTdaU1BscWxlR1IgbTk3M2ZoNUduWEZTM3MwIDJzQ2FvclpmN2c1RUo5TiBlS1hkZkFSMWF6TVRBek8gSmNEM1hDNDBwVTRpaG9mIE8wYnB2RU1UOVlUb3ZOWCBobVUxZWZ6enpyMUFDdXcgWExwcUhlVXNXdEtGcXRnIHdyWEZleExBYU50T21jRSBOeFFDUi4gRU5EIFNBTFRQQUNLIEVOQ1JZUFRFRCBNRVNTQUdFLgo=","Tags":null}`
|
||||
|
||||
err := json.Unmarshal([]byte(s), m)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func TestMarshal(t *testing.T) {
|
||||
m := &PostEvent{
|
||||
payload: []byte("QkVHSU4gU0FMVFBBQ0sgRU5DUllQVEVEIE1FU1NBR0UuIGtlRElETVFXWXZWUjU4QiBGVGZUZURRTkhzT2ZuZWMgWUV1dkNYSTNoVDBYZzZKIDJxeXBJcmdsT3ZlZjlqciA0dHVQaWpJRVgxdlpoTEkgUTVKTzNvYVY5cnNna01BIEFOeTJwYjg2Qkd6N0JGMCA4MXJuZk9OV2RRM0VldFAgSmU0ZFlHeUI4NkRydkVrIGNqcFpoajNmcEJUcDdiZiBpMktwRDJQM1kzNVJBQU8gWmIyZGZtOVpneHZNSVJ2IDJsVVRCWTQxVEtZNkJhTyB2NGVIeXF1MENjQkR4dW8gSEZIekxJd3BBb3ZoRGt1IGFJdXRZYzdhZ3puMUxvNCBZQWFyUDZxVVVtTVlrQXAgYkdSYTZLZWVOa3ZzTDdMIHFoMWd6WUlnS2l6cW51eCB1SVQ0QTdaU1BscWxlR1IgbTk3M2ZoNUduWEZTM3MwIDJzQ2FvclpmN2c1RUo5TiBlS1hkZkFSMWF6TVRBek8gSmNEM1hDNDBwVTRpaG9mIE8wYnB2RU1UOVlUb3ZOWCBobVUxZWZ6enpyMUFDdXcgWExwcUhlVXNXdEtGcXRnIHdyWEZleExBYU50T21jRSBOeFFDUi4gRU5EIFNBTFRQQUNLIEVOQ1JZUFRFRCBNRVNTQUdFLgo="),
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
t.Log(err)
|
||||
|
||||
err = json.Unmarshal(b, m)
|
||||
t.Log(err)
|
||||
}
|
183
app/peerfinder/peer.go
Normal file
183
app/peerfinder/peer.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
package peerfinder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
eventMeta event.Meta
|
||||
|
||||
RequestIP string `json:"req_ip"`
|
||||
Hidden bool `json:"hide,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Request) StreamID() string {
|
||||
return r.EventMeta().GetEventID()
|
||||
}
|
||||
func (r *Request) RequestID() string {
|
||||
return r.EventMeta().GetEventID()
|
||||
}
|
||||
func (r *Request) Created() time.Time {
|
||||
return r.EventMeta().Created()
|
||||
}
|
||||
func (r *Request) CreatedString() string {
|
||||
return r.Created().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
func (r *Request) Family() int {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(r.RequestIP)
|
||||
switch {
|
||||
case err != nil:
|
||||
return 0
|
||||
case ip.Is4():
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
var _ event.Event = (*Request)(nil)
|
||||
|
||||
func (e *Request) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
func (e *Request) SetEventMeta(m event.Meta) {
|
||||
if e != nil {
|
||||
e.eventMeta = m
|
||||
}
|
||||
}
|
||||
func (e *Request) MarshalBinary() (text []byte, err error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
func (e *Request) UnmarshalBinary(b []byte) error {
|
||||
return json.Unmarshal(b, e)
|
||||
}
|
||||
func (e *Request) MarshalEnviron() ([]byte, error) {
|
||||
if e == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
b.WriteString("REQ_ID=")
|
||||
b.WriteString(e.RequestID())
|
||||
b.WriteRune('\n')
|
||||
|
||||
b.WriteString("REQ_IP=")
|
||||
b.WriteString(e.RequestIP)
|
||||
b.WriteRune('\n')
|
||||
|
||||
b.WriteString("REQ_FAMILY=")
|
||||
if family := e.Family(); family > 0 {
|
||||
b.WriteString(strconv.Itoa(family))
|
||||
}
|
||||
b.WriteRune('\n')
|
||||
|
||||
b.WriteString("REQ_CREATED=")
|
||||
b.WriteString(e.CreatedString())
|
||||
b.WriteRune('\n')
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
eventMeta event.Meta
|
||||
|
||||
RequestID string `json:"req_id"`
|
||||
PeerID string `json:"peer_id"`
|
||||
PeerVersion string `json:"peer_version"`
|
||||
Latency float64 `json:"latency,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Result) Created() time.Time {
|
||||
return r.eventMeta.Created()
|
||||
}
|
||||
|
||||
var _ event.Event = (*Result)(nil)
|
||||
|
||||
func (e *Result) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
func (e *Result) SetEventMeta(m event.Meta) {
|
||||
if e != nil {
|
||||
e.eventMeta = m
|
||||
}
|
||||
}
|
||||
func (e *Result) MarshalBinary() (text []byte, err error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
func (e *Result) UnmarshalBinary(b []byte) error {
|
||||
return json.Unmarshal(b, e)
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
ScriptVersion string `json:"script_version"`
|
||||
|
||||
event.AggregateRoot
|
||||
}
|
||||
|
||||
var _ event.Aggregate = (*Info)(nil)
|
||||
|
||||
func (a *Info) ApplyEvent(lis ...event.Event) {
|
||||
for _, e := range lis {
|
||||
switch e := e.(type) {
|
||||
case *VersionChanged:
|
||||
a.ScriptVersion = e.ScriptVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
func (a *Info) MarshalEnviron() ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
b.WriteString("SCRIPT_VERSION=")
|
||||
b.WriteString(a.ScriptVersion)
|
||||
b.WriteRune('\n')
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
func (a *Info) OnCreate() error {
|
||||
if a.StreamVersion() == 0 {
|
||||
event.Raise(a, &VersionChanged{ScriptVersion: initVersion})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type VersionChanged struct {
|
||||
ScriptVersion string `json:"script_version"`
|
||||
|
||||
eventMeta event.Meta
|
||||
}
|
||||
|
||||
var _ event.Event = (*VersionChanged)(nil)
|
||||
|
||||
func (e *VersionChanged) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
func (e *VersionChanged) SetEventMeta(m event.Meta) {
|
||||
if e != nil {
|
||||
e.eventMeta = m
|
||||
}
|
||||
}
|
||||
func (e *VersionChanged) MarshalBinary() (text []byte, err error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
func (e *VersionChanged) UnmarshalBinary(b []byte) error {
|
||||
return json.Unmarshal(b, e)
|
||||
}
|
268
app/peerfinder/service.go
Normal file
268
app/peerfinder/service.go
Normal file
|
@ -0,0 +1,268 @@
|
|||
package peerfinder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ulid "github.com/oklog/ulid/v2"
|
||||
contentnegotiation "gitlab.com/jamietanna/content-negotiation-go"
|
||||
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
const (
|
||||
queueRequests = "pf-requests"
|
||||
queueResponses = "pf-response-"
|
||||
aggInfo = "pf-info"
|
||||
initVersion = "1.1.0"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
es *es.EventStore
|
||||
}
|
||||
|
||||
func New(ctx context.Context, es *es.EventStore) (*service, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if err := event.Register(ctx, &Request{}, &Result{}, &VersionChanged{}); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc := &service{es: es}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||
mux.Handle("/peers/", lg.Htrace(s, "peers"))
|
||||
|
||||
}
|
||||
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/peers/pending/"):
|
||||
s.getPending(w, r, strings.TrimPrefix(r.URL.Path, "/peers/pending/"))
|
||||
return
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/peers/req/"):
|
||||
s.getResults(w, r, strings.TrimPrefix(r.URL.Path, "/peers/req/"))
|
||||
return
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
case http.MethodPost:
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/peers/req/"):
|
||||
s.postResult(w, r, strings.TrimPrefix(r.URL.Path, "/peers/req/"))
|
||||
return
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/peers/req"):
|
||||
s.postRequest(w, r)
|
||||
return
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) getPending(w http.ResponseWriter, r *http.Request, uuid string) {
|
||||
ctx := r.Context()
|
||||
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
info, err := es.Upsert(ctx, s.es, "pf-info", func(ctx context.Context, agg *Info) error {
|
||||
return agg.OnCreate() // initialize if not exists
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
requests, err := s.es.Read(ctx, queueRequests, -1, -30)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
responses, err := s.es.Read(ctx, queueResponses+uuid, -1, -30)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
req := filter(requests, responses)
|
||||
|
||||
negotiator := contentnegotiation.NewNegotiator("application/json", "text/environment", "text/plain", "text/html")
|
||||
negotiated, _, err := negotiator.Negotiate(r.Header.Get("Accept"))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
|
||||
span.AddEvent(negotiated.String())
|
||||
switch negotiated.String() {
|
||||
case "text/environment":
|
||||
_, err = encodeTo(w, info.MarshalEnviron, req.MarshalEnviron)
|
||||
case "application/json":
|
||||
var out interface{} = info
|
||||
if req != nil {
|
||||
out = struct {
|
||||
ScriptVersion string `json:"script_version"`
|
||||
RequestID string `json:"req_id"`
|
||||
RequestIP string `json:"req_ip"`
|
||||
Family string `json:"req_family"`
|
||||
Created string `json:"req_created"`
|
||||
}{
|
||||
info.ScriptVersion,
|
||||
req.RequestID(),
|
||||
req.RequestIP,
|
||||
strconv.Itoa(req.Family()),
|
||||
req.CreatedString(),
|
||||
}
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
span.RecordError(err)
|
||||
}
|
||||
func (s *service) getResults(w http.ResponseWriter, r *http.Request, uuid string) {
|
||||
ctx := r.Context()
|
||||
|
||||
responses, err := s.es.Read(ctx, queueResponses+uuid, -1, es.AllEvents)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
negotiator := contentnegotiation.NewNegotiator("application/json", "text/environment", "text/csv", "text/plain", "text/html")
|
||||
negotiated, _, err := negotiator.Negotiate("application/json")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
switch negotiated.String() {
|
||||
// case "text/environment":
|
||||
// encodeTo(w, responses.MarshalBinary)
|
||||
case "application/json":
|
||||
json.NewEncoder(w).Encode(responses)
|
||||
}
|
||||
}
|
||||
func (s *service) postRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
req := &Request{
|
||||
RequestIP: r.Form.Get("req_ip"),
|
||||
}
|
||||
|
||||
if hidden, err := strconv.ParseBool(r.Form.Get("req_hidden")); err != nil {
|
||||
req.Hidden = hidden
|
||||
}
|
||||
|
||||
s.es.Append(ctx, queueRequests, event.NewEvents(req))
|
||||
}
|
||||
func (s *service) postResult(w http.ResponseWriter, r *http.Request, id string) {
|
||||
ctx := r.Context()
|
||||
|
||||
if _, err := ulid.ParseStrict(id); err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
latency, err := strconv.ParseFloat(r.Form.Get("res_latency"), 64)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
req := &Result{
|
||||
RequestID: id,
|
||||
PeerID: r.Form.Get("peer_id"),
|
||||
PeerVersion: r.Form.Get("peer_version"),
|
||||
Latency: latency,
|
||||
}
|
||||
|
||||
s.es.Append(ctx, queueResponses+id, event.NewEvents(req))
|
||||
}
|
||||
|
||||
func filter(requests, responses event.Events) *Request {
|
||||
have := make(map[string]struct{}, len(responses))
|
||||
for _, res := range toList[Result](responses...) {
|
||||
have[res.RequestID] = struct{}{}
|
||||
}
|
||||
for _, req := range reverse(toList[Request](requests...)...) {
|
||||
if _, ok := have[req.RequestID()]; !ok {
|
||||
return req
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func toList[E any, T es.PE[E]](lis ...event.Event) []T {
|
||||
newLis := make([]T, 0, len(lis))
|
||||
for i := range lis {
|
||||
if e, ok := lis[i].(T); ok {
|
||||
newLis = append(newLis, e)
|
||||
}
|
||||
}
|
||||
return newLis
|
||||
}
|
||||
func reverse[T any](s ...T) []T {
|
||||
first, last := 0, len(s)-1
|
||||
for first < last {
|
||||
s[first], s[last] = s[last], s[first]
|
||||
first++
|
||||
last--
|
||||
}
|
||||
return s
|
||||
}
|
||||
func encodeTo(w io.Writer, fns ...func() ([]byte, error)) (int, error) {
|
||||
i := 0
|
||||
for _, fn := range fns {
|
||||
b, err := fn()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
j, err := w.Write(b)
|
||||
i += j
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
return i, nil
|
||||
}
|
155
app/salty/salty-addr.go
Normal file
155
app/salty/salty-addr.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package salty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/keys-pub/keys"
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
)
|
||||
|
||||
// Config represents a Salty Config for a User which at a minimum is required
|
||||
// to have an Endpoint and Key (Public Key)
|
||||
type Config struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type Capabilities struct {
|
||||
AcceptEncoding string
|
||||
}
|
||||
|
||||
func (c Capabilities) String() string {
|
||||
if c.AcceptEncoding == "" {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprint("accept-encoding: ", c.AcceptEncoding)
|
||||
}
|
||||
|
||||
type Addr struct {
|
||||
User string
|
||||
Domain string
|
||||
|
||||
capabilities Capabilities
|
||||
discoveredDomain string
|
||||
dns DNSResolver
|
||||
endpoint *url.URL
|
||||
key *keys.EdX25519PublicKey
|
||||
}
|
||||
|
||||
// ParseAddr parsers a Salty Address for a user into it's user and domain
|
||||
// parts and returns an Addr object with the User and Domain and a method
|
||||
// for returning the expected User's Well-Known URI
|
||||
func (s *service) ParseAddr(addr string) (*Addr, error) {
|
||||
parts := strings.Split(strings.ToLower(addr), "@")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("expected nick@domain found %q", addr)
|
||||
}
|
||||
|
||||
return &Addr{User: parts[0], Domain: parts[1], dns: s.dns}, nil
|
||||
}
|
||||
|
||||
func (a *Addr) String() string {
|
||||
return fmt.Sprintf("%s@%s", a.User, a.Domain)
|
||||
}
|
||||
|
||||
// Hash returns the Hex(SHA256Sum()) of the Address
|
||||
func (a *Addr) Hash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.ToLower(a.String()))))
|
||||
}
|
||||
|
||||
// URI returns the Well-Known URI for this Addr
|
||||
func (a *Addr) URI() string {
|
||||
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.User)
|
||||
}
|
||||
|
||||
// HashURI returns the Well-Known HashURI for this Addr
|
||||
func (a *Addr) HashURI() string {
|
||||
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.Hash())
|
||||
}
|
||||
|
||||
// DiscoveredDomain returns the discovered domain (if any) of fallbacks to the Domain
|
||||
func (a *Addr) DiscoveredDomain() string {
|
||||
if a.discoveredDomain != "" {
|
||||
return a.discoveredDomain
|
||||
}
|
||||
return a.Domain
|
||||
}
|
||||
|
||||
func (a *Addr) Refresh(ctx context.Context) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.AddEvent(fmt.Sprintf("Looking up SRV record for _salty._tcp.%s", a.Domain))
|
||||
if _, srv, err := a.dns.LookupSRV(ctx, "salty", "tcp", a.Domain); err == nil {
|
||||
if len(srv) > 0 {
|
||||
a.discoveredDomain = strings.TrimSuffix(srv[0].Target, ".")
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("Discovered salty services %s", a.discoveredDomain))
|
||||
} else if err != nil {
|
||||
span.RecordError(fmt.Errorf("error looking up SRV record for _salty._tcp.%s : %s", a.Domain, err))
|
||||
}
|
||||
|
||||
config, cap, err := fetchConfig(ctx, a.HashURI())
|
||||
if err != nil {
|
||||
// Fallback to plain user nick
|
||||
span.RecordError(err)
|
||||
|
||||
config, cap, err = fetchConfig(ctx, a.URI())
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error looking up user %s: %w", a, err)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(config.Key))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error parsing public key %s: %w", config.Key, err)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
a.key = key
|
||||
|
||||
u, err := url.Parse(config.Endpoint)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
a.endpoint = u
|
||||
a.capabilities = cap
|
||||
|
||||
span.AddEvent(fmt.Sprintf("Discovered endpoint: %v", a.endpoint))
|
||||
span.AddEvent(fmt.Sprintf("Discovered capability: %v", a.capabilities))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchConfig(ctx context.Context, addr string) (config Config, cap Capabilities, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(res.Body).Decode(&config); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cap.AcceptEncoding = res.Header.Get("Accept-Encoding")
|
||||
|
||||
return
|
||||
}
|
100
app/salty/salty-user.go
Normal file
100
app/salty/salty-user.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package salty
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
|
||||
"github.com/keys-pub/keys"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
"github.com/sour-is/ev/pkg/gql"
|
||||
)
|
||||
|
||||
type SaltyUser struct {
|
||||
name string
|
||||
pubkey *keys.EdX25519PublicKey
|
||||
inbox ulid.ULID
|
||||
|
||||
event.AggregateRoot
|
||||
}
|
||||
|
||||
var _ event.Aggregate = (*SaltyUser)(nil)
|
||||
|
||||
// ApplyEvent applies the event to the aggrigate state
|
||||
func (a *SaltyUser) ApplyEvent(lis ...event.Event) {
|
||||
for _, e := range lis {
|
||||
switch e := e.(type) {
|
||||
case *UserRegistered:
|
||||
a.name = e.Name
|
||||
a.pubkey = e.Pubkey
|
||||
a.inbox = e.EventMeta().EventID
|
||||
a.SetStreamID(a.streamID())
|
||||
default:
|
||||
log.Printf("unknown event %T", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *SaltyUser) streamID() string {
|
||||
return fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(a.name))))
|
||||
}
|
||||
|
||||
func (a *SaltyUser) OnUserRegister(name string, pubkey *keys.EdX25519PublicKey) error {
|
||||
event.Raise(a, &UserRegistered{Name: name, Pubkey: pubkey})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *SaltyUser) Nick() string { return a.name }
|
||||
func (a *SaltyUser) Inbox() string { return a.inbox.String() }
|
||||
func (a *SaltyUser) Pubkey() string { return a.pubkey.String() }
|
||||
func (s *SaltyUser) Endpoint(ctx context.Context) (string, error) {
|
||||
svc := gql.FromContext[contextKey, *service](ctx, saltyKey)
|
||||
return url.JoinPath(svc.BaseURL(), s.inbox.String())
|
||||
}
|
||||
|
||||
type UserRegistered struct {
|
||||
Name string
|
||||
Pubkey *keys.EdX25519PublicKey
|
||||
|
||||
eventMeta event.Meta
|
||||
}
|
||||
|
||||
var _ event.Event = (*UserRegistered)(nil)
|
||||
|
||||
func (e *UserRegistered) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
func (e *UserRegistered) SetEventMeta(m event.Meta) {
|
||||
if e != nil {
|
||||
e.eventMeta = m
|
||||
}
|
||||
}
|
||||
func (e *UserRegistered) MarshalBinary() (text []byte, err error) {
|
||||
var b bytes.Buffer
|
||||
b.WriteString(e.Name)
|
||||
b.WriteRune('\t')
|
||||
b.WriteString(e.Pubkey.String())
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
func (e *UserRegistered) UnmarshalBinary(b []byte) error {
|
||||
name, pub, ok := bytes.Cut(b, []byte{'\t'})
|
||||
if !ok {
|
||||
return fmt.Errorf("parse error")
|
||||
}
|
||||
|
||||
var err error
|
||||
e.Name = string(name)
|
||||
e.Pubkey, err = keys.NewEdX25519PublicKeyFromID(keys.ID(pub))
|
||||
|
||||
return err
|
||||
}
|
14
app/salty/salty.graphqls
Normal file
14
app/salty/salty.graphqls
Normal file
|
@ -0,0 +1,14 @@
|
|||
extend type Query {
|
||||
saltyUser(nick: String!): SaltyUser
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createSaltyUser(nick: String! pubkey: String!): SaltyUser
|
||||
}
|
||||
|
||||
type SaltyUser @goModel(model: "github.com/sour-is/ev/app/salty.SaltyUser"){
|
||||
nick: String!
|
||||
pubkey: String!
|
||||
inbox: String!
|
||||
endpoint: String!
|
||||
}
|
299
app/salty/service.go
Normal file
299
app/salty/service.go
Normal file
|
@ -0,0 +1,299 @@
|
|||
package salty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/keys-pub/keys"
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
"github.com/sour-is/ev/pkg/gql"
|
||||
"go.opentelemetry.io/otel/metric/instrument"
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||
"go.opentelemetry.io/otel/metric/unit"
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
type DNSResolver interface {
|
||||
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
baseURL string
|
||||
es *es.EventStore
|
||||
dns DNSResolver
|
||||
|
||||
m_create_user syncint64.Counter
|
||||
m_get_user syncint64.Counter
|
||||
m_api_ping syncint64.Counter
|
||||
m_api_register syncint64.Counter
|
||||
m_api_lookup syncint64.Counter
|
||||
m_api_send syncint64.Counter
|
||||
m_req_time syncint64.Histogram
|
||||
}
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var saltyKey = contextKey{"salty"}
|
||||
|
||||
type SaltyResolver interface {
|
||||
CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error)
|
||||
SaltyUser(ctx context.Context, nick string) (*SaltyUser, error)
|
||||
RegisterHTTP(mux *http.ServeMux)
|
||||
}
|
||||
|
||||
func New(ctx context.Context, es *es.EventStore, baseURL string) (*service, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if err := event.Register(ctx, &UserRegistered{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := event.RegisterName(ctx, "domain.UserRegistered", &UserRegistered{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := lg.Meter(ctx)
|
||||
|
||||
svc := &service{baseURL: baseURL, es: es, dns: net.DefaultResolver}
|
||||
|
||||
var err, errs error
|
||||
svc.m_create_user, err = m.SyncInt64().Counter("salty_create_user",
|
||||
instrument.WithDescription("salty create user graphql called"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_get_user, err = m.SyncInt64().Counter("salty_get_user",
|
||||
instrument.WithDescription("salty get user graphql called"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_api_ping, err = m.SyncInt64().Counter("salty_api_ping",
|
||||
instrument.WithDescription("salty api ping called"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_api_register, err = m.SyncInt64().Counter("salty_api_register",
|
||||
instrument.WithDescription("salty api register"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_api_lookup, err = m.SyncInt64().Counter("salty_api_lookup",
|
||||
instrument.WithDescription("salty api ping lookup"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_api_send, err = m.SyncInt64().Counter("salty_api_send",
|
||||
instrument.WithDescription("salty api ping send"),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
svc.m_req_time, err = m.SyncInt64().Histogram("salty_request_time",
|
||||
instrument.WithDescription("histogram of requests"),
|
||||
instrument.WithUnit(unit.Unit("ns")),
|
||||
)
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
span.RecordError(err)
|
||||
|
||||
return svc, errs
|
||||
}
|
||||
|
||||
func (s *service) BaseURL() string {
|
||||
if s == nil {
|
||||
return "http://missing.context/"
|
||||
}
|
||||
return s.baseURL
|
||||
}
|
||||
func (s *service) RegisterHTTP(mux *http.ServeMux) {
|
||||
mux.Handle("/.well-known/salty/", lg.Htrace(s, "lookup"))
|
||||
}
|
||||
func (s *service) RegisterAPIv1(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/ping", s.apiv1)
|
||||
mux.HandleFunc("/register", s.apiv1)
|
||||
mux.HandleFunc("/lookup/", s.apiv1)
|
||||
mux.HandleFunc("/send", s.apiv1)
|
||||
}
|
||||
func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
addr := "saltyuser-" + strings.TrimPrefix(r.URL.Path, "/.well-known/salty/")
|
||||
addr = strings.TrimSuffix(addr, ".json")
|
||||
|
||||
span.AddEvent(fmt.Sprint("find ", addr))
|
||||
a, err := es.Update(ctx, s.es, addr, func(ctx context.Context, agg *SaltyUser) error { return nil })
|
||||
switch {
|
||||
case errors.Is(err, event.ErrShouldExist):
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
basePath, _ := url.JoinPath(s.baseURL, a.inbox.String())
|
||||
|
||||
err = json.NewEncoder(w).Encode(
|
||||
struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Key string `json:"key"`
|
||||
}{
|
||||
Endpoint: basePath,
|
||||
Key: a.pubkey.ID().String(),
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
}
|
||||
func (s *service) CreateSaltyUser(ctx context.Context, nick string, pub string) (*SaltyUser, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s.m_create_user.Add(ctx, 1)
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
|
||||
span.AddEvent(streamID)
|
||||
|
||||
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(pub))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a, err := es.Create(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error {
|
||||
return agg.OnUserRegister(nick, key)
|
||||
})
|
||||
switch {
|
||||
case errors.Is(err, es.ErrShouldNotExist):
|
||||
span.RecordError(err)
|
||||
return nil, fmt.Errorf("user exists")
|
||||
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
return nil, fmt.Errorf("internal error")
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
func (s *service) SaltyUser(ctx context.Context, nick string) (*SaltyUser, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s.m_get_user.Add(ctx, 1)
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Milliseconds())
|
||||
|
||||
streamID := fmt.Sprintf("saltyuser-%x", sha256.Sum256([]byte(strings.ToLower(nick))))
|
||||
span.AddEvent(streamID)
|
||||
|
||||
a, err := es.Update(ctx, s.es, streamID, func(ctx context.Context, agg *SaltyUser) error { return nil })
|
||||
switch {
|
||||
case errors.Is(err, es.ErrShouldExist):
|
||||
span.RecordError(err)
|
||||
return nil, fmt.Errorf("user not found")
|
||||
|
||||
case err != nil:
|
||||
span.RecordError(err)
|
||||
return nil, fmt.Errorf("%w internal error", err)
|
||||
}
|
||||
|
||||
return a, err
|
||||
}
|
||||
func (s *service) GetMiddleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r = r.WithContext(gql.ToContext(r.Context(), saltyKey, s))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) apiv1(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
defer s.m_req_time.Record(ctx, time.Since(start).Nanoseconds())
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
switch {
|
||||
case r.URL.Path == "/ping":
|
||||
s.m_api_ping.Add(ctx, 1)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/lookup/"):
|
||||
s.m_api_lookup.Add(ctx, 1)
|
||||
|
||||
addr, err := s.ParseAddr(strings.TrimPrefix(r.URL.Path, "/lookup/"))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = addr.Refresh(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(addr)
|
||||
span.RecordError(err)
|
||||
return
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
case http.MethodPost:
|
||||
switch r.URL.Path {
|
||||
case "/register":
|
||||
s.m_api_register.Add(ctx, 1)
|
||||
notImplemented(w)
|
||||
return
|
||||
|
||||
case "/send":
|
||||
s.m_api_send.Add(ctx, 1)
|
||||
notImplemented(w)
|
||||
return
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func notImplemented(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
|
@ -1,402 +0,0 @@
|
|||
// package diskstore provides a driver that reads and writes events to disk.
|
||||
|
||||
package diskstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/wal"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"go.sour.is/pkg/cache"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
const CachSize = 1000
|
||||
|
||||
const AppendOnly = ev.AppendOnly
|
||||
const AllEvents = ev.AllEvents
|
||||
|
||||
type lockedWal = locker.Locked[*wal.Log]
|
||||
type openlogs struct {
|
||||
logs *cache.Cache[string, *lockedWal]
|
||||
}
|
||||
type diskStore struct {
|
||||
path string
|
||||
openlogs *locker.Locked[*openlogs]
|
||||
|
||||
m_disk_open metric.Int64Counter
|
||||
m_disk_evict metric.Int64Counter
|
||||
m_disk_read metric.Int64Counter
|
||||
m_disk_write metric.Int64Counter
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*diskStore)(nil)
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
d := &diskStore{}
|
||||
|
||||
m := lg.Meter(ctx)
|
||||
var err, errs error
|
||||
|
||||
d.m_disk_open, err = m.Int64Counter("disk_open")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_evict, err = m.Int64Counter("disk_evict")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_read, err = m.Int64Counter("disk_read")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_write, err = m.Int64Counter("disk_write")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
ev.Register(ctx, "file", d)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (d *diskStore) Open(ctx context.Context, dsn string) (driver.Driver, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("args.dsn", dsn),
|
||||
)
|
||||
|
||||
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 {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c, err := cache.NewWithEvict(CachSize, func(ctx context.Context, s string, l *lockedWal) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
l.Use(ctx, func(ctx context.Context, w *wal.Log) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
d.m_disk_evict.Add(ctx, 1)
|
||||
|
||||
err := w.Close()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
logs := &openlogs{logs: c}
|
||||
return &diskStore{
|
||||
path: path,
|
||||
openlogs: locker.New(logs),
|
||||
m_disk_open: d.m_disk_open,
|
||||
m_disk_evict: d.m_disk_evict,
|
||||
m_disk_read: d.m_disk_read,
|
||||
m_disk_write: d.m_disk_write,
|
||||
}, nil
|
||||
}
|
||||
func (d *diskStore) EventLog(ctx context.Context, streamID string) (driver.EventLog, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("args.streamID", streamID),
|
||||
attribute.String("path", d.path),
|
||||
)
|
||||
|
||||
el := &eventLog{streamID: streamID, diskStore: d}
|
||||
|
||||
return el, d.openlogs.Use(ctx, func(ctx context.Context, openlogs *openlogs) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if events, ok := openlogs.logs.Get(streamID); ok {
|
||||
el.events = *events
|
||||
return nil
|
||||
}
|
||||
|
||||
d.m_disk_open.Add(ctx, 1)
|
||||
|
||||
// migrate streams into dir friendly subdirs
|
||||
hashPart := mkDirName(streamID)
|
||||
oldPath := filepath.Join(d.path, streamID)
|
||||
newPath := filepath.Join(d.path, hashPart, streamID)
|
||||
if _, err := os.Stat(oldPath); !os.IsNotExist(err) {
|
||||
os.MkdirAll(filepath.Join(d.path, hashPart), 0700)
|
||||
os.Rename(oldPath, newPath)
|
||||
}
|
||||
|
||||
l, err := wal.Open(newPath, wal.DefaultOptions)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
el.events = locker.New(l)
|
||||
openlogs.logs.Add(ctx, streamID, el.events)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type eventLog struct {
|
||||
streamID string
|
||||
events *locker.Locked[*wal.Log]
|
||||
diskStore *diskStore
|
||||
}
|
||||
|
||||
var _ driver.EventLog = (*eventLog)(nil)
|
||||
|
||||
func (e *eventLog) Append(ctx context.Context, events event.Events, version uint64) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int("args.events", len(events)),
|
||||
attribute.Int64("args.version", int64(version)),
|
||||
attribute.String("streamID", e.streamID),
|
||||
attribute.String("path", e.diskStore.path),
|
||||
)
|
||||
|
||||
event.SetStreamID(e.streamID, events...)
|
||||
|
||||
var count uint64
|
||||
err := e.events.Use(ctx, func(ctx context.Context, l *wal.Log) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
last, err := l.LastIndex()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if version != AppendOnly && version != last {
|
||||
err = fmt.Errorf("%w: current version wrong %d != %d", ev.ErrWrongVersion, version, last)
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
batch := &wal.Batch{}
|
||||
for i, e := range events {
|
||||
span.AddEvent(fmt.Sprintf("append event %d of %d", i, len(events)))
|
||||
|
||||
b, err = event.MarshalBinary(e)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
return err
|
||||
}
|
||||
pos := last + uint64(i) + 1
|
||||
event.SetPosition(e, pos)
|
||||
|
||||
batch.Write(pos, b)
|
||||
}
|
||||
|
||||
count = uint64(len(events))
|
||||
e.diskStore.m_disk_write.Add(ctx, int64(len(events)))
|
||||
|
||||
return l.WriteBatch(batch)
|
||||
})
|
||||
span.RecordError(err)
|
||||
|
||||
return count, err
|
||||
}
|
||||
func (e *eventLog) ReadN(ctx context.Context, index ...uint64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
lis := make([]int64, len(index))
|
||||
for i := range index {
|
||||
lis[i] = int64(index[i])
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int64Slice("args.index", lis),
|
||||
attribute.String("streamID", e.streamID),
|
||||
attribute.String("path", e.diskStore.path),
|
||||
)
|
||||
|
||||
var events event.Events
|
||||
err := e.events.Use(ctx, func(ctx context.Context, stream *wal.Log) error {
|
||||
var err error
|
||||
|
||||
events, err = readStreamN(ctx, stream, index...)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return events, err
|
||||
}
|
||||
func (e *eventLog) Read(ctx context.Context, after, count int64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
span.SetAttributes(
|
||||
attribute.Int64("args.after", after),
|
||||
attribute.Int64("args.count", count),
|
||||
attribute.String("streamID", e.streamID),
|
||||
attribute.String("path", e.diskStore.path),
|
||||
)
|
||||
|
||||
var events event.Events
|
||||
err := e.events.Use(ctx, func(ctx context.Context, stream *wal.Log) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
first, err := stream.FirstIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
last, err := stream.LastIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
streamIDs, err := driver.GenerateStreamIDs(first, last, after, count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
events, err = readStreamN(ctx, stream, streamIDs...)
|
||||
event.SetStreamID(e.streamID, events...)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.diskStore.m_disk_read.Add(ctx, int64(len(events)))
|
||||
|
||||
return events, nil
|
||||
}
|
||||
func (e *eventLog) FirstIndex(ctx context.Context) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("streamID", e.streamID),
|
||||
attribute.String("path", e.diskStore.path),
|
||||
)
|
||||
|
||||
var idx uint64
|
||||
var err error
|
||||
|
||||
err = e.events.Use(ctx, func(ctx context.Context, events *wal.Log) error {
|
||||
idx, err = events.FirstIndex()
|
||||
return err
|
||||
})
|
||||
|
||||
return idx, err
|
||||
}
|
||||
func (e *eventLog) LastIndex(ctx context.Context) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("streamID", e.streamID),
|
||||
attribute.String("path", e.diskStore.path),
|
||||
)
|
||||
|
||||
var idx uint64
|
||||
var err error
|
||||
|
||||
err = e.events.Use(ctx, func(ctx context.Context, events *wal.Log) error {
|
||||
idx, err = events.LastIndex()
|
||||
return err
|
||||
})
|
||||
|
||||
return idx, err
|
||||
}
|
||||
func (e *eventLog) Truncate(ctx context.Context, index int64) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int64("args.index", index),
|
||||
attribute.String("streamID", e.streamID),
|
||||
attribute.String("path", e.diskStore.path),
|
||||
)
|
||||
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
return e.events.Use(ctx, func(ctx context.Context, events *wal.Log) error {
|
||||
if index < 0 {
|
||||
return events.TruncateBack(uint64(-index))
|
||||
}
|
||||
return events.TruncateFront(uint64(index))
|
||||
})
|
||||
}
|
||||
func readStreamN(ctx context.Context, stream *wal.Log, index ...uint64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
lis := make([]int64, len(index))
|
||||
for i := range index {
|
||||
lis[i] = int64(index[i])
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int64Slice("args.index", lis),
|
||||
)
|
||||
|
||||
var b []byte
|
||||
var err error
|
||||
events := make(event.Events, len(index))
|
||||
for i, idx := range index {
|
||||
b, err = stream.Read(idx)
|
||||
if err != nil {
|
||||
if errors.Is(err, wal.ErrNotFound) || errors.Is(err, wal.ErrOutOfRange) {
|
||||
err = fmt.Errorf("%w: empty", ev.ErrNotFound)
|
||||
}
|
||||
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
events[i], err = event.UnmarshalBinary(ctx, b, idx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("read event %d of %d - %d", i, len(events), events[i].EventMeta().ActualPosition))
|
||||
}
|
||||
return events, err
|
||||
}
|
||||
func mkDirName(name string) string {
|
||||
h := fnv.New32a()
|
||||
fmt.Fprint(h, name)
|
||||
return fmt.Sprintf("%x/%x/%x", h.Sum32()>>24&0xff, h.Sum32()>>16&0xff, h.Sum32()&0xffff)
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// package driver defines interfaces to be used by driver implementations.
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.sour.is/ev/event"
|
||||
"go.sour.is/pkg/math"
|
||||
)
|
||||
|
||||
type Driver interface {
|
||||
Open(ctx context.Context, dsn string) (Driver, error)
|
||||
EventLog(ctx context.Context, streamID string) (EventLog, error)
|
||||
}
|
||||
|
||||
type EventLog interface {
|
||||
Read(ctx context.Context, after, count int64) (event.Events, error)
|
||||
ReadN(ctx context.Context, index ...uint64) (event.Events, error)
|
||||
Append(ctx context.Context, events event.Events, version uint64) (uint64, error)
|
||||
FirstIndex(context.Context) (uint64, error)
|
||||
LastIndex(context.Context) (uint64, error)
|
||||
}
|
||||
|
||||
type EventLogWithTruncate interface {
|
||||
Truncate(context.Context, int64) error
|
||||
}
|
||||
|
||||
type EventLogWithUpdate interface {
|
||||
LoadForUpdate(context.Context, event.Aggregate, func(context.Context, event.Aggregate) error) (uint64, error)
|
||||
}
|
||||
|
||||
type Subscription interface {
|
||||
Recv(context.Context) <-chan bool
|
||||
Events(context.Context) (event.Events, error)
|
||||
Close(context.Context) error
|
||||
}
|
||||
|
||||
type EventStream interface {
|
||||
Subscribe(ctx context.Context, streamID string, start int64) (Subscription, error)
|
||||
Send(ctx context.Context, streamID string, events event.Events) error
|
||||
}
|
||||
|
||||
func GenerateStreamIDs(first, last uint64, after, count int64) ([]uint64, error) {
|
||||
// ---
|
||||
if first == 0 || last == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
start, count := math.PagerBox(first, last, after, count)
|
||||
if count == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
streamIDs := make([]uint64, math.Abs(count))
|
||||
for i := range streamIDs {
|
||||
streamIDs[i] = start
|
||||
|
||||
if count > 0 {
|
||||
start += 1
|
||||
} else {
|
||||
start -= 1
|
||||
}
|
||||
if start < first || start > last {
|
||||
streamIDs = streamIDs[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
return streamIDs, nil
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
// package memstore provides a driver that reads and writes events to memory.
|
||||
package memstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
const AppendOnly = ev.AppendOnly
|
||||
const AllEvents = ev.AllEvents
|
||||
|
||||
type state struct {
|
||||
streams map[string]*locker.Locked[*event.Events]
|
||||
}
|
||||
type memstore struct {
|
||||
state *locker.Locked[*state]
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*memstore)(nil)
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return ev.Register(ctx, "mem", &memstore{})
|
||||
}
|
||||
|
||||
func (memstore) Open(ctx context.Context, name string) (driver.Driver, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s := &state{streams: make(map[string]*locker.Locked[*event.Events])}
|
||||
return &memstore{locker.New(s)}, nil
|
||||
}
|
||||
func (m *memstore) EventLog(ctx context.Context, streamID string) (driver.EventLog, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
el := &eventLog{streamID: streamID}
|
||||
|
||||
err := m.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
l, ok := state.streams[streamID]
|
||||
if !ok {
|
||||
l = locker.New(&event.Events{})
|
||||
state.streams[streamID] = l
|
||||
}
|
||||
el.events = l
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return el, err
|
||||
}
|
||||
|
||||
type eventLog struct {
|
||||
streamID string
|
||||
events *locker.Locked[*event.Events]
|
||||
}
|
||||
|
||||
var _ driver.EventLog = (*eventLog)(nil)
|
||||
|
||||
// Append implements driver.EventStore
|
||||
func (m *eventLog) Append(ctx context.Context, events event.Events, version uint64) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
event.SetStreamID(m.streamID, events...)
|
||||
|
||||
return uint64(len(events)), m.events.Use(ctx, func(ctx context.Context, stream *event.Events) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.AddEvent(fmt.Sprintf(" %s %d", m.streamID, len(*stream)))
|
||||
|
||||
last := uint64(len(*stream))
|
||||
if version != AppendOnly && version != last {
|
||||
return fmt.Errorf("%w: current version wrong %d != %d", ev.ErrWrongVersion, version, last)
|
||||
}
|
||||
|
||||
for i := range events {
|
||||
span.AddEvent(fmt.Sprintf("read event %d of %d", i, len(events)))
|
||||
|
||||
// --- clone event
|
||||
e := events[i]
|
||||
b, err := event.MarshalBinary(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e, err = event.UnmarshalBinary(ctx, b, e.EventMeta().Position)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ---
|
||||
|
||||
pos := last + uint64(i) + 1
|
||||
event.SetPosition(e, pos)
|
||||
*stream = append(*stream, e)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ReadOne implements readone
|
||||
func (m *eventLog) ReadN(ctx context.Context, index ...uint64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
lis := make([]int64, len(index))
|
||||
for i := range index {
|
||||
lis[i] = int64(index[i])
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int64Slice("args.index", lis),
|
||||
attribute.String("streamID", m.streamID),
|
||||
)
|
||||
|
||||
var events event.Events
|
||||
err := m.events.Use(ctx, func(ctx context.Context, stream *event.Events) error {
|
||||
var err error
|
||||
|
||||
events, err = readStreamN(ctx, stream, index...)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return events, err
|
||||
}
|
||||
|
||||
// Read implements driver.EventStore
|
||||
func (m *eventLog) Read(ctx context.Context, after int64, count int64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
span.SetAttributes(
|
||||
attribute.Int64("args.after", after),
|
||||
attribute.Int64("args.count", count),
|
||||
attribute.String("streamID", m.streamID),
|
||||
)
|
||||
|
||||
var events event.Events
|
||||
err := m.events.Use(ctx, func(ctx context.Context, stream *event.Events) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
first := stream.First().EventMeta().Position
|
||||
last := stream.Last().EventMeta().Position
|
||||
|
||||
streamIDs, err := driver.GenerateStreamIDs(first, last, after, count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
events, err = readStreamN(ctx, stream, streamIDs...)
|
||||
event.SetStreamID(m.streamID, events...)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// FirstIndex for the streamID
|
||||
func (m *eventLog) FirstIndex(ctx context.Context) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
events, err := m.events.Copy(ctx)
|
||||
return events.First().EventMeta().Position, err
|
||||
}
|
||||
|
||||
// LastIndex for the streamID
|
||||
func (m *eventLog) LastIndex(ctx context.Context) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
events, err := m.events.Copy(ctx)
|
||||
return events.Last().EventMeta().Position, err
|
||||
}
|
||||
|
||||
func (e *eventLog) Truncate(ctx context.Context, index int64) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int64("args.index", index),
|
||||
attribute.String("streamID", e.streamID),
|
||||
)
|
||||
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
return e.events.Use(ctx, func(ctx context.Context, events *event.Events) error {
|
||||
if index < 0 {
|
||||
*events = (*events)[:index]
|
||||
return nil
|
||||
}
|
||||
*events = (*events)[index:]
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func readStreamN(ctx context.Context, stream *event.Events, index ...uint64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var b []byte
|
||||
var err error
|
||||
|
||||
count := len(index)
|
||||
events := make(event.Events, count)
|
||||
for i, index := range index {
|
||||
span.AddEvent(fmt.Sprintf("read event %d of %d", i, count))
|
||||
|
||||
e := (*stream)[index-1]
|
||||
b, err = event.MarshalBinary(e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events[i], err = event.UnmarshalBinary(ctx, b, e.EventMeta().Position)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return events, err
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
package projecter_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/driver/projecter"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type mockDriver struct {
|
||||
onOpen func(context.Context, string) (driver.Driver, error)
|
||||
onEventLog func(context.Context, string) (driver.EventLog, error)
|
||||
}
|
||||
|
||||
// Open implements driver.Driver
|
||||
func (m *mockDriver) Open(ctx context.Context, dsn string) (driver.Driver, error) {
|
||||
if m.onOpen != nil {
|
||||
return m.onOpen(ctx, dsn)
|
||||
}
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// EventLog implements driver.Driver
|
||||
func (m *mockDriver) EventLog(ctx context.Context, streamID string) (driver.EventLog, error) {
|
||||
if m.onEventLog != nil {
|
||||
return m.onEventLog(ctx, streamID)
|
||||
}
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*mockDriver)(nil)
|
||||
|
||||
type mockEventLog struct {
|
||||
onAppend func(context.Context, event.Events, uint64) (uint64, error)
|
||||
onFirstIndex func(context.Context) (uint64, error)
|
||||
onLastIndex func(context.Context) (uint64, error)
|
||||
onRead func(context.Context, int64, int64) (event.Events, error)
|
||||
onReadN func(context.Context, ...uint64) (event.Events, error)
|
||||
}
|
||||
|
||||
// Append implements driver.EventLog
|
||||
func (m *mockEventLog) Append(ctx context.Context, events event.Events, version uint64) (uint64, error) {
|
||||
if m.onAppend != nil {
|
||||
return m.onAppend(ctx, events, version)
|
||||
}
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// FirstIndex implements driver.EventLog
|
||||
func (m *mockEventLog) FirstIndex(ctx context.Context) (uint64, error) {
|
||||
if m.onFirstIndex != nil {
|
||||
return m.onFirstIndex(ctx)
|
||||
}
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// LastIndex implements driver.EventLog
|
||||
func (m *mockEventLog) LastIndex(ctx context.Context) (uint64, error) {
|
||||
if m.onLastIndex != nil {
|
||||
return m.onLastIndex(ctx)
|
||||
}
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Read implements driver.EventLog
|
||||
func (m *mockEventLog) Read(ctx context.Context, pos int64, count int64) (event.Events, error) {
|
||||
if m.onRead != nil {
|
||||
return m.onRead(ctx, pos, count)
|
||||
}
|
||||
|
||||
panic("unimplemented")
|
||||
}
|
||||
func (m *mockEventLog) ReadN(ctx context.Context, index ...uint64) (event.Events, error) {
|
||||
if m.onReadN != nil {
|
||||
return m.onReadN(ctx, index...)
|
||||
}
|
||||
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ driver.EventLog = (*mockEventLog)(nil)
|
||||
|
||||
func TestProjecter(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
var events []event.Event
|
||||
|
||||
wait := make(chan struct{})
|
||||
|
||||
mockEL := &mockEventLog{}
|
||||
mockEL.onRead = func(ctx context.Context, i1, i2 int64) (event.Events, error) {
|
||||
return event.NewEvents(), nil
|
||||
}
|
||||
mockEL.onAppend = func(ctx context.Context, e event.Events, u uint64) (uint64, error) {
|
||||
events = append(events, e...)
|
||||
if wait != nil && len(events) > 3 {
|
||||
close(wait)
|
||||
}
|
||||
return uint64(len(e)), nil
|
||||
}
|
||||
|
||||
mock := &mockDriver{}
|
||||
mock.onOpen = func(ctx context.Context, s string) (driver.Driver, error) {
|
||||
return mock, nil
|
||||
}
|
||||
mock.onEventLog = func(ctx context.Context, s string) (driver.EventLog, error) {
|
||||
return mockEL, nil
|
||||
}
|
||||
|
||||
ev.Init(ctx)
|
||||
ev.Register(ctx, "mock", mock)
|
||||
|
||||
es, err := ev.Open(
|
||||
ctx,
|
||||
"mock:",
|
||||
projecter.New(ctx, projecter.DefaultProjection),
|
||||
)
|
||||
|
||||
is.NoErr(err)
|
||||
|
||||
_, err = es.Read(ctx, "test", 0, 1)
|
||||
|
||||
is.NoErr(err)
|
||||
|
||||
_, err = es.Append(ctx, "test", event.NewEvents(event.NilEvent))
|
||||
is.NoErr(err)
|
||||
|
||||
<-wait
|
||||
|
||||
is.Equal(len(events), 4)
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package resolvelinks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
"go.sour.is/pkg/lg"
|
||||
)
|
||||
|
||||
type resolvelinks struct {
|
||||
up driver.Driver
|
||||
}
|
||||
|
||||
func New() *resolvelinks {
|
||||
return &resolvelinks{}
|
||||
}
|
||||
|
||||
func (r *resolvelinks) Apply(es *ev.EventStore) {
|
||||
r.up = es.Driver
|
||||
es.Driver = r
|
||||
}
|
||||
|
||||
func (r *resolvelinks) Unwrap() driver.Driver {
|
||||
return r.up
|
||||
}
|
||||
|
||||
func (r *resolvelinks) Open(ctx context.Context, dsn string) (driver.Driver, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return r.up.Open(ctx, dsn)
|
||||
}
|
||||
|
||||
func (r *resolvelinks) EventLog(ctx context.Context, streamID string) (driver.EventLog, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
l, err := r.up.EventLog(ctx, streamID)
|
||||
return &wrapper{l, r}, err
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
up driver.EventLog
|
||||
resolver *resolvelinks
|
||||
}
|
||||
|
||||
func (r *wrapper) Unwrap() driver.EventLog {
|
||||
return r.up
|
||||
}
|
||||
|
||||
func (w *wrapper) Read(ctx context.Context, after int64, count int64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
events, err := w.up.Read(ctx, after, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = w.resolvelinks(ctx, events)
|
||||
return events, err
|
||||
}
|
||||
|
||||
func (w *wrapper) ReadN(ctx context.Context, index ...uint64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
events, err := w.up.ReadN(ctx, index...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = w.resolvelinks(ctx, events)
|
||||
return events, err
|
||||
}
|
||||
|
||||
func (w *wrapper) Append(ctx context.Context, events event.Events, version uint64) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.Append(ctx, events, version)
|
||||
}
|
||||
|
||||
func (w *wrapper) FirstIndex(ctx context.Context) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.FirstIndex(ctx)
|
||||
}
|
||||
|
||||
func (w *wrapper) LastIndex(ctx context.Context) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.LastIndex(ctx)
|
||||
}
|
||||
|
||||
func (w *wrapper) resolvelinks(ctx context.Context, events event.Events) error {
|
||||
idx := make(map[string][]uint64)
|
||||
ptrs := make(map[string][]int)
|
||||
for i := range events {
|
||||
e := events[i]
|
||||
if e, ok := e.(*event.EventPtr); ok {
|
||||
idx[e.StreamID] = append(idx[e.StreamID], e.Pos)
|
||||
ptrs[e.StreamID] = append(ptrs[e.StreamID], i)
|
||||
}
|
||||
}
|
||||
|
||||
for streamID, ids := range idx {
|
||||
d, err := w.resolver.EventLog(ctx, streamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ptr := ptrs[streamID]
|
||||
lis, err := d.ReadN(ctx, ids...)
|
||||
if err != nil && !errors.Is(err, ev.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range lis {
|
||||
meta := lis[i].EventMeta()
|
||||
actual := events[ptr[i]].EventMeta()
|
||||
meta.ActualPosition = actual.Position
|
||||
meta.ActualStreamID = actual.ActualStreamID
|
||||
lis[i].SetEventMeta(meta)
|
||||
events[i] = lis[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
105
go.mod
105
go.mod
|
@ -1,53 +1,86 @@
|
|||
module go.sour.is/ev
|
||||
module github.com/sour-is/ev
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/99designs/gqlgen v0.17.13
|
||||
github.com/go-logr/stdr v1.2.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/logzio/logzio-go v1.0.6
|
||||
github.com/ravilushqa/otelgqlgen v0.9.0
|
||||
github.com/rs/cors v1.8.2
|
||||
github.com/tidwall/wal v1.1.7
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 // indirect
|
||||
go.opentelemetry.io/otel v1.19.0
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.41.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.19.0
|
||||
go.opentelemetry.io/otel/sdk v1.18.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.19.0
|
||||
github.com/vektah/gqlparser/v2 v2.4.7
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.34.0
|
||||
go.opentelemetry.io/otel v1.9.0
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.31.0
|
||||
go.opentelemetry.io/otel/metric v0.31.0
|
||||
go.opentelemetry.io/otel/sdk v1.9.0
|
||||
go.opentelemetry.io/otel/sdk/metric v0.31.0
|
||||
go.opentelemetry.io/otel/trace v1.9.0
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
)
|
||||
|
||||
require go.sour.is/pkg v0.0.5
|
||||
|
||||
require (
|
||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 // indirect
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/beeker1121/goque v2.1.0+incompatible // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
||||
github.com/dchest/blake2b v1.0.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.13.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||
google.golang.org/grpc v1.58.3 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/keybase/saltpack v0.0.0-20200430135328-e19b1910c0c5 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/onsi/ginkgo v1.14.0 // indirect
|
||||
github.com/onsi/gomega v1.10.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_golang v1.12.2 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.22.3 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
|
||||
github.com/vmihailenco/tagparser v0.1.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.opentelemetry.io/contrib v1.9.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.9.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.18.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect
|
||||
google.golang.org/grpc v1.46.2 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/matryer/is v1.4.1
|
||||
github.com/keys-pub/keys v0.1.22
|
||||
github.com/matryer/is v1.4.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/gjson v1.10.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/tinylru v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 // indirect
|
||||
go.uber.org/multierr v1.11.0
|
||||
gitlab.com/jamietanna/content-negotiation-go v0.2.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.9.0
|
||||
go.uber.org/multierr v1.8.0
|
||||
)
|
||||
|
|
796
go.sum
796
go.sum
|
@ -1,121 +1,731 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/99designs/gqlgen v0.17.13 h1:ETUEqvRg5Zvr1lXtpoRdj026fzVay0ZlJPwI33qXLIw=
|
||||
github.com/99designs/gqlgen v0.17.13/go.mod h1:w1brbeOdqVyNJI553BGwtwdVcYu1LKeYE1opLWN9RgQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E=
|
||||
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/beeker1121/goque v2.1.0+incompatible h1:m5pZ5b8nqzojS2DF2ioZphFYQUqGYsDORq6uefUItPM=
|
||||
github.com/beeker1121/goque v2.1.0+incompatible/go.mod h1:L6dOWBhDOnxUVQsb0wkLve0VCnt2xJW/MI8pdRX4ANw=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
|
||||
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=
|
||||
github.com/dchest/blake2b v1.0.0 h1:KK9LimVmE0MjRl9095XJmKqZ+iLxWATvlcpVFRtaw6s=
|
||||
github.com/dchest/blake2b v1.0.0/go.mod h1:U034kXgbJpCle2wSk5ybGIVhOSHCVLMDqOzcPEA0F7s=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
|
||||
github.com/keybase/go-codec v0.0.0-20180928230036-164397562123/go.mod h1:r/eVVWCngg6TsFV/3HuS9sWhDkAzGG8mXhiuYA+Z/20=
|
||||
github.com/keybase/go-keychain v0.0.0-20201121013009-976c83ec27a6/go.mod h1:N83iQ9rnnzi2KZuTu+0xBcD1JNWn1jSN140ggAF7HeE=
|
||||
github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
|
||||
github.com/keybase/saltpack v0.0.0-20200430135328-e19b1910c0c5 h1:X6nYzCVURqxDv0GuyptaCcRFTXPM0rSGNUrTeQ2NKUQ=
|
||||
github.com/keybase/saltpack v0.0.0-20200430135328-e19b1910c0c5/go.mod h1:FNSq71OhXv/Z1W9M37nnHxJVhXitc03z6qshCbAten8=
|
||||
github.com/keys-pub/keys v0.1.22 h1:bO0nx7c3HuC8dqjmjZ8njC8DzpuKWnOZQ5njaO5+A+o=
|
||||
github.com/keys-pub/keys v0.1.22/go.mod h1:+41yREqLkYyGfGf4OkhUn/ljwe/+kwhrlTq1/46Jj8c=
|
||||
github.com/keys-pub/secretservice v0.0.0-20200519003656-26e44b8df47f/go.mod h1:YRHMiVbZqh7u8xRm77CvwJNAZdDlNXwWvQ4DK0N9mYg=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/logzio/logzio-go v1.0.6 h1:BIVu5TWDZc0vlEkwSDjoxPlV/aMJV2LdM3k+CjdzFDg=
|
||||
github.com/logzio/logzio-go v1.0.6/go.mod h1:ljlI3Zfi3hntJiHqCqWSUPT9cZP6yvDHUzDl5ZLGYRE=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
|
||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/ravilushqa/otelgqlgen v0.9.0 h1:NgbrU9OHPKB/X6Ja618+SqPNls05M3T8aV1fkUiR9ow=
|
||||
github.com/ravilushqa/otelgqlgen v0.9.0/go.mod h1:TqSvbt/7E23CHOOgL6G+42kCbhvxUpT/21tMsarq4Hk=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
|
||||
github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shirou/gopsutil/v3 v3.22.3 h1:UebRzEomgMpv61e3hgD1tGooqX5trFbdU/ehphbHd00=
|
||||
github.com/shirou/gopsutil/v3 v3.22.3/go.mod h1:D01hZJ4pVHPpCTZ3m3T2+wDF2YAGfd+H4ifUguaQzHM=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
|
||||
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/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/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/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=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
|
||||
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
|
||||
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 h1:6pu8ttx76BxHf+xz/H77AUZkPF3cwWzXqAUsXhVKI18=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0/go.mod h1:IOmXxPrxoxFMXdNy7lfDmE8MzE61YPcurbUm0SMjerI=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.41.0 h1:A3/bhjP5SmELy8dcpK+uttHeh9Qrh+YnS16/VzrztRQ=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.41.0/go.mod h1:mKuXEMi9suyyNJQ99SZCO0mpWGFe0MIALtjd3r6uo7Q=
|
||||
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
|
||||
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
|
||||
go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY=
|
||||
go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.41.0 h1:c3sAt9/pQ5fSIUfl0gPtClV3HhE18DCVzByD33R/zsk=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.41.0/go.mod h1:PmOmSt+iOklKtIg5O4Vz9H/ttcRFSNTgii+E1KGyn1w=
|
||||
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
|
||||
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.sour.is/pkg v0.0.5 h1:ngRYyFl+Ks1S63hnAxcIpaKdoqXGQIKvQnz4QIrYm94=
|
||||
go.sour.is/pkg v0.0.5/go.mod h1:kb8ERLsUC3DqHNwm7gra2tE6x+2C9g+zxzVEC36R884=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
|
||||
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
|
||||
github.com/vektah/gqlparser/v2 v2.4.6/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.4.7 h1:yub2WLoSIr+chP1zMv6bjrsgTasfubxGZJeC8ISEpgE=
|
||||
github.com/vektah/gqlparser/v2 v2.4.7/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U=
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
|
||||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
|
||||
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
gitlab.com/jamietanna/content-negotiation-go v0.2.0 h1:vT0OLEPQ6DYRG3/1F7joXSNjVQHGivJ6+JzODlJfjWw=
|
||||
gitlab.com/jamietanna/content-negotiation-go v0.2.0/go.mod h1:n4ZZ8/X5TstnjYRnjEtR/fC7MCTe+aRKM7PQlLBH3PQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/contrib v1.9.0 h1:2KAoCVu4OMI9TYoSWvcV7+UbbIPOi4623S77nV+M/Ks=
|
||||
go.opentelemetry.io/contrib v1.9.0/go.mod h1:yp0N4+hnpWCpnMzs6T6WbD9Amfg7reEZsS0jAd/5M2Q=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 h1:9NkMW03wwEzPtP/KciZ4Ozu/Uz5ZA7kfqXJIObnrjGU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0/go.mod h1:548ZsYzmT4PL4zWKRd8q/N4z0Wxzn/ZxUE+lkEpwWQA=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.34.0 h1:zt4RDodWkgiHk8tyUmFOjFoOOfyGH7vwIbUzKP6CCh8=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.34.0/go.mod h1:5wIoZE96WbcQVU3D6UF/ukRfFQXbB6OYgeWi9CjHa90=
|
||||
go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw=
|
||||
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.9.0 h1:ggqApEjDKczicksfvZUCxuvoyDmR6Sbm56LwiK8DVR0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.9.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0 h1:NN90Cuna0CnBg8YNu1Q0V35i2E8LDByFOwHRCq/ZP9I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0/go.mod h1:0EsCXjZAiiZGnLdEUXM9YjCKuuLZMYyglh2QDXcYKVA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.9.0 h1:FAF9l8Wjxi9Ad2k/vLTfHZyzXYX72C62wBGpV3G6AIo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.9.0/go.mod h1:smUdtylgc0YQiUr2PuifS4hBXhAS5xtR6WQhxP1wiNA=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.31.0 h1:jwtnOGBM8dIty5AVZ+9ZCzZexCea3aVKmUfZAQcHqxs=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.31.0/go.mod h1:QarXIB8L79IwIPoNgG3A6zNvBgVmcppeFogV1d8612s=
|
||||
go.opentelemetry.io/otel/metric v0.31.0 h1:6SiklT+gfWAwWUR0meEMxQBtihpiEs4c+vL9spDTqUs=
|
||||
go.opentelemetry.io/otel/metric v0.31.0/go.mod h1:ohmwj9KTSIeBnDBm/ZwH2PSZxZzoOaG2xZeekTRzL5A=
|
||||
go.opentelemetry.io/otel/sdk v1.9.0 h1:LNXp1vrr83fNXTHgU8eO89mhzxb/bbWAsHG6fNf3qWo=
|
||||
go.opentelemetry.io/otel/sdk v1.9.0/go.mod h1:AEZc8nt5bd2F7BC24J5R0mrjYnpEgYHyTcM/vrSple4=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.31.0 h1:2sZx4R43ZMhJdteKAlKoHvRgrMp53V1aRxvEf5lCq8Q=
|
||||
go.opentelemetry.io/otel/sdk/metric v0.31.0/go.mod h1:fl0SmNnX9mN9xgU6OLYLMBMrNAsaZQi7qBwprwO3abk=
|
||||
go.opentelemetry.io/otel/trace v1.9.0 h1:oZaCNJUjWcg60VXWee8lJKlqhPbXAPB51URuR47pQYc=
|
||||
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.18.0 h1:W5hyXNComRa23tGpKwG+FRAc4rfF6ZUg1JReK+QHS80=
|
||||
go.opentelemetry.io/proto/otlp v0.18.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
|
||||
google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o=
|
||||
google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
|
||||
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 h1:b9mVrqYfq3P4bCdaLg1qtBnPzUYgglsIdjZkL/fQVOE=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
|
||||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
203
gql/graph.go
203
gql/graph.go
|
@ -1,203 +0,0 @@
|
|||
package gql_ev
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.sour.is/pkg/gql"
|
||||
"go.sour.is/pkg/lg"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type EventResolver interface {
|
||||
Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error)
|
||||
EventAdded(ctx context.Context, streamID string, after int64) (<-chan *Event, error)
|
||||
TruncateStream(ctx context.Context, streamID string, index int64) (bool, error)
|
||||
}
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
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, ev.ErrNotFound) {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edges := make([]gql.Edge, 0, len(lis))
|
||||
for i := range lis {
|
||||
span.AddEvent(fmt.Sprint("event ", i, " of ", len(lis)))
|
||||
edges = append(edges, &Event{lis[i]})
|
||||
}
|
||||
|
||||
var first, last uint64
|
||||
if first, err = es.FirstIndex(ctx, streamID); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
if last, err = es.LastIndex(ctx, streamID); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gql.Connection{
|
||||
Paging: &gql.PageInfo{
|
||||
Next: lis.Last().EventMeta().ActualPosition < last,
|
||||
Prev: lis.First().EventMeta().ActualPosition > first,
|
||||
Begin: lis.First().EventMeta().ActualPosition,
|
||||
End: lis.Last().EventMeta().ActualPosition,
|
||||
},
|
||||
Edges: edges,
|
||||
}, nil
|
||||
}
|
||||
func (e *EventStore) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *Event, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
es := e.EventStream()
|
||||
if es == nil {
|
||||
return nil, fmt.Errorf("EventStore does not implement streaming")
|
||||
}
|
||||
|
||||
sub, err := es.Subscribe(ctx, streamID, after)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan *Event)
|
||||
|
||||
go func() {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
{
|
||||
ctx, span := lg.Fork(ctx)
|
||||
defer func() {
|
||||
defer span.End()
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
err := sub.Close(ctx)
|
||||
span.RecordError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
for <-sub.Recv(ctx) {
|
||||
events, err := sub.Events(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
break
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("received %d events", len(events)))
|
||||
|
||||
for i := range events {
|
||||
select {
|
||||
case ch <- &Event{events[i]}:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
func (es *EventStore) TruncateStream(ctx context.Context, streamID string, index int64) (bool, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
err := es.Truncate(ctx, streamID, index)
|
||||
return err == nil, err
|
||||
}
|
||||
func (e *EventStore) GetMiddleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := lg.Span(r.Context())
|
||||
defer span.End()
|
||||
|
||||
r = r.WithContext(gql.ToContext(ctx, esKey, e))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
e event.Event
|
||||
}
|
||||
|
||||
func (e *Event) ID() string {
|
||||
return fmt.Sprint(e.e.EventMeta().StreamID, "@", e.e.EventMeta().Position)
|
||||
}
|
||||
func (e *Event) EventID() string {
|
||||
return e.e.EventMeta().GetEventID()
|
||||
}
|
||||
func (e *Event) StreamID() string {
|
||||
return e.e.EventMeta().StreamID
|
||||
}
|
||||
func (e *Event) Position() uint64 {
|
||||
return e.e.EventMeta().Position
|
||||
}
|
||||
func (e *Event) Type() string {
|
||||
return event.TypeOf(e.e)
|
||||
}
|
||||
func (e *Event) Created() time.Time {
|
||||
return e.e.EventMeta().Created()
|
||||
}
|
||||
func (e *Event) Values() map[string]interface{} {
|
||||
return event.Values(e.e)
|
||||
}
|
||||
func (e *Event) Bytes() (string, error) {
|
||||
switch e := e.e.(type) {
|
||||
case encoding.BinaryMarshaler:
|
||||
b, err := e.MarshalBinary()
|
||||
return string(b), err
|
||||
case encoding.TextMarshaler:
|
||||
b, err := e.MarshalText()
|
||||
return string(b), err
|
||||
default:
|
||||
b, err := json.Marshal(e)
|
||||
return string(b), err
|
||||
}
|
||||
}
|
||||
func (e *Event) Meta() *event.Meta {
|
||||
meta := e.e.EventMeta()
|
||||
return &meta
|
||||
}
|
||||
func (e *Event) Linked(ctx context.Context) (*Event, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
values := event.Values(e.e)
|
||||
streamID, ok := values["stream_id"].(string)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
pos, ok := values["pos"].(uint64)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
events, err := gql.FromContext[contextKey, *EventStore](ctx, esKey).ReadN(ctx, streamID, pos)
|
||||
return &Event{e: events.First()}, err
|
||||
}
|
||||
func (e *Event) IsEdge() {}
|
64
gqlgen.yml
Normal file
64
gqlgen.yml
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
|
||||
schema:
|
||||
- pkg/*/*.graphqls
|
||||
- app/*/*.graphqls
|
||||
|
||||
# Where should the generated server code go?
|
||||
exec:
|
||||
filename: internal/graph/generated/generated.go
|
||||
package: generated
|
||||
|
||||
# Uncomment to enable federation
|
||||
federation:
|
||||
filename: internal/graph/generated/federation.go
|
||||
package: generated
|
||||
|
||||
# Where should any generated models go?
|
||||
model:
|
||||
filename: internal/graph/model/models_gen.go
|
||||
package: model
|
||||
|
||||
# Where should the resolver implementations go?
|
||||
# resolver:
|
||||
# layout: follow-schema
|
||||
# dir: internal/graph
|
||||
# package: graph
|
||||
|
||||
# Optional: turn on use `gqlgen:"fieldName"` tags in your models
|
||||
# struct_tag: json
|
||||
|
||||
# Optional: turn on to use []Thing instead of []*Thing
|
||||
# omit_slice_element_pointers: false
|
||||
|
||||
# Optional: set to speed up generation time by not performing a final validation pass.
|
||||
# skip_validation: true
|
||||
|
||||
# gqlgen will search for any type names in the schema in these go packages
|
||||
# if they match it will use them, otherwise it will generate them.
|
||||
# autobind:
|
||||
# - "github.com/sour-is/ev/pkg/gql"
|
||||
|
||||
# This section declares type mapping between the GraphQL and go type systems
|
||||
#
|
||||
# The first line in each type will be used as defaults for resolver arguments and
|
||||
# modelgen, the others will be allowed when binding to fields. Configure them to
|
||||
# your liking
|
||||
models:
|
||||
ID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.ID
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
- github.com/99designs/gqlgen/graphql.Uint
|
||||
- github.com/99designs/gqlgen/graphql.Uint64
|
||||
- github.com/99designs/gqlgen/graphql.Uint32
|
||||
Int:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Uint64
|
||||
- github.com/99designs/gqlgen/graphql.Uint32
|
||||
- github.com/99designs/gqlgen/graphql.Uint
|
||||
|
36
httpmux.go
Normal file
36
httpmux.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
type mux struct {
|
||||
*http.ServeMux
|
||||
api *http.ServeMux
|
||||
}
|
||||
|
||||
func httpMux(fns ...interface{ RegisterHTTP(*http.ServeMux) }) http.Handler {
|
||||
mux := newMux()
|
||||
for _, fn := range fns {
|
||||
fn.RegisterHTTP(mux.ServeMux)
|
||||
|
||||
if fn, ok := fn.(interface{ RegisterAPIv1(*http.ServeMux) }); ok {
|
||||
log.Printf("register api %T", fn)
|
||||
fn.RegisterAPIv1(mux.api)
|
||||
}
|
||||
}
|
||||
|
||||
return cors.AllowAll().Handler(mux)
|
||||
}
|
||||
func newMux() *mux {
|
||||
mux := &mux{
|
||||
api: http.NewServeMux(),
|
||||
ServeMux: http.NewServeMux(),
|
||||
}
|
||||
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", mux.api))
|
||||
|
||||
return mux
|
||||
}
|
40
httpmux_test.go
Normal file
40
httpmux_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
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 := httpMux(&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)
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package clean
|
||||
|
||||
import "encoding"
|
||||
|
||||
type EventLog[T, K, C comparable, E any] interface {
|
||||
EventLog(T) List[K, C, E]
|
||||
}
|
||||
|
||||
type EventStore[T, K, C comparable, E, A any] interface {
|
||||
Bus[T, K, E]
|
||||
EventLog[T, K, C, E]
|
||||
|
||||
Load(T, A) error
|
||||
Store(A) error
|
||||
Truncate(T) error
|
||||
}
|
||||
|
||||
type Event[T, C comparable, V any] struct {
|
||||
Topic T
|
||||
Position C
|
||||
Payload V
|
||||
}
|
||||
|
||||
type codec interface {
|
||||
encoding.BinaryMarshaler
|
||||
encoding.BinaryUnmarshaler
|
||||
}
|
||||
|
||||
type aggr = struct{}
|
||||
|
||||
type evvent = Event[string, uint64, codec]
|
||||
type evvee = EventStore[string, string, uint64, evvent, aggr]
|
||||
type evvesub = Subscription[Event[string, uint64, codec]]
|
||||
|
||||
type PAGE = Page[string, string]
|
||||
type LOG struct{}
|
||||
|
||||
var _ List[string, string, evvee] = (*LOG)(nil)
|
||||
|
||||
func (*LOG) First(n uint64, after string) ([]PAGE, error) { panic("N/A") }
|
||||
func (*LOG) Last(n uint64, before string) ([]PAGE, error) { panic("N/A") }
|
||||
|
||||
type SUB struct{}
|
||||
|
||||
var _ evvesub = (*SUB)(nil)
|
||||
|
||||
func (*SUB) Recv() error { return nil }
|
||||
func (*SUB) Events() []evvent { return nil }
|
||||
func (*SUB) Close() {}
|
||||
|
||||
type EV struct{}
|
||||
|
||||
var _ evvee = (*EV)(nil)
|
||||
|
||||
func (*EV) Emit(topic string, event evvent) error { panic("N/A") }
|
||||
func (*EV) EventLog(topic string) List[string, uint64, evvent] { panic("N/A") }
|
||||
func (*EV) Subscribe(topic string, after uint64) evvesub { panic("N/A") }
|
||||
func (*EV) Load(topic string, a aggr) error { panic("N/A") }
|
||||
func (*EV) Store(a aggr) error { panic("N/A") }
|
||||
func (*EV) Truncate(topic string) error { panic("N/A") }
|
|
@ -1,38 +0,0 @@
|
|||
package clean
|
||||
|
||||
type GPD[K comparable, V any] interface {
|
||||
Get(...K) ([]V, error)
|
||||
Put(K, V) error
|
||||
Delete(K) error
|
||||
}
|
||||
|
||||
type Edge[C, K comparable] struct {
|
||||
Key K
|
||||
Kursor C
|
||||
}
|
||||
type Page[C, K comparable] struct {
|
||||
Edges Edge[C, K]
|
||||
Start C
|
||||
End C
|
||||
Next bool
|
||||
Prev bool
|
||||
}
|
||||
type List[K, C comparable, V any] interface {
|
||||
First(n uint64, after C) ([]Page[C, K], error)
|
||||
Last(n uint64, before C) ([]Page[C, K], error)
|
||||
}
|
||||
type Emitter[T comparable, E any] interface {
|
||||
Emit(T, E) error
|
||||
}
|
||||
type Subscription[E any] interface {
|
||||
Recv() error
|
||||
Events() []E
|
||||
Close()
|
||||
}
|
||||
type Subscriber[T comparable, E any] interface {
|
||||
Subscribe(T, uint64) Subscription[E]
|
||||
}
|
||||
type Bus[T, K comparable, E any] interface {
|
||||
Emitter[T, E]
|
||||
Subscriber[T, E]
|
||||
}
|
35
internal/graph/generated/federation.go
Normal file
35
internal/graph/generated/federation.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/plugin/federation/fedruntime"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownType = errors.New("unknown type")
|
||||
ErrTypeNotFound = errors.New("type not found")
|
||||
)
|
||||
|
||||
func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime.Service, error) {
|
||||
if ec.DisableIntrospection {
|
||||
return fedruntime.Service{}, errors.New("federated introspection disabled")
|
||||
}
|
||||
|
||||
var sdl []string
|
||||
|
||||
for _, src := range sources {
|
||||
if src.BuiltIn {
|
||||
continue
|
||||
}
|
||||
sdl = append(sdl, src.Input)
|
||||
}
|
||||
|
||||
return fedruntime.Service{
|
||||
SDL: strings.Join(sdl, "\n"),
|
||||
}, nil
|
||||
}
|
6032
internal/graph/generated/generated.go
Normal file
6032
internal/graph/generated/generated.go
Normal file
File diff suppressed because it is too large
Load Diff
3
internal/graph/model/models_gen.go
Normal file
3
internal/graph/model/models_gen.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package model
|
68
internal/lg/init.go
Normal file
68
internal/lg/init.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package lg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context, name string) (context.Context, func() error) {
|
||||
ctx, span := Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
stop := [3]func() error{
|
||||
initLogger(name),
|
||||
}
|
||||
ctx, stop[1] = initMetrics(ctx, name)
|
||||
ctx, stop[2] = initTracing(ctx, name)
|
||||
|
||||
reverse(stop[:])
|
||||
|
||||
return ctx, func() error {
|
||||
log.Println("flushing logs...")
|
||||
errs := make([]error, len(stop))
|
||||
for i, fn := range stop {
|
||||
if fn != nil {
|
||||
errs[i] = fn()
|
||||
}
|
||||
}
|
||||
log.Println("all stopped.")
|
||||
return multierr.Combine(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
func env(name, defaultValue string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
defaultValue = strings.TrimSpace(defaultValue)
|
||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
||||
log.Println("# ", name, "=", v)
|
||||
return v
|
||||
}
|
||||
log.Println("# ", name, "=", defaultValue, "(default)")
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
type secret string
|
||||
|
||||
func (s secret) String() string {
|
||||
if s == "" {
|
||||
return "(nil)"
|
||||
}
|
||||
return "***"
|
||||
}
|
||||
func (s secret) Secret() string {
|
||||
return string(s)
|
||||
}
|
||||
func envSecret(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)
|
||||
}
|
111
internal/lg/logger.go
Normal file
111
internal/lg/logger.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package lg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/stdr"
|
||||
"github.com/logzio/logzio-go"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
type logzwriter struct {
|
||||
name string
|
||||
pkg string
|
||||
goversion string
|
||||
hostname string
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (l *logzwriter) Write(b []byte) (int, error) {
|
||||
i := 0
|
||||
for _, sp := range bytes.Split(b, []byte("\n")) {
|
||||
msg := struct {
|
||||
Message string `json:"message"`
|
||||
Host string `json:"host"`
|
||||
GoVersion string `json:"go_version"`
|
||||
Package string `json:"pkg"`
|
||||
App string `json:"app"`
|
||||
}{
|
||||
Message: strings.TrimSpace(string(sp)),
|
||||
Host: l.hostname,
|
||||
GoVersion: l.goversion,
|
||||
Package: l.pkg,
|
||||
App: l.name,
|
||||
}
|
||||
|
||||
if msg.Message == "" || strings.HasPrefix(msg.Message, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
b, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
j, err := l.w.Write(b)
|
||||
|
||||
i += j
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func initLogger(name string) func() error {
|
||||
log.SetPrefix("[" + name + "] ")
|
||||
log.SetFlags(log.LstdFlags&^(log.Ldate|log.Ltime) | log.Lshortfile)
|
||||
|
||||
token := envSecret("LOGZIO_LOG_TOKEN", "")
|
||||
if token == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
l, err := logzio.New(
|
||||
token.Secret(),
|
||||
// logzio.SetDebug(os.Stderr),
|
||||
logzio.SetUrl(env("LOGZIO_LOG_URL", "https://listener.lg.io:8071")),
|
||||
logzio.SetDrainDuration(time.Second*5),
|
||||
logzio.SetTempDirectory(env("LOGZIO_DIR", os.TempDir())),
|
||||
logzio.SetCheckDiskSpace(true),
|
||||
logzio.SetDrainDiskThreshold(70),
|
||||
)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := io.MultiWriter(os.Stderr, lzw(l, name))
|
||||
log.SetOutput(w)
|
||||
otel.SetLogger(stdr.New(log.Default()))
|
||||
|
||||
return func() error {
|
||||
defer log.Println("logger stopped")
|
||||
log.SetOutput(os.Stderr)
|
||||
l.Stop()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func lzw(l io.Writer, name string) io.Writer {
|
||||
lz := &logzwriter{
|
||||
name: name,
|
||||
w: l,
|
||||
}
|
||||
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
lz.goversion = info.GoVersion
|
||||
lz.pkg = info.Path
|
||||
}
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
lz.hostname = hostname
|
||||
}
|
||||
|
||||
return lz
|
||||
}
|
103
internal/lg/metric.go
Normal file
103
internal/lg/metric.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package lg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/contrib/instrumentation/runtime"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/metric/global"
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/histogram"
|
||||
controller "go.opentelemetry.io/otel/sdk/metric/controller/basic"
|
||||
"go.opentelemetry.io/otel/sdk/metric/export/aggregation"
|
||||
processor "go.opentelemetry.io/otel/sdk/metric/processor/basic"
|
||||
selector "go.opentelemetry.io/otel/sdk/metric/selector/simple"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
|
||||
)
|
||||
|
||||
var meterKey = contextKey{"meter"}
|
||||
var promHTTPKey = contextKey{"promHTTP"}
|
||||
|
||||
func Meter(ctx context.Context) metric.Meter {
|
||||
if t := fromContext[contextKey, metric.Meter](ctx, tracerKey); t != nil {
|
||||
return t
|
||||
}
|
||||
return global.Meter("")
|
||||
}
|
||||
func NewHTTP(ctx context.Context) *httpHandle {
|
||||
t := fromContext[contextKey, *prometheus.Exporter](ctx, promHTTPKey)
|
||||
return &httpHandle{t}
|
||||
}
|
||||
|
||||
func initMetrics(ctx context.Context, name string) (context.Context, func() error) {
|
||||
goversion := ""
|
||||
pkg := ""
|
||||
host := ""
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
goversion = info.GoVersion
|
||||
pkg = info.Path
|
||||
}
|
||||
if h, err := os.Hostname(); err == nil {
|
||||
host = h
|
||||
}
|
||||
|
||||
config := prometheus.Config{
|
||||
DefaultHistogramBoundaries: []float64{
|
||||
2 << 6, 2 << 8, 2 << 10, 2 << 12, 2 << 14, 2 << 16, 2 << 18, 2 << 20, 2 << 22, 2 << 24, 2 << 26, 2 << 28,
|
||||
},
|
||||
}
|
||||
cont := controller.New(
|
||||
processor.NewFactory(
|
||||
selector.NewWithHistogramDistribution(
|
||||
histogram.WithExplicitBoundaries(config.DefaultHistogramBoundaries),
|
||||
),
|
||||
aggregation.CumulativeTemporalitySelector(),
|
||||
processor.WithMemory(true),
|
||||
),
|
||||
controller.WithResource(
|
||||
resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
attribute.String("app", name),
|
||||
attribute.String("host", host),
|
||||
attribute.String("go_version", goversion),
|
||||
attribute.String("pkg", pkg),
|
||||
),
|
||||
),
|
||||
)
|
||||
ex, err := prometheus.New(config, cont)
|
||||
if err != nil {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
ctx = toContext(ctx, promHTTPKey, ex)
|
||||
|
||||
global.SetMeterProvider(cont)
|
||||
m := cont.Meter(name)
|
||||
ctx = toContext(ctx, meterKey, m)
|
||||
runtime.Start()
|
||||
|
||||
return ctx, func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
defer log.Println("metrics stopped")
|
||||
return cont.Stop(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
type httpHandle struct {
|
||||
exp *prometheus.Exporter
|
||||
}
|
||||
|
||||
func (h *httpHandle) RegisterHTTP(mux *http.ServeMux) {
|
||||
if h.exp == nil {
|
||||
return
|
||||
}
|
||||
mux.Handle("/metrics", h.exp)
|
||||
}
|
166
internal/lg/tracer.go
Normal file
166
internal/lg/tracer.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
package lg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var tracerKey = contextKey{"tracer"}
|
||||
|
||||
func Tracer(ctx context.Context) trace.Tracer {
|
||||
if t := fromContext[contextKey, trace.Tracer](ctx, tracerKey); t != nil {
|
||||
return t
|
||||
}
|
||||
return otel.Tracer("")
|
||||
}
|
||||
|
||||
func attrs(ctx context.Context) (string, []attribute.KeyValue) {
|
||||
var attrs []attribute.KeyValue
|
||||
var name string
|
||||
if pc, file, line, ok := runtime.Caller(2); ok {
|
||||
if fn := runtime.FuncForPC(pc); fn != nil {
|
||||
name = fn.Name()
|
||||
}
|
||||
attrs = append(attrs,
|
||||
attribute.String("pc", fmt.Sprintf("%v", pc)),
|
||||
attribute.String("file", file),
|
||||
attribute.Int("line", line),
|
||||
attribute.String("name", name),
|
||||
)
|
||||
}
|
||||
return name, attrs
|
||||
}
|
||||
|
||||
func Span(ctx context.Context, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||
name, attrs := attrs(ctx)
|
||||
ctx, span := Tracer(ctx).Start(ctx, name, opts...)
|
||||
span.SetAttributes(attrs...)
|
||||
|
||||
return ctx, span
|
||||
}
|
||||
|
||||
func Fork(ctx context.Context, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||
name, attrs := attrs(ctx)
|
||||
childCTX, childSpan := Tracer(ctx).Start(context.Background(), name, append(opts, trace.WithLinks(trace.LinkFromContext(ctx)))...)
|
||||
childSpan.SetAttributes(attrs...)
|
||||
|
||||
_, span := Tracer(ctx).Start(ctx, name, append(opts, trace.WithLinks(trace.LinkFromContext(childCTX)))...)
|
||||
span.SetAttributes(attrs...)
|
||||
defer span.End()
|
||||
|
||||
return childCTX, childSpan
|
||||
}
|
||||
|
||||
type SampleRate string
|
||||
|
||||
const (
|
||||
SampleAlways SampleRate = "always"
|
||||
SampleNever SampleRate = "never"
|
||||
)
|
||||
|
||||
func initTracing(ctx context.Context, name string) (context.Context, func() error) {
|
||||
res, err := resource.New(ctx,
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceNameKey.String(name),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Println(wrap(err, "failed to create trace resource"))
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
exporterAddr := env("EV_TRACE_ENDPOINT", "")
|
||||
if exporterAddr == "" {
|
||||
return ctx, nil
|
||||
}
|
||||
traceExporter, err := otlptracehttp.New(ctx,
|
||||
otlptracehttp.WithInsecure(),
|
||||
otlptracehttp.WithEndpoint(exporterAddr),
|
||||
)
|
||||
if err != nil {
|
||||
log.Println(wrap(err, "failed to create trace exporter"))
|
||||
return ctx, nil
|
||||
}
|
||||
bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
|
||||
|
||||
var sample sdktrace.TracerProviderOption
|
||||
sampleRate := SampleRate(env("EV_TRACE_SAMPLE", string(SampleNever)))
|
||||
switch sampleRate {
|
||||
case "always":
|
||||
sample = sdktrace.WithSampler(sdktrace.AlwaysSample())
|
||||
case "never":
|
||||
sample = sdktrace.WithSampler(sdktrace.NeverSample())
|
||||
default:
|
||||
if v, err := strconv.Atoi(string(sampleRate)); err != nil {
|
||||
sample = sdktrace.WithSampler(sdktrace.NeverSample())
|
||||
} else {
|
||||
sample = sdktrace.WithSampler(sdktrace.TraceIDRatioBased(float64(v) * 0.01))
|
||||
}
|
||||
}
|
||||
|
||||
tracerProvider := sdktrace.NewTracerProvider(
|
||||
sample,
|
||||
sdktrace.WithResource(res),
|
||||
sdktrace.WithSpanProcessor(bsp),
|
||||
)
|
||||
otel.SetTracerProvider(tracerProvider)
|
||||
otel.SetTextMapPropagator(propagation.TraceContext{})
|
||||
|
||||
ctx = toContext(ctx, tracerKey, otel.Tracer(name))
|
||||
|
||||
return ctx, func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
defer log.Println("tracer stopped")
|
||||
return wrap(tracerProvider.Shutdown(ctx), "failed to shutdown TracerProvider")
|
||||
}
|
||||
}
|
||||
|
||||
func wrap(err error, s string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf(s, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func reverse[T any](s []T) {
|
||||
first, last := 0, len(s)-1
|
||||
for first < last {
|
||||
s[first], s[last] = s[last], s[first]
|
||||
first++
|
||||
last--
|
||||
}
|
||||
}
|
||||
|
||||
func Htrace(h http.Handler, name string) http.Handler {
|
||||
return otelhttp.NewHandler(h, name)
|
||||
}
|
||||
|
||||
func toContext[K comparable, V any](ctx context.Context, key K, value V) context.Context {
|
||||
return context.WithValue(ctx, key, value)
|
||||
}
|
||||
func fromContext[K comparable, V any](ctx context.Context, key K) V {
|
||||
var empty V
|
||||
if v, ok := ctx.Value(key).(V); ok {
|
||||
return v
|
||||
}
|
||||
return empty
|
||||
}
|
171
main.go
Normal file
171
main.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/sour-is/ev/app/gql"
|
||||
"github.com/sour-is/ev/app/msgbus"
|
||||
"github.com/sour-is/ev/app/peerfinder"
|
||||
"github.com/sour-is/ev/app/salty"
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
diskstore "github.com/sour-is/ev/pkg/es/driver/disk-store"
|
||||
memstore "github.com/sour-is/ev/pkg/es/driver/mem-store"
|
||||
"github.com/sour-is/ev/pkg/es/driver/projecter"
|
||||
"github.com/sour-is/ev/pkg/es/driver/streamer"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
"github.com/sour-is/ev/pkg/set"
|
||||
)
|
||||
|
||||
const AppName string = "sour.is-ev"
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
defer cancel()
|
||||
}()
|
||||
|
||||
ctx, stop := lg.Init(ctx, AppName)
|
||||
defer stop()
|
||||
|
||||
if err := run(ctx); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
func run(ctx context.Context) error {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
{
|
||||
ctx, span := lg.Span(ctx)
|
||||
|
||||
err := multierr.Combine(
|
||||
es.Init(ctx),
|
||||
event.Init(ctx),
|
||||
diskstore.Init(ctx),
|
||||
memstore.Init(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
es, err := es.Open(
|
||||
ctx,
|
||||
env("EV_DATA", "mem:"),
|
||||
streamer.New(ctx),
|
||||
projecter.New(ctx, projecter.DefaultProjection),
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
s := http.Server{
|
||||
Addr: env("EV_HTTP", ":8080"),
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s.Addr, ":") {
|
||||
s.Addr = "[::]" + s.Addr
|
||||
}
|
||||
|
||||
enable := set.New(strings.Fields(env("EV_ENABLE", "salty msgbus gql peers"))...)
|
||||
var svcs []interface{ RegisterHTTP(*http.ServeMux) }
|
||||
|
||||
svcs = append(svcs, es)
|
||||
|
||||
if enable.Has("salty") {
|
||||
span.AddEvent("Enable Salty")
|
||||
base, err := url.JoinPath(env("EV_BASE_URL", "http://"+s.Addr), "inbox")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
salty, err := salty.New(ctx, es, base)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
svcs = append(svcs, salty)
|
||||
}
|
||||
|
||||
if enable.Has("msgbus") {
|
||||
span.AddEvent("Enable Msgbus")
|
||||
msgbus, err := msgbus.New(ctx, es)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
svcs = append(svcs, msgbus)
|
||||
}
|
||||
|
||||
if enable.Has("peers") {
|
||||
span.AddEvent("Enable Peers")
|
||||
peers, err := peerfinder.New(ctx, es)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
svcs = append(svcs, peers)
|
||||
}
|
||||
|
||||
if enable.Has("gql") {
|
||||
span.AddEvent("Enable GraphQL")
|
||||
gql, err := gql.New(ctx, svcs...)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
svcs = append(svcs, gql)
|
||||
}
|
||||
svcs = append(svcs, lg.NewHTTP(ctx))
|
||||
|
||||
s.Handler = httpMux(svcs...)
|
||||
|
||||
log.Print("Listen on ", s.Addr)
|
||||
span.AddEvent("begin listen and serve on " + s.Addr)
|
||||
|
||||
Mup, err := lg.Meter(ctx).SyncInt64().UpDownCounter("up")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Mup.Add(ctx, 1)
|
||||
|
||||
g.Go(s.ListenAndServe)
|
||||
|
||||
g.Go(func() error {
|
||||
<-ctx.Done()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return s.Shutdown(ctx)
|
||||
})
|
||||
|
||||
span.End()
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func env(name, defaultValue string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
defaultValue = strings.TrimSpace(defaultValue)
|
||||
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
||||
log.Println("#", name, "=", v)
|
||||
return v
|
||||
}
|
||||
log.Println("#", name, "=", defaultValue, "(default)")
|
||||
return defaultValue
|
||||
}
|
238
pkg/cache/cache.go
vendored
Normal file
238
pkg/cache/cache.go
vendored
Normal file
|
@ -0,0 +1,238 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
|
||||
DefaultEvictedBufferSize = 16
|
||||
)
|
||||
|
||||
// Cache is a thread-safe fixed size LRU cache.
|
||||
type Cache[K comparable, V any] struct {
|
||||
lru *LRU[K, V]
|
||||
evictedKeys []K
|
||||
evictedVals []V
|
||||
onEvictedCB func(ctx context.Context, k K, v V)
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates an LRU of the given size.
|
||||
func NewCache[K comparable, V any](size int) (*Cache[K, V], error) {
|
||||
return NewWithEvict[K, V](size, nil)
|
||||
}
|
||||
|
||||
// NewWithEvict constructs a fixed size cache with the given eviction
|
||||
// callback.
|
||||
func NewWithEvict[K comparable, V any](size int, onEvicted func(context.Context, K, V)) (c *Cache[K, V], err error) {
|
||||
// create a cache with default settings
|
||||
c = &Cache[K, V]{
|
||||
onEvictedCB: onEvicted,
|
||||
}
|
||||
if onEvicted != nil {
|
||||
c.initEvictBuffers()
|
||||
onEvicted = c.onEvicted
|
||||
}
|
||||
c.lru, err = NewLRU(size, onEvicted)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) initEvictBuffers() {
|
||||
c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize)
|
||||
c.evictedVals = make([]V, 0, DefaultEvictedBufferSize)
|
||||
}
|
||||
|
||||
// onEvicted save evicted key/val and sent in externally registered callback
|
||||
// outside of critical section
|
||||
func (c *Cache[K, V]) onEvicted(ctx context.Context, k K, v V) {
|
||||
c.evictedKeys = append(c.evictedKeys, k)
|
||||
c.evictedVals = append(c.evictedVals, v)
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *Cache[K, V]) Purge(ctx context.Context) {
|
||||
var ks []K
|
||||
var vs []V
|
||||
c.lock.Lock()
|
||||
c.lru.Purge(ctx)
|
||||
if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
|
||||
ks, vs = c.evictedKeys, c.evictedVals
|
||||
c.initEvictBuffers()
|
||||
}
|
||||
c.lock.Unlock()
|
||||
// invoke callback outside of critical section
|
||||
if c.onEvictedCB != nil {
|
||||
for i := 0; i < len(ks); i++ {
|
||||
c.onEvictedCB(ctx, ks[i], vs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *Cache[K, V]) Add(ctx context.Context, key K, value V) (evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Add(ctx, key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(ctx, k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *Cache[K, V]) Get(key K) (value *V, ok bool) {
|
||||
c.lock.Lock()
|
||||
value, ok = c.lru.Get(key)
|
||||
c.lock.Unlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the
|
||||
// recent-ness or deleting it for being stale.
|
||||
func (c *Cache[K, V]) Contains(key K) bool {
|
||||
c.lock.RLock()
|
||||
containKey := c.lru.Contains(key)
|
||||
c.lock.RUnlock()
|
||||
return containKey
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *Cache[K, V]) Peek(key K) (value *V, ok bool) {
|
||||
c.lock.RLock()
|
||||
value, ok = c.lru.Peek(key)
|
||||
c.lock.RUnlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// ContainsOrAdd checks if a key is in the cache without updating the
|
||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
||||
// Returns whether found and whether an eviction occurred.
|
||||
func (c *Cache[K, V]) ContainsOrAdd(ctx context.Context, key K, value V) (ok, evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
if c.lru.Contains(key) {
|
||||
c.lock.Unlock()
|
||||
return true, false
|
||||
}
|
||||
evicted = c.lru.Add(ctx, key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(ctx, k, v)
|
||||
}
|
||||
return false, evicted
|
||||
}
|
||||
|
||||
// PeekOrAdd checks if a key is in the cache without updating the
|
||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
||||
// Returns whether found and whether an eviction occurred.
|
||||
func (c *Cache[K, V]) PeekOrAdd(ctx context.Context, key K, value V) (previous *V, ok, evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
previous, ok = c.lru.Peek(key)
|
||||
if ok {
|
||||
c.lock.Unlock()
|
||||
return previous, true, false
|
||||
}
|
||||
evicted = c.lru.Add(ctx, key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(ctx, k, v)
|
||||
}
|
||||
return nil, false, evicted
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache.
|
||||
func (c *Cache[K, V]) Remove(ctx context.Context, key K) (present bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
present = c.lru.Remove(ctx, key)
|
||||
if c.onEvictedCB != nil && present {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && present {
|
||||
c.onEvicted(ctx, k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *Cache[K, V]) Resize(ctx context.Context, size int) (evicted int) {
|
||||
var ks []K
|
||||
var vs []V
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Resize(ctx, size)
|
||||
if c.onEvictedCB != nil && evicted > 0 {
|
||||
ks, vs = c.evictedKeys, c.evictedVals
|
||||
c.initEvictBuffers()
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && evicted > 0 {
|
||||
for i := 0; i < len(ks); i++ {
|
||||
c.onEvictedCB(ctx, ks[i], vs[i])
|
||||
}
|
||||
}
|
||||
return evicted
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *Cache[K, V]) RemoveOldest(ctx context.Context) (key *K, value *V, ok bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
key, value, ok = c.lru.RemoveOldest(ctx)
|
||||
if c.onEvictedCB != nil && ok {
|
||||
k, v = c.evictedKeys[0], c.evictedVals[0]
|
||||
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if c.onEvictedCB != nil && ok {
|
||||
c.onEvictedCB(ctx, k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *Cache[K, V]) GetOldest() (key *K, value *V, ok bool) {
|
||||
c.lock.RLock()
|
||||
key, value, ok = c.lru.GetOldest()
|
||||
c.lock.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *Cache[K, V]) Keys() []K {
|
||||
c.lock.RLock()
|
||||
keys := c.lru.Keys()
|
||||
c.lock.RUnlock()
|
||||
return keys
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache[K, V]) Len() int {
|
||||
c.lock.RLock()
|
||||
length := c.lru.Len()
|
||||
c.lock.RUnlock()
|
||||
return length
|
||||
}
|
131
pkg/cache/cache_test.go
vendored
Normal file
131
pkg/cache/cache_test.go
vendored
Normal file
|
@ -0,0 +1,131 @@
|
|||
package cache_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"github.com/sour-is/ev/pkg/cache"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := cache.NewCache[string, int](1)
|
||||
is.NoErr(err)
|
||||
|
||||
evicted := c.Add(ctx, "one", 1)
|
||||
is.True(!evicted)
|
||||
|
||||
is.True(c.Contains("one"))
|
||||
_, ok := c.Peek("one")
|
||||
is.True(ok)
|
||||
|
||||
ok, evicted = c.ContainsOrAdd(ctx, "two", 2)
|
||||
is.True(!ok)
|
||||
is.True(evicted)
|
||||
|
||||
is.True(!c.Contains("one"))
|
||||
is.True(c.Contains("two"))
|
||||
|
||||
is.Equal(c.Len(), 1)
|
||||
is.Equal(c.Keys(), []string{"two"})
|
||||
|
||||
v, ok := c.Get("two")
|
||||
is.True(ok)
|
||||
is.Equal(*v, 2)
|
||||
|
||||
evictCount := c.Resize(ctx, 100)
|
||||
is.True(evictCount == 0)
|
||||
|
||||
c.Add(ctx, "one", 1)
|
||||
|
||||
prev, ok, evicted := c.PeekOrAdd(ctx, "three", 3)
|
||||
is.True(!ok)
|
||||
is.True(!evicted)
|
||||
is.Equal(prev, nil)
|
||||
|
||||
key, value, ok := c.GetOldest()
|
||||
is.True(ok)
|
||||
is.Equal(*key, "two")
|
||||
is.Equal(*value, 2)
|
||||
|
||||
key, value, ok = c.RemoveOldest(ctx)
|
||||
is.True(ok)
|
||||
is.Equal(*key, "two")
|
||||
is.Equal(*value, 2)
|
||||
|
||||
c.Remove(ctx, "one")
|
||||
|
||||
c.Purge(ctx)
|
||||
is.True(!c.Contains("three"))
|
||||
}
|
||||
|
||||
func TestCacheWithEvict(t *testing.T) {
|
||||
is := is.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
evictions := 0
|
||||
|
||||
c, err := cache.NewWithEvict(1, func(ctx context.Context, s string, i int) { evictions++ })
|
||||
is.NoErr(err)
|
||||
|
||||
key, value, ok := c.GetOldest()
|
||||
is.True(!ok)
|
||||
is.Equal(key, nil)
|
||||
is.Equal(value, nil)
|
||||
|
||||
key, value, ok = c.RemoveOldest(ctx)
|
||||
is.True(!ok)
|
||||
is.Equal(key, nil)
|
||||
is.Equal(value, nil)
|
||||
|
||||
evicted := c.Add(ctx, "one", 1)
|
||||
is.True(!evicted)
|
||||
|
||||
is.True(c.Contains("one"))
|
||||
_, ok = c.Peek("one")
|
||||
is.True(ok)
|
||||
|
||||
ok, evicted = c.ContainsOrAdd(ctx, "two", 2)
|
||||
is.True(!ok)
|
||||
is.True(evicted)
|
||||
|
||||
is.True(!c.Contains("one"))
|
||||
is.True(c.Contains("two"))
|
||||
|
||||
is.Equal(c.Len(), 1)
|
||||
is.Equal(c.Keys(), []string{"two"})
|
||||
|
||||
v, ok := c.Get("two")
|
||||
is.True(ok)
|
||||
is.Equal(*v, 2)
|
||||
|
||||
evictCount := c.Resize(ctx, 100)
|
||||
is.True(evictCount == 0)
|
||||
|
||||
c.Add(ctx, "one", 1)
|
||||
|
||||
prev, ok, evicted := c.PeekOrAdd(ctx, "three", 3)
|
||||
is.True(!ok)
|
||||
is.True(!evicted)
|
||||
is.Equal(prev, nil)
|
||||
|
||||
key, value, ok = c.GetOldest()
|
||||
is.True(ok)
|
||||
is.Equal(*key, "two")
|
||||
is.Equal(*value, 2)
|
||||
|
||||
key, value, ok = c.RemoveOldest(ctx)
|
||||
is.True(ok)
|
||||
is.Equal(*key, "two")
|
||||
is.Equal(*value, 2)
|
||||
|
||||
c.Resize(ctx, 1)
|
||||
|
||||
c.Purge(ctx)
|
||||
is.True(!c.Contains("three"))
|
||||
|
||||
is.Equal(evictions, 4)
|
||||
}
|
235
pkg/cache/list.go
vendored
Normal file
235
pkg/cache/list.go
vendored
Normal file
|
@ -0,0 +1,235 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package list implements a doubly linked list.
|
||||
//
|
||||
// To iterate over a list (where l is a *List):
|
||||
//
|
||||
// for e := l.Front(); e != nil; e = e.Next() {
|
||||
// // do something with e.Value
|
||||
// }
|
||||
package cache
|
||||
|
||||
// Element is an element of a linked list.
|
||||
type Element[V any] struct {
|
||||
// Next and previous pointers in the doubly-linked list of elements.
|
||||
// To simplify the implementation, internally a list l is implemented
|
||||
// as a ring, such that &l.root is both the next element of the last
|
||||
// list element (l.Back()) and the previous element of the first list
|
||||
// element (l.Front()).
|
||||
next, prev *Element[V]
|
||||
|
||||
// The list to which this element belongs.
|
||||
list *List[V]
|
||||
|
||||
// The value stored with this element.
|
||||
Value V
|
||||
}
|
||||
|
||||
// Next returns the next list element or nil.
|
||||
func (e *Element[V]) Next() *Element[V] {
|
||||
if p := e.next; e.list != nil && p != &e.list.root {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prev returns the previous list element or nil.
|
||||
func (e *Element[V]) Prev() *Element[V] {
|
||||
if p := e.prev; e.list != nil && p != &e.list.root {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List represents a doubly linked list.
|
||||
// The zero value for List is an empty list ready to use.
|
||||
type List[V any] struct {
|
||||
root Element[V] // sentinel list element, only &root, root.prev, and root.next are used
|
||||
len int // current list length excluding (this) sentinel element
|
||||
}
|
||||
|
||||
// Init initializes or clears list l.
|
||||
func (l *List[V]) Init() *List[V] {
|
||||
l.root.next = &l.root
|
||||
l.root.prev = &l.root
|
||||
l.len = 0
|
||||
return l
|
||||
}
|
||||
|
||||
// NewList returns an initialized list.
|
||||
func NewList[V any]() *List[V] { return new(List[V]).Init() }
|
||||
|
||||
// Len returns the number of elements of list l.
|
||||
// The complexity is O(1).
|
||||
func (l *List[V]) Len() int { return l.len }
|
||||
|
||||
// Front returns the first element of list l or nil if the list is empty.
|
||||
func (l *List[V]) Front() *Element[V] {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.next
|
||||
}
|
||||
|
||||
// Back returns the last element of list l or nil if the list is empty.
|
||||
func (l *List[V]) Back() *Element[V] {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.prev
|
||||
}
|
||||
|
||||
// lazyInit lazily initializes a zero List value.
|
||||
func (l *List[V]) lazyInit() {
|
||||
if l.root.next == nil {
|
||||
l.Init()
|
||||
}
|
||||
}
|
||||
|
||||
// insert inserts e after at, increments l.len, and returns e.
|
||||
func (l *List[V]) insert(e, at *Element[V]) *Element[V] {
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
e.list = l
|
||||
l.len++
|
||||
return e
|
||||
}
|
||||
|
||||
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
|
||||
func (l *List[V]) insertValue(v V, at *Element[V]) *Element[V] {
|
||||
return l.insert(&Element[V]{Value: v}, at)
|
||||
}
|
||||
|
||||
// remove removes e from its list, decrements l.len
|
||||
func (l *List[V]) remove(e *Element[V]) {
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
e.next = nil // avoid memory leaks
|
||||
e.prev = nil // avoid memory leaks
|
||||
e.list = nil
|
||||
l.len--
|
||||
}
|
||||
|
||||
// move moves e to next to at.
|
||||
func (l *List[V]) move(e, at *Element[V]) {
|
||||
if e == at {
|
||||
return
|
||||
}
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
}
|
||||
|
||||
// Remove removes e from l if e is an element of list l.
|
||||
// It returns the element value e.Value.
|
||||
// The element must not be nil.
|
||||
func (l *List[V]) Remove(e *Element[V]) any {
|
||||
if e.list == l {
|
||||
// if e.list == l, l must have been initialized when e was inserted
|
||||
// in l or l == nil (e is a zero Element) and l.remove will crash
|
||||
l.remove(e)
|
||||
}
|
||||
return e.Value
|
||||
}
|
||||
|
||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
||||
func (l *List[V]) PushFront(v V) *Element[V] {
|
||||
l.lazyInit()
|
||||
return l.insertValue(v, &l.root)
|
||||
}
|
||||
|
||||
// PushBack inserts a new element e with value v at the back of list l and returns e.
|
||||
func (l *List[V]) PushBack(v V) *Element[V] {
|
||||
l.lazyInit()
|
||||
return l.insertValue(v, l.root.prev)
|
||||
}
|
||||
|
||||
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
|
||||
// If mark is not an element of l, the list is not modified.
|
||||
// The mark must not be nil.
|
||||
func (l *List[V]) InsertBefore(v V, mark *Element[V]) *Element[V] {
|
||||
if mark.list != l {
|
||||
return nil
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
return l.insertValue(v, mark.prev)
|
||||
}
|
||||
|
||||
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
|
||||
// If mark is not an element of l, the list is not modified.
|
||||
// The mark must not be nil.
|
||||
func (l *List[V]) InsertAfter(v V, mark *Element[V]) *Element[V] {
|
||||
if mark.list != l {
|
||||
return nil
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
return l.insertValue(v, mark)
|
||||
}
|
||||
|
||||
// MoveToFront moves element e to the front of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
// The element must not be nil.
|
||||
func (l *List[V]) MoveToFront(e *Element[V]) {
|
||||
if e.list != l || l.root.next == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.move(e, &l.root)
|
||||
}
|
||||
|
||||
// MoveToBack moves element e to the back of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
// The element must not be nil.
|
||||
func (l *List[V]) MoveToBack(e *Element[V]) {
|
||||
if e.list != l || l.root.prev == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.move(e, l.root.prev)
|
||||
}
|
||||
|
||||
// MoveBefore moves element e to its new position before mark.
|
||||
// If e or mark is not an element of l, or e == mark, the list is not modified.
|
||||
// The element and mark must not be nil.
|
||||
func (l *List[V]) MoveBefore(e, mark *Element[V]) {
|
||||
if e.list != l || e == mark || mark.list != l {
|
||||
return
|
||||
}
|
||||
l.move(e, mark.prev)
|
||||
}
|
||||
|
||||
// MoveAfter moves element e to its new position after mark.
|
||||
// If e or mark is not an element of l, or e == mark, the list is not modified.
|
||||
// The element and mark must not be nil.
|
||||
func (l *List[V]) MoveAfter(e, mark *Element[V]) {
|
||||
if e.list != l || e == mark || mark.list != l {
|
||||
return
|
||||
}
|
||||
l.move(e, mark)
|
||||
}
|
||||
|
||||
// PushBackList inserts a copy of another list at the back of list l.
|
||||
// The lists l and other may be the same. They must not be nil.
|
||||
func (l *List[V]) PushBackList(other *List[V]) {
|
||||
l.lazyInit()
|
||||
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
|
||||
l.insertValue(e.Value, l.root.prev)
|
||||
}
|
||||
}
|
||||
|
||||
// PushFrontList inserts a copy of another list at the front of list l.
|
||||
// The lists l and other may be the same. They must not be nil.
|
||||
func (l *List[V]) PushFrontList(other *List[V]) {
|
||||
l.lazyInit()
|
||||
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
|
||||
l.insertValue(e.Value, &l.root)
|
||||
}
|
||||
}
|
175
pkg/cache/lru.go
vendored
Normal file
175
pkg/cache/lru.go
vendored
Normal file
|
@ -0,0 +1,175 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||
type EvictCallback[K comparable, V any] func(context.Context, K, V)
|
||||
|
||||
// LRU implements a non-thread safe fixed size LRU cache
|
||||
type LRU[K comparable, V any] struct {
|
||||
size int
|
||||
evictList *List[entry[K, V]]
|
||||
items map[K]*Element[entry[K, V]]
|
||||
onEvict EvictCallback[K, V]
|
||||
}
|
||||
|
||||
// entry is used to hold a value in the evictList
|
||||
type entry[K comparable, V any] struct {
|
||||
key K
|
||||
value V
|
||||
}
|
||||
|
||||
// NewLRU constructs an LRU of the given size
|
||||
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) {
|
||||
if size <= 0 {
|
||||
return nil, errors.New("must provide a positive size")
|
||||
}
|
||||
c := &LRU[K, V]{
|
||||
size: size,
|
||||
evictList: NewList[entry[K, V]](),
|
||||
items: make(map[K]*Element[entry[K, V]]),
|
||||
onEvict: onEvict,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *LRU[K, V]) Purge(ctx context.Context) {
|
||||
for k, v := range c.items {
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(ctx, k, v.Value.value)
|
||||
}
|
||||
delete(c.items, k)
|
||||
}
|
||||
c.evictList.Init()
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *LRU[K, V]) Add(ctx context.Context, key K, value V) (evicted bool) {
|
||||
// Check for existing item
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
ent.Value.value = value
|
||||
return false
|
||||
}
|
||||
|
||||
// Add new item
|
||||
entry := c.evictList.PushFront(entry[K, V]{key, value})
|
||||
c.items[key] = entry
|
||||
|
||||
evict := c.evictList.Len() > c.size
|
||||
// Verify size not exceeded
|
||||
if evict {
|
||||
c.removeOldest(ctx)
|
||||
}
|
||||
return evict
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *LRU[K, V]) Get(key K) (value *V, ok bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
if ent == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &ent.Value.value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the recent-ness
|
||||
// or deleting it for being stale.
|
||||
func (c *LRU[K, V]) Contains(key K) (ok bool) {
|
||||
_, ok = c.items[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *LRU[K, V]) Peek(key K) (value *V, ok bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
return &ent.Value.value, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache, returning if the
|
||||
// key was contained.
|
||||
func (c *LRU[K, V]) Remove(ctx context.Context, key K) (present bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.removeElement(ctx, ent)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *LRU[K, V]) RemoveOldest(ctx context.Context) (key *K, value *V, ok bool) {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
c.removeElement(ctx, ent)
|
||||
kv := ent.Value
|
||||
return &kv.key, &kv.value, true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *LRU[K, V]) GetOldest() (key *K, value *V, ok bool) {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
kv := ent.Value
|
||||
return &kv.key, &kv.value, true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *LRU[K, V]) Keys() []K {
|
||||
keys := make([]K, len(c.items))
|
||||
i := 0
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
||||
keys[i] = ent.Value.key
|
||||
i++
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *LRU[K, V]) Len() int {
|
||||
return c.evictList.Len()
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *LRU[K, V]) Resize(ctx context.Context, size int) (evicted int) {
|
||||
diff := c.Len() - size
|
||||
if diff < 0 {
|
||||
diff = 0
|
||||
}
|
||||
for i := 0; i < diff; i++ {
|
||||
c.removeOldest(ctx)
|
||||
}
|
||||
c.size = size
|
||||
return diff
|
||||
}
|
||||
|
||||
// removeOldest removes the oldest item from the cache.
|
||||
func (c *LRU[K, V]) removeOldest(ctx context.Context) {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
c.removeElement(ctx, ent)
|
||||
}
|
||||
}
|
||||
|
||||
// removeElement is used to remove a given list element from the cache
|
||||
func (c *LRU[K, V]) removeElement(ctx context.Context, e *Element[entry[K, V]]) {
|
||||
c.evictList.Remove(e)
|
||||
kv := e.Value
|
||||
delete(c.items, kv.key)
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(ctx, kv.key, kv.value)
|
||||
}
|
||||
}
|
306
pkg/es/driver/disk-store/disk-store.go
Normal file
306
pkg/es/driver/disk-store/disk-store.go
Normal file
|
@ -0,0 +1,306 @@
|
|||
package diskstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/wal"
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"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"
|
||||
"github.com/sour-is/ev/pkg/math"
|
||||
)
|
||||
|
||||
const CachSize = 1000
|
||||
|
||||
type lockedWal = locker.Locked[wal.Log]
|
||||
type openlogs struct {
|
||||
logs *cache.Cache[string, *lockedWal]
|
||||
}
|
||||
type diskStore struct {
|
||||
path string
|
||||
openlogs *locker.Locked[openlogs]
|
||||
|
||||
m_disk_open syncint64.Counter
|
||||
m_disk_evict syncint64.Counter
|
||||
m_disk_read syncint64.Counter
|
||||
m_disk_write syncint64.Counter
|
||||
}
|
||||
|
||||
const AppendOnly = es.AppendOnly
|
||||
const AllEvents = es.AllEvents
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
d := &diskStore{}
|
||||
|
||||
m := lg.Meter(ctx)
|
||||
var err, errs error
|
||||
|
||||
d.m_disk_open, err = m.SyncInt64().Counter("disk_open")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_evict, err = m.SyncInt64().Counter("disk_evict")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_read, err = m.SyncInt64().Counter("disk_read")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
d.m_disk_write, err = m.SyncInt64().Counter("disk_write")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
es.Register(ctx, "file", d)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*diskStore)(nil)
|
||||
|
||||
func (d *diskStore) Open(ctx context.Context, dsn string) (driver.Driver, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
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 {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c, err := cache.NewWithEvict(CachSize, func(ctx context.Context, s string, l *lockedWal) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
l.Modify(ctx, func(w *wal.Log) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
d.m_disk_evict.Add(ctx, 1)
|
||||
|
||||
err := w.Close()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
logs := &openlogs{logs: c}
|
||||
return &diskStore{
|
||||
path: path,
|
||||
openlogs: locker.New(logs),
|
||||
m_disk_open: d.m_disk_open,
|
||||
m_disk_evict: d.m_disk_evict,
|
||||
m_disk_read: d.m_disk_read,
|
||||
m_disk_write: d.m_disk_write,
|
||||
}, nil
|
||||
}
|
||||
func (d *diskStore) EventLog(ctx context.Context, streamID string) (driver.EventLog, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
el := &eventLog{streamID: streamID, diskStore: d}
|
||||
|
||||
return el, d.openlogs.Modify(ctx, func(openlogs *openlogs) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if events, ok := openlogs.logs.Get(streamID); ok {
|
||||
el.events = *events
|
||||
return nil
|
||||
}
|
||||
|
||||
d.m_disk_open.Add(ctx, 1)
|
||||
|
||||
l, err := wal.Open(filepath.Join(d.path, streamID), wal.DefaultOptions)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
el.events = locker.New(l)
|
||||
openlogs.logs.Add(ctx, streamID, el.events)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type eventLog struct {
|
||||
streamID string
|
||||
events *locker.Locked[wal.Log]
|
||||
diskStore *diskStore
|
||||
}
|
||||
|
||||
var _ driver.EventLog = (*eventLog)(nil)
|
||||
|
||||
func (e *eventLog) Append(ctx context.Context, events event.Events, version uint64) (uint64, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
event.SetStreamID(e.streamID, events...)
|
||||
|
||||
var count uint64
|
||||
err := e.events.Modify(ctx, func(l *wal.Log) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
last, err := l.LastIndex()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if version != AppendOnly && version != last {
|
||||
return fmt.Errorf("%w: current version wrong %d != %d", es.ErrWrongVersion, version, last)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
batch := &wal.Batch{}
|
||||
for i, e := range events {
|
||||
span.AddEvent(fmt.Sprintf("append event %d of %d", i, len(events)))
|
||||
|
||||
b, err = event.MarshalBinary(e)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
return err
|
||||
}
|
||||
pos := last + uint64(i) + 1
|
||||
event.SetPosition(e, pos)
|
||||
|
||||
batch.Write(pos, b)
|
||||
}
|
||||
|
||||
count = uint64(len(events))
|
||||
e.diskStore.m_disk_write.Add(ctx, int64(len(events)))
|
||||
|
||||
return l.WriteBatch(batch)
|
||||
})
|
||||
|
||||
return count, err
|
||||
}
|
||||
func (e *eventLog) Read(ctx context.Context, pos, count int64) (event.Events, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var events event.Events
|
||||
|
||||
err := e.events.Modify(ctx, func(stream *wal.Log) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
first, err := stream.FirstIndex()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
last, err := stream.LastIndex()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
// ---
|
||||
if first == 0 || last == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
start, count := math.PagerBox(first, last, pos, count)
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
events = make([]event.Event, math.Abs(count))
|
||||
for i := range events {
|
||||
span.AddEvent(fmt.Sprintf("read event %d of %d", i, len(events)))
|
||||
|
||||
// ---
|
||||
var b []byte
|
||||
b, err = stream.Read(start)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
events[i], err = event.UnmarshalBinary(ctx, b, start)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
// ---
|
||||
|
||||
if count > 0 {
|
||||
start += 1
|
||||
} else {
|
||||
start -= 1
|
||||
}
|
||||
if start < first || start > last {
|
||||
events = events[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event.SetStreamID(e.streamID, events...)
|
||||
e.diskStore.m_disk_read.Add(ctx, int64(len(events)))
|
||||
|
||||
return events, nil
|
||||
}
|
||||
func (e *eventLog) FirstIndex(ctx context.Context) (uint64, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var idx uint64
|
||||
var err error
|
||||
|
||||
err = e.events.Modify(ctx, func(events *wal.Log) error {
|
||||
idx, err = events.FirstIndex()
|
||||
return err
|
||||
})
|
||||
|
||||
return idx, err
|
||||
}
|
||||
func (e *eventLog) LastIndex(ctx context.Context) (uint64, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var idx uint64
|
||||
var err error
|
||||
|
||||
err = e.events.Modify(ctx, func(events *wal.Log) error {
|
||||
idx, err = events.LastIndex()
|
||||
return err
|
||||
})
|
||||
|
||||
return idx, err
|
||||
}
|
||||
func (e *eventLog) LoadForUpdate(ctx context.Context, a event.Aggregate, fn func(context.Context, event.Aggregate) error) (uint64, error) {
|
||||
panic("not implemented")
|
||||
}
|
32
pkg/es/driver/driver.go
Normal file
32
pkg/es/driver/driver.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
type Driver interface {
|
||||
Open(ctx context.Context, dsn string) (Driver, error)
|
||||
EventLog(ctx context.Context, streamID string) (EventLog, error)
|
||||
}
|
||||
|
||||
type EventLog interface {
|
||||
Read(ctx context.Context, pos, count int64) (event.Events, error)
|
||||
Append(ctx context.Context, events event.Events, version uint64) (uint64, error)
|
||||
FirstIndex(context.Context) (uint64, error)
|
||||
LastIndex(context.Context) (uint64, error)
|
||||
|
||||
LoadForUpdate(context.Context, event.Aggregate, func(context.Context, event.Aggregate) error) (uint64, error)
|
||||
}
|
||||
|
||||
type Subscription interface {
|
||||
Recv(context.Context) bool
|
||||
Events(context.Context) (event.Events, error)
|
||||
Close(context.Context) error
|
||||
}
|
||||
|
||||
type EventStream interface {
|
||||
Subscribe(ctx context.Context, streamID string, start int64) (Subscription, error)
|
||||
Send(ctx context.Context, streamID string, events event.Events) error
|
||||
}
|
195
pkg/es/driver/mem-store/mem-store.go
Normal file
195
pkg/es/driver/mem-store/mem-store.go
Normal file
|
@ -0,0 +1,195 @@
|
|||
package memstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
"github.com/sour-is/ev/pkg/math"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
streams map[string]*locker.Locked[event.Events]
|
||||
}
|
||||
type eventLog struct {
|
||||
streamID string
|
||||
events *locker.Locked[event.Events]
|
||||
}
|
||||
type memstore struct {
|
||||
state *locker.Locked[state]
|
||||
}
|
||||
|
||||
const AppendOnly = es.AppendOnly
|
||||
const AllEvents = es.AllEvents
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return es.Register(ctx, "mem", &memstore{})
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*memstore)(nil)
|
||||
|
||||
func (memstore) Open(ctx context.Context, name string) (driver.Driver, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
s := &state{streams: make(map[string]*locker.Locked[event.Events])}
|
||||
return &memstore{locker.New(s)}, nil
|
||||
}
|
||||
func (m *memstore) EventLog(ctx context.Context, streamID string) (driver.EventLog, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
el := &eventLog{streamID: streamID}
|
||||
|
||||
err := m.state.Modify(ctx, func(state *state) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
l, ok := state.streams[streamID]
|
||||
if !ok {
|
||||
l = locker.New(&event.Events{})
|
||||
state.streams[streamID] = l
|
||||
}
|
||||
el.events = l
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return el, err
|
||||
}
|
||||
|
||||
var _ driver.EventLog = (*eventLog)(nil)
|
||||
|
||||
// Append implements driver.EventStore
|
||||
func (m *eventLog) Append(ctx context.Context, events event.Events, version uint64) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
event.SetStreamID(m.streamID, events...)
|
||||
|
||||
return uint64(len(events)), m.events.Modify(ctx, func(stream *event.Events) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.AddEvent(fmt.Sprintf(" %s %d", m.streamID, len(*stream)))
|
||||
|
||||
last := uint64(len(*stream))
|
||||
if version != AppendOnly && version != last {
|
||||
return fmt.Errorf("%w: current version wrong %d != %d", es.ErrWrongVersion, version, last)
|
||||
}
|
||||
|
||||
for i := range events {
|
||||
span.AddEvent(fmt.Sprintf("read event %d of %d", i, len(events)))
|
||||
|
||||
// --- clone event
|
||||
e := events[i]
|
||||
b, err := event.MarshalBinary(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e, err = event.UnmarshalBinary(ctx, b, e.EventMeta().Position)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ---
|
||||
|
||||
pos := last + uint64(i) + 1
|
||||
event.SetPosition(e, pos)
|
||||
*stream = append(*stream, e)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Read implements driver.EventStore
|
||||
func (m *eventLog) Read(ctx context.Context, pos int64, count int64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var events event.Events
|
||||
|
||||
err := m.events.Modify(ctx, func(stream *event.Events) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.AddEvent(fmt.Sprintf("%s %d", m.streamID, len(*stream)))
|
||||
|
||||
first := stream.First().EventMeta().Position
|
||||
last := stream.Last().EventMeta().Position
|
||||
// ---
|
||||
if first == 0 || last == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
start, count := math.PagerBox(first, last, pos, count)
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
span.AddEvent(fmt.Sprint("box", first, last, pos, count))
|
||||
events = make([]event.Event, math.Abs(count))
|
||||
for i := range events {
|
||||
span.AddEvent(fmt.Sprintf("read event %d of %d", i, math.Abs(count)))
|
||||
|
||||
// --- clone event
|
||||
e := (*stream)[start-1]
|
||||
b, err := event.MarshalBinary(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
events[i], err = event.UnmarshalBinary(ctx, b, e.EventMeta().Position)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ---
|
||||
|
||||
if count > 0 {
|
||||
start += 1
|
||||
} else {
|
||||
start -= 1
|
||||
}
|
||||
if start < first || start > last {
|
||||
events = events[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
event.SetStreamID(m.streamID, events...)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// FirstIndex for the streamID
|
||||
func (m *eventLog) FirstIndex(ctx context.Context) (uint64, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
events, err := m.events.Copy(ctx)
|
||||
return events.First().EventMeta().Position, err
|
||||
}
|
||||
|
||||
// LastIndex for the streamID
|
||||
func (m *eventLog) LastIndex(ctx context.Context) (uint64, error) {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
events, err := m.events.Copy(ctx)
|
||||
return events.Last().EventMeta().Position, err
|
||||
}
|
||||
|
||||
func (m *eventLog) LoadForUpdate(ctx context.Context, a event.Aggregate, fn func(context.Context, event.Aggregate) error) (uint64, error) {
|
||||
panic("not implemented")
|
||||
}
|
|
@ -1,15 +1,13 @@
|
|||
// package projecter provides a driver middleware to derive new events from other events.
|
||||
package projecter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
"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"
|
||||
)
|
||||
|
||||
type projector struct {
|
||||
|
@ -17,21 +15,10 @@ type projector struct {
|
|||
fns []func(event.Event) []event.Event
|
||||
}
|
||||
|
||||
func New(_ context.Context, fns ...func(event.Event) []event.Event) *projector {
|
||||
func New(ctx context.Context, fns ...func(event.Event) []event.Event) *projector {
|
||||
return &projector{fns: fns}
|
||||
}
|
||||
func (p *projector) Apply(e *ev.EventStore) {
|
||||
up := e.Driver
|
||||
for up != nil {
|
||||
if op, ok := up.(*projector); ok {
|
||||
op.AddProjections(p.fns...)
|
||||
p.up = op.up
|
||||
return
|
||||
}
|
||||
|
||||
up = ev.Unwrap(up)
|
||||
}
|
||||
|
||||
func (p *projector) Apply(e *es.EventStore) {
|
||||
p.up = e.Driver
|
||||
e.Driver = p
|
||||
}
|
||||
|
@ -51,9 +38,6 @@ func (s *projector) EventLog(ctx context.Context, streamID string) (driver.Event
|
|||
l, err := s.up.EventLog(ctx, streamID)
|
||||
return &wrapper{l, s}, err
|
||||
}
|
||||
func (s *projector) AddProjections(fns ...func(event.Event) []event.Event) {
|
||||
s.fns = append(s.fns, fns...)
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
up driver.EventLog
|
||||
|
@ -62,20 +46,11 @@ type wrapper struct {
|
|||
|
||||
var _ driver.EventLog = (*wrapper)(nil)
|
||||
|
||||
func (r *wrapper) Unwrap() driver.EventLog {
|
||||
return r.up
|
||||
}
|
||||
func (w *wrapper) Read(ctx context.Context, after int64, count int64) (event.Events, error) {
|
||||
func (w *wrapper) Read(ctx context.Context, pos int64, count int64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.Read(ctx, after, count)
|
||||
}
|
||||
func (w *wrapper) ReadN(ctx context.Context, index ...uint64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.ReadN(ctx, index...)
|
||||
return w.up.Read(ctx, pos, count)
|
||||
}
|
||||
func (w *wrapper) Append(ctx context.Context, events event.Events, version uint64) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
|
@ -112,7 +87,7 @@ func (w *wrapper) Append(ctx context.Context, events event.Events, version uint6
|
|||
span.RecordError(err)
|
||||
continue
|
||||
}
|
||||
_, err = l.Append(ctx, event.NewEvents(e), ev.AppendOnly)
|
||||
_, err = l.Append(ctx, event.NewEvents(e), es.AppendOnly)
|
||||
span.RecordError(err)
|
||||
}
|
||||
}()
|
||||
|
@ -132,13 +107,18 @@ func (w *wrapper) LastIndex(ctx context.Context) (uint64, error) {
|
|||
|
||||
return w.up.LastIndex(ctx)
|
||||
}
|
||||
func (w *wrapper) LoadForUpdate(ctx context.Context, a event.Aggregate, fn func(context.Context, event.Aggregate) error) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.LoadForUpdate(ctx, a, fn)
|
||||
}
|
||||
|
||||
func DefaultProjection(e event.Event) []event.Event {
|
||||
eventType := event.TypeOf(e)
|
||||
m := e.EventMeta()
|
||||
streamID := m.StreamID
|
||||
streamPos := m.Position
|
||||
eventType := event.TypeOf(e)
|
||||
pkg, _, _ := strings.Cut(eventType, ".")
|
||||
|
||||
e1 := event.NewPtr(streamID, streamPos)
|
||||
event.SetStreamID("$all", e1)
|
||||
|
@ -147,6 +127,7 @@ func DefaultProjection(e event.Event) []event.Event {
|
|||
event.SetStreamID("$type-"+eventType, e2)
|
||||
|
||||
e3 := event.NewPtr(streamID, streamPos)
|
||||
pkg, _, _ := strings.Cut(eventType, ".")
|
||||
event.SetStreamID("$pkg-"+pkg, e3)
|
||||
|
||||
return []event.Event{e1, e2, e3}
|
29
pkg/es/driver/projecter/projector_test.go
Normal file
29
pkg/es/driver/projecter/projector_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package projecter_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/sour-is/ev/pkg/es/driver"
|
||||
)
|
||||
|
||||
type mockDriver struct {
|
||||
onOpen func()
|
||||
onEventLog func()
|
||||
}
|
||||
|
||||
// EventLog implements driver.Driver
|
||||
func (*mockDriver) EventLog(ctx context.Context, streamID string) (driver.EventLog, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Open implements driver.Driver
|
||||
func (*mockDriver) Open(ctx context.Context, dsn string) (driver.Driver, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*mockDriver)(nil)
|
||||
|
||||
func TestProjecter(t *testing.T) {
|
||||
|
||||
}
|
|
@ -1,20 +1,16 @@
|
|||
// package streamer provides a driver to allow awaiting for new events to be added to a stream.
|
||||
package streamer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"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"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
|
||||
"go.sour.is/ev"
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
|
@ -22,7 +18,7 @@ type state struct {
|
|||
}
|
||||
|
||||
type streamer struct {
|
||||
state *locker.Locked[*state]
|
||||
state *locker.Locked[state]
|
||||
up driver.Driver
|
||||
}
|
||||
|
||||
|
@ -33,9 +29,9 @@ func New(ctx context.Context) *streamer {
|
|||
return &streamer{state: locker.New(&state{subscribers: map[string][]*subscription{}})}
|
||||
}
|
||||
|
||||
var _ ev.Option = (*streamer)(nil)
|
||||
var _ es.Option = (*streamer)(nil)
|
||||
|
||||
func (s *streamer) Apply(e *ev.EventStore) {
|
||||
func (s *streamer) Apply(e *es.EventStore) {
|
||||
s.up = e.Driver
|
||||
e.Driver = s
|
||||
}
|
||||
|
@ -61,8 +57,6 @@ func (s *streamer) EventLog(ctx context.Context, streamID string) (driver.EventL
|
|||
|
||||
var _ driver.EventStream = (*streamer)(nil)
|
||||
|
||||
// Subscribe sets up a subscription that tracks the last HWM. when new events arrive they are notified via Send.
|
||||
// Call Close when done to clean up allocation.
|
||||
func (s *streamer) Subscribe(ctx context.Context, streamID string, start int64) (driver.Subscription, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
@ -71,51 +65,39 @@ func (s *streamer) Subscribe(ctx context.Context, streamID string, start int64)
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sub := &subscription{topic: streamID, events: events}
|
||||
sub.position = locker.New(&position{
|
||||
idx: start,
|
||||
size: ev.AllEvents,
|
||||
size: es.AllEvents,
|
||||
})
|
||||
sub.unsub = s.delete(streamID, sub)
|
||||
|
||||
return sub, s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||
return sub, s.state.Modify(ctx, func(state *state) error {
|
||||
state.subscribers[streamID] = append(state.subscribers[streamID], sub)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Send will notify all Recv-ing subscribers that new events are ready.
|
||||
func (s *streamer) Send(ctx context.Context, streamID string, events event.Events) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||
return s.state.Modify(ctx, func(state *state) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.AddEvent(fmt.Sprint("subscribers=", len(state.subscribers[streamID])))
|
||||
|
||||
for _, sub := range state.subscribers[streamID] {
|
||||
err := sub.position.Use(ctx, func(ctx context.Context, position *position) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
err := sub.position.Modify(ctx, func(position *position) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("streamID", streamID),
|
||||
attribute.Int64("actualPosition", int64(events.Last().EventMeta().ActualPosition)),
|
||||
attribute.String("actualStreamID", events.Last().EventMeta().ActualStreamID),
|
||||
attribute.Int64("position", int64(events.Last().EventMeta().Position)),
|
||||
)
|
||||
|
||||
position.size = int64(events.Last().EventMeta().ActualPosition - uint64(position.idx))
|
||||
position.size = int64(events.Last().EventMeta().Position - uint64(position.idx))
|
||||
|
||||
if position.wait != nil {
|
||||
close(position.wait)
|
||||
position.link = trace.LinkFromContext(ctx, attribute.String("src", "event"))
|
||||
position.wait = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -133,7 +115,7 @@ func (s *streamer) delete(streamID string, sub *subscription) func(context.Conte
|
|||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.state.Use(ctx, func(ctx context.Context, state *state) error {
|
||||
return s.state.Modify(ctx, func(state *state) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
|
@ -159,20 +141,11 @@ type wrapper struct {
|
|||
|
||||
var _ driver.EventLog = (*wrapper)(nil)
|
||||
|
||||
func (r *wrapper) Unwrap() driver.EventLog {
|
||||
return r.up
|
||||
}
|
||||
func (w *wrapper) Read(ctx context.Context, after int64, count int64) (event.Events, error) {
|
||||
func (w *wrapper) Read(ctx context.Context, pos int64, count int64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.Read(ctx, after, count)
|
||||
}
|
||||
func (w *wrapper) ReadN(ctx context.Context, index ...uint64) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.ReadN(ctx, index...)
|
||||
return w.up.Read(ctx, pos, count)
|
||||
}
|
||||
func (w *wrapper) Append(ctx context.Context, events event.Events, version uint64) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
|
@ -205,6 +178,12 @@ func (w *wrapper) LastIndex(ctx context.Context) (uint64, error) {
|
|||
|
||||
return w.up.LastIndex(ctx)
|
||||
}
|
||||
func (w *wrapper) LoadForUpdate(ctx context.Context, a event.Aggregate, fn func(context.Context, event.Aggregate) error) (uint64, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return w.up.LoadForUpdate(ctx, a, fn)
|
||||
}
|
||||
|
||||
type position struct {
|
||||
size int64
|
||||
|
@ -216,29 +195,23 @@ type position struct {
|
|||
type subscription struct {
|
||||
topic string
|
||||
|
||||
position *locker.Locked[*position]
|
||||
position *locker.Locked[position]
|
||||
|
||||
events driver.EventLog
|
||||
unsub func(context.Context) error
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// Recv checks for new events. Returned chan will block until an event is ready or cancelled.
|
||||
func (s *subscription) Recv(ctx context.Context) <-chan bool {
|
||||
func (s *subscription) Recv(ctx context.Context) bool {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
var wait func(context.Context) bool
|
||||
defer close(done)
|
||||
|
||||
err := s.position.Use(ctx, func(ctx context.Context, position *position) error {
|
||||
err := s.position.Modify(ctx, func(position *position) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if position.size == ev.AllEvents {
|
||||
if position.size == es.AllEvents {
|
||||
return nil
|
||||
}
|
||||
if position.size == 0 {
|
||||
|
@ -255,9 +228,10 @@ func (s *subscription) Recv(ctx context.Context) <-chan bool {
|
|||
span.End()
|
||||
position.link = trace.Link{}
|
||||
}
|
||||
return true
|
||||
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -267,28 +241,21 @@ func (s *subscription) Recv(ctx context.Context) <-chan bool {
|
|||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
done <- false
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if wait != nil {
|
||||
done <- wait(ctx)
|
||||
return
|
||||
return wait(ctx)
|
||||
}
|
||||
|
||||
done <- true
|
||||
}()
|
||||
|
||||
return done
|
||||
return true
|
||||
}
|
||||
|
||||
// Events returns a batch of events since last call. Updates position of HWM to end of stream. Call Recv to be notified when more are ready.
|
||||
func (s *subscription) Events(ctx context.Context) (event.Events, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var events event.Events
|
||||
return events, s.position.Use(ctx, func(ctx context.Context, position *position) error {
|
||||
return events, s.position.Modify(ctx, func(position *position) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
|
@ -299,30 +266,14 @@ func (s *subscription) Events(ctx context.Context) (event.Events, error) {
|
|||
}
|
||||
position.size = int64(len(events))
|
||||
if len(events) > 0 {
|
||||
position.idx = int64(events.First().EventMeta().ActualPosition - 1)
|
||||
position.idx = int64(events.First().EventMeta().Position - 1)
|
||||
}
|
||||
span.SetAttributes(
|
||||
attribute.Int64("position.idx", position.idx),
|
||||
attribute.Int64("position.size", position.size),
|
||||
attribute.Int64("meta.ActualPosition", int64(events.First().EventMeta().ActualPosition)),
|
||||
attribute.Int64("meta.Position", int64(events.First().EventMeta().Position)),
|
||||
)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Close unsubscribes from a stream
|
||||
func (s *subscription) Close(ctx context.Context) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if s == nil || s.unsub == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
s.once.Do(func() { err = s.unsub(ctx) })
|
||||
|
||||
return err
|
||||
return s.unsub(ctx)
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
// package es implements an event store and drivers for extending its functionality.
|
||||
package ev
|
||||
package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -7,14 +6,12 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
"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"
|
||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"go.sour.is/ev/driver"
|
||||
"go.sour.is/ev/event"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
|
@ -32,19 +29,19 @@ func Init(ctx context.Context) error {
|
|||
m := lg.Meter(ctx)
|
||||
|
||||
var err, errs error
|
||||
Mes_open, err = m.Int64Counter("es_open")
|
||||
Mes_open, err = m.SyncInt64().Counter("es_open")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
Mes_read, err = m.Int64Counter("es_read")
|
||||
Mes_read, err = m.SyncInt64().Counter("es_read")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
Mes_load, err = m.Int64Counter("es_load")
|
||||
Mes_load, err = m.SyncInt64().Counter("es_load")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
Mes_save, err = m.Int64Counter("es_save")
|
||||
Mes_save, err = m.SyncInt64().Counter("es_save")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
Mes_append, err = m.Int64Counter("es_append")
|
||||
Mes_append, err = m.SyncInt64().Counter("es_append")
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
return errs
|
||||
|
@ -54,7 +51,7 @@ func Register(ctx context.Context, name string, d driver.Driver) error {
|
|||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
return drivers.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
return drivers.Modify(ctx, func(c *config) error {
|
||||
if _, set := c.drivers[name]; set {
|
||||
return fmt.Errorf("driver %s already set", name)
|
||||
}
|
||||
|
@ -68,19 +65,17 @@ type EventStore struct {
|
|||
}
|
||||
|
||||
var (
|
||||
Mes_open metric.Int64Counter
|
||||
Mes_read metric.Int64Counter
|
||||
Mes_load metric.Int64Counter
|
||||
Mes_save metric.Int64Counter
|
||||
Mes_append metric.Int64Counter
|
||||
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)
|
||||
|
@ -100,19 +95,15 @@ func Open(ctx context.Context, dsn string, options ...Option) (*EventStore, erro
|
|||
conn, err := d.Open(ctx, dsn)
|
||||
|
||||
es := &EventStore{Driver: conn}
|
||||
es.Option(options...)
|
||||
for _, o := range options {
|
||||
o.Apply(es)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -126,11 +117,7 @@ func (es *EventStore) Save(ctx context.Context, agg event.Aggregate) (uint64, er
|
|||
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())),
|
||||
)
|
||||
Mes_save.Add(ctx, 1)
|
||||
|
||||
l, err := es.EventLog(ctx, agg.StreamID())
|
||||
if err != nil {
|
||||
|
@ -141,7 +128,6 @@ func (es *EventStore) Save(ctx context.Context, agg event.Aggregate) (uint64, er
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
Mes_save.Add(ctx, int64(count))
|
||||
|
||||
agg.Commit()
|
||||
return count, err
|
||||
|
@ -150,16 +136,9 @@ 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
|
||||
}
|
||||
Mes_load.Add(ctx, 1)
|
||||
|
||||
first, err := l.FirstIndex(ctx)
|
||||
l, err := es.Driver.EventLog(ctx, agg.StreamID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -168,72 +147,28 @@ func (es *EventStore) Load(ctx context.Context, agg event.Aggregate) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(events) == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
Mes_load.Add(ctx, events.Count())
|
||||
event.Start(agg, first-1)
|
||||
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) {
|
||||
func (es *EventStore) Read(ctx context.Context, streamID string, pos, 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),
|
||||
)
|
||||
Mes_read.Add(ctx, 1)
|
||||
|
||||
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
|
||||
return l.Read(ctx, pos, count)
|
||||
}
|
||||
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 {
|
||||
|
@ -245,10 +180,6 @@ func (es *EventStore) FirstIndex(ctx context.Context, streamID string) (uint64,
|
|||
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
|
||||
|
@ -259,10 +190,6 @@ func (es *EventStore) LastIndex(ctx context.Context, streamID string) (uint64, e
|
|||
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
|
||||
|
@ -283,27 +210,6 @@ func (es *EventStore) EventStream() driver.EventStream {
|
|||
}
|
||||
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 {
|
||||
|
@ -318,7 +224,6 @@ 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
|
||||
|
@ -336,11 +241,8 @@ func Create[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string
|
|||
|
||||
agg = new(A)
|
||||
agg.SetStreamID(streamID)
|
||||
span.SetAttributes(
|
||||
attribute.String("agg.streamID", streamID),
|
||||
)
|
||||
|
||||
if err = es.Load(ctx, agg); err != nil && !errors.Is(err, ErrNotFound) {
|
||||
if err = es.Load(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -369,9 +271,6 @@ func Update[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string
|
|||
|
||||
agg = new(A)
|
||||
agg.SetStreamID(streamID)
|
||||
span.SetAttributes(
|
||||
attribute.String("agg.streamID", streamID),
|
||||
)
|
||||
|
||||
if err = es.Load(ctx, agg); err != nil {
|
||||
return
|
||||
|
@ -399,11 +298,8 @@ func Upsert[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string
|
|||
|
||||
agg = new(A)
|
||||
agg.SetStreamID(streamID)
|
||||
span.SetAttributes(
|
||||
attribute.String("agg.streamID", streamID),
|
||||
)
|
||||
|
||||
if err = es.Load(ctx, agg); err != nil && !errors.Is(err, ErrNotFound) {
|
||||
if err = es.Load(ctx, agg); err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -1,34 +1,24 @@
|
|||
|
||||
type Meta @goModel(model: "go.sour.is/ev/pkg/event.Meta") {
|
||||
type Meta @goModel(model: "github.com/sour-is/ev/pkg/es/event.Meta") {
|
||||
eventID: String! @goField(name: "getEventID")
|
||||
streamID: String! @goField(name: "ActualStreamID")
|
||||
position: Int! @goField(name: "ActualPosition")
|
||||
streamID: String!
|
||||
created: Time!
|
||||
position: Int!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
events(streamID: String! paging: PageInput): Connection!
|
||||
}
|
||||
extend type Mutation {
|
||||
truncateStream(streamID: String! index:Int!): Boolean!
|
||||
}
|
||||
extend type Subscription {
|
||||
"""after == 0 start from begining, after == -1 start from end"""
|
||||
eventAdded(streamID: String! after: Int! = -1): Event
|
||||
}
|
||||
|
||||
type Event implements Edge @goModel(model: "go.sour.is/ev/pkg/gql.Event") {
|
||||
type Event implements Edge @goModel(model: "github.com/sour-is/ev/pkg/es.GQLEvent") {
|
||||
id: ID!
|
||||
|
||||
eventID: String!
|
||||
streamID: String!
|
||||
position: Int!
|
||||
|
||||
values: Map!
|
||||
bytes: String!
|
||||
type: String!
|
||||
created: Time!
|
||||
meta: Meta!
|
||||
|
||||
linked: Event
|
||||
}
|
|
@ -1,21 +1,20 @@
|
|||
package ev_test
|
||||
package es_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"go.sour.is/ev"
|
||||
memstore "go.sour.is/ev/driver/mem-store"
|
||||
"go.sour.is/ev/driver/projecter"
|
||||
resolvelinks "go.sour.is/ev/driver/resolve-links"
|
||||
"go.sour.is/ev/driver/streamer"
|
||||
"go.sour.is/ev/event"
|
||||
"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"
|
||||
"github.com/sour-is/ev/pkg/es/driver/streamer"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -27,9 +26,12 @@ type Thing struct {
|
|||
Name string
|
||||
Value string
|
||||
|
||||
event.IsAggregate
|
||||
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) {
|
||||
|
@ -46,7 +48,26 @@ func (a *Thing) OnSetValue(value string) error {
|
|||
type ValueSet struct {
|
||||
Value string
|
||||
|
||||
event.IsEvent
|
||||
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) {
|
||||
|
@ -57,23 +78,26 @@ func TestES(t *testing.T) {
|
|||
err := event.Register(ctx, &ValueSet{})
|
||||
is.NoErr(err)
|
||||
|
||||
es.Init(ctx)
|
||||
memstore.Init(ctx)
|
||||
|
||||
{
|
||||
store, err := ev.Open(ctx, "mem")
|
||||
is.True(errors.Is(err, ev.ErrNoDriver))
|
||||
store, err := es.Open(ctx, "mem")
|
||||
is.True(errors.Is(err, es.ErrNoDriver))
|
||||
is.True(store.EventStream() == nil)
|
||||
}
|
||||
|
||||
{
|
||||
_, err := ev.Open(ctx, "bogo:")
|
||||
is.True(errors.Is(err, ev.ErrNoDriver))
|
||||
_, err := es.Open(ctx, "bogo:")
|
||||
is.True(errors.Is(err, es.ErrNoDriver))
|
||||
}
|
||||
|
||||
store, err := ev.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
||||
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.True(errors.Is(err, ev.ErrNotFound))
|
||||
is.NoErr(err)
|
||||
|
||||
t.Log(thing.StreamVersion(), thing.Name, thing.Value)
|
||||
|
||||
|
@ -114,10 +138,13 @@ func TestESOperations(t *testing.T) {
|
|||
is := is.New(t)
|
||||
ctx := context.Background()
|
||||
|
||||
store, err := ev.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
||||
es.Init(ctx)
|
||||
memstore.Init(ctx)
|
||||
|
||||
store, err := es.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
|
||||
is.NoErr(err)
|
||||
|
||||
thing, err := ev.Create(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
||||
thing, err := es.Create(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
||||
return agg.OnSetValue("foo")
|
||||
})
|
||||
|
||||
|
@ -125,7 +152,7 @@ func TestESOperations(t *testing.T) {
|
|||
is.Equal(thing.Version(), uint64(1))
|
||||
is.Equal(thing.Value, "foo")
|
||||
|
||||
thing, err = ev.Update(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
||||
thing, err = es.Update(ctx, store, "thing-1", func(ctx context.Context, agg *Thing) error {
|
||||
return agg.OnSetValue("bar")
|
||||
})
|
||||
|
||||
|
@ -133,7 +160,7 @@ func TestESOperations(t *testing.T) {
|
|||
is.Equal(thing.Version(), uint64(2))
|
||||
is.Equal(thing.Value, "bar")
|
||||
|
||||
thing, err = ev.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
||||
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
||||
return agg.OnSetValue("bin")
|
||||
})
|
||||
|
||||
|
@ -141,7 +168,7 @@ func TestESOperations(t *testing.T) {
|
|||
is.Equal(thing.Version(), uint64(1))
|
||||
is.Equal(thing.Value, "bin")
|
||||
|
||||
thing, err = ev.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
||||
thing, err = es.Upsert(ctx, store, "thing-2", func(ctx context.Context, agg *Thing) error {
|
||||
return agg.OnSetValue("baz")
|
||||
})
|
||||
|
||||
|
@ -157,46 +184,6 @@ func TestUnwrap(t *testing.T) {
|
|||
err := errors.New("foo")
|
||||
werr := fmt.Errorf("wrap: %w", err)
|
||||
|
||||
is.Equal(ev.Unwrap(werr), err)
|
||||
is.Equal(ev.Unwrap("test"), "")
|
||||
}
|
||||
|
||||
func TestUnwrapProjector(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
defer stop()
|
||||
|
||||
es, err := ev.Open(
|
||||
ctx,
|
||||
"mem:",
|
||||
resolvelinks.New(),
|
||||
streamer.New(ctx),
|
||||
projecter.New(
|
||||
ctx,
|
||||
projecter.DefaultProjection,
|
||||
),
|
||||
)
|
||||
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(
|
||||
ev.Init(ctx),
|
||||
event.Init(ctx),
|
||||
memstore.Init(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
m.Run()
|
||||
is.Equal(es.Unwrap(werr), err)
|
||||
is.Equal(es.Unwrap("test"), "")
|
||||
}
|
|
@ -6,17 +6,11 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// Aggregate implements functionality for working with event store streams as an aggregate.
|
||||
// When creating a new Aggregate the struct should have an ApplyEvent method and embed the AggregateRoot.
|
||||
type Aggregate interface {
|
||||
// ApplyEvent applies the event to the aggrigate state
|
||||
ApplyEvent(...Event)
|
||||
|
||||
AggregateRoot
|
||||
}
|
||||
|
||||
func Start(a Aggregate, i uint64) {
|
||||
a.start(i)
|
||||
AggregateRootInterface
|
||||
}
|
||||
|
||||
// Raise adds new uncommitted events
|
||||
|
@ -49,7 +43,7 @@ func ShouldExist(a Aggregate) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type AggregateRoot interface {
|
||||
type AggregateRootInterface interface {
|
||||
// Events returns the aggregate events
|
||||
// pass true for only uncommitted events
|
||||
Events(bool) Events
|
||||
|
@ -62,35 +56,33 @@ type AggregateRoot interface {
|
|||
// Version returns the current aggrigate version. (committed + uncommitted)
|
||||
Version() uint64
|
||||
|
||||
start(uint64)
|
||||
raise(lis ...Event)
|
||||
append(lis ...Event)
|
||||
Commit()
|
||||
}
|
||||
|
||||
var _ AggregateRoot = &IsAggregate{}
|
||||
var _ AggregateRootInterface = &AggregateRoot{}
|
||||
|
||||
type IsAggregate struct {
|
||||
type AggregateRoot struct {
|
||||
events Events
|
||||
streamID string
|
||||
firstIndex uint64
|
||||
lastIndex uint64
|
||||
streamVersion uint64
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (a *IsAggregate) Commit() { a.lastIndex = uint64(len(a.events)) }
|
||||
func (a *IsAggregate) StreamID() string { return a.streamID }
|
||||
func (a *IsAggregate) SetStreamID(streamID string) { a.streamID = streamID }
|
||||
func (a *IsAggregate) StreamVersion() uint64 { return a.lastIndex }
|
||||
func (a *IsAggregate) Version() uint64 { return a.firstIndex + uint64(len(a.events)) }
|
||||
func (a *IsAggregate) Events(new bool) Events {
|
||||
func (a *AggregateRoot) Commit() { a.streamVersion = uint64(len(a.events)) }
|
||||
func (a *AggregateRoot) StreamID() string { return a.streamID }
|
||||
func (a *AggregateRoot) SetStreamID(streamID string) { a.streamID = streamID }
|
||||
func (a *AggregateRoot) StreamVersion() uint64 { return a.streamVersion }
|
||||
func (a *AggregateRoot) Version() uint64 { return uint64(len(a.events)) }
|
||||
func (a *AggregateRoot) Events(new bool) Events {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
|
||||
events := a.events
|
||||
if new {
|
||||
events = events[a.lastIndex-a.firstIndex:]
|
||||
events = events[a.streamVersion:]
|
||||
}
|
||||
|
||||
lis := make(Events, len(events))
|
||||
|
@ -99,13 +91,8 @@ func (a *IsAggregate) Events(new bool) Events {
|
|||
return lis
|
||||
}
|
||||
|
||||
func (a *IsAggregate) start(i uint64) {
|
||||
a.firstIndex = i
|
||||
a.lastIndex = i
|
||||
}
|
||||
|
||||
//lint:ignore U1000 is called by embeded interface
|
||||
func (a *IsAggregate) raise(lis ...Event) { //nolint
|
||||
func (a *AggregateRoot) raise(lis ...Event) { //nolint
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
|
@ -115,20 +102,20 @@ func (a *IsAggregate) raise(lis ...Event) { //nolint
|
|||
}
|
||||
|
||||
//lint:ignore U1000 is called by embeded interface
|
||||
func (a *IsAggregate) append(lis ...Event) {
|
||||
func (a *AggregateRoot) append(lis ...Event) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.posStartAt(lis...)
|
||||
|
||||
a.events = append(a.events, lis...)
|
||||
a.lastIndex += uint64(len(lis))
|
||||
a.streamVersion += uint64(len(lis))
|
||||
}
|
||||
|
||||
func (a *IsAggregate) posStartAt(lis ...Event) {
|
||||
func (a *AggregateRoot) posStartAt(lis ...Event) {
|
||||
for i, e := range lis {
|
||||
m := e.EventMeta()
|
||||
m.Position = a.lastIndex + uint64(i) + 1
|
||||
m.Position = a.streamVersion + uint64(i) + 1
|
||||
e.SetEventMeta(m)
|
||||
}
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
package event_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"go.sour.is/ev/event"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
type Agg struct {
|
||||
Value string
|
||||
|
||||
event.IsAggregate
|
||||
event.AggregateRoot
|
||||
}
|
||||
|
||||
var _ event.Aggregate = (*Agg)(nil)
|
||||
|
@ -32,11 +33,31 @@ func (a *Agg) ApplyEvent(lis ...event.Event) {
|
|||
type ValueApplied struct {
|
||||
Value string
|
||||
|
||||
event.IsEvent
|
||||
eventMeta event.Meta
|
||||
}
|
||||
|
||||
var _ event.Event = (*ValueApplied)(nil)
|
||||
|
||||
func (e *ValueApplied) EventMeta() event.Meta {
|
||||
if e == nil {
|
||||
return event.Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
|
||||
func (e *ValueApplied) SetEventMeta(m event.Meta) {
|
||||
if e != nil {
|
||||
e.eventMeta = m
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ValueApplied) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
func (e *ValueApplied) UnmarshalBinary(b []byte) error {
|
||||
return json.Unmarshal(b, e)
|
||||
}
|
||||
|
||||
func TestAggregate(t *testing.T) {
|
||||
agg := &Agg{}
|
||||
event.Append(agg, &ValueApplied{Value: "one"})
|
|
@ -1,9 +1,10 @@
|
|||
// package event implements functionality for working with an eventstore.
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
@ -27,10 +28,12 @@ func getULID() ulid.ULID {
|
|||
return ulid.MustNew(ulid.Now(), entropy)
|
||||
}
|
||||
|
||||
// Event implements functionality of an individual event used with the event store. It should implement the getter/setter for EventMeta and BinaryMarshaler/BinaryUnmarshaler.
|
||||
type Event interface {
|
||||
EventMeta() Meta
|
||||
SetEventMeta(Meta)
|
||||
|
||||
encoding.BinaryMarshaler
|
||||
encoding.BinaryUnmarshaler
|
||||
}
|
||||
|
||||
// Events is a list of events
|
||||
|
@ -40,9 +43,6 @@ func NewEvents(lis ...Event) Events {
|
|||
for i, e := range lis {
|
||||
meta := e.EventMeta()
|
||||
meta.Position = uint64(i)
|
||||
if meta.ActualPosition == 0 {
|
||||
meta.ActualPosition = uint64(i)
|
||||
}
|
||||
meta.EventID = getULID()
|
||||
e.SetEventMeta(meta)
|
||||
}
|
||||
|
@ -58,9 +58,6 @@ func (lis Events) StreamID() string {
|
|||
func (lis Events) SetStreamID(streamID string) {
|
||||
SetStreamID(streamID, lis...)
|
||||
}
|
||||
func (lis Events) Count() int64 {
|
||||
return int64(len(lis))
|
||||
}
|
||||
func (lis Events) First() Event {
|
||||
if len(lis) == 0 {
|
||||
return NilEvent
|
||||
|
@ -80,7 +77,7 @@ func (lis Events) Last() Event {
|
|||
return lis[len(lis)-1]
|
||||
}
|
||||
|
||||
func TypeOf(e any) string {
|
||||
func TypeOf(e Event) string {
|
||||
if ie, ok := e.(interface{ UnwrapEvent() Event }); ok {
|
||||
e = ie.UnwrapEvent()
|
||||
}
|
||||
|
@ -105,9 +102,6 @@ func SetStreamID(id string, lis ...Event) {
|
|||
for _, e := range lis {
|
||||
meta := e.EventMeta()
|
||||
meta.StreamID = id
|
||||
if meta.ActualStreamID == "" {
|
||||
meta.ActualStreamID = id
|
||||
}
|
||||
e.SetEventMeta(meta)
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +117,6 @@ func SetEventID(e Event, id ulid.ULID) {
|
|||
func SetPosition(e Event, i uint64) {
|
||||
meta := e.EventMeta()
|
||||
meta.Position = i
|
||||
meta.ActualPosition = i
|
||||
e.SetEventMeta(meta)
|
||||
}
|
||||
|
||||
|
@ -131,8 +124,6 @@ type Meta struct {
|
|||
EventID ulid.ULID
|
||||
StreamID string
|
||||
Position uint64
|
||||
ActualStreamID string
|
||||
ActualPosition uint64
|
||||
}
|
||||
|
||||
func (m Meta) Created() time.Time {
|
||||
|
@ -141,126 +132,79 @@ func (m Meta) Created() time.Time {
|
|||
func (m Meta) GetEventID() string { return m.EventID.String() }
|
||||
|
||||
func Init(ctx context.Context) error {
|
||||
if err := Register(ctx, NilEvent, &EventPtr{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := RegisterName(ctx, "event.eventPtr", &EventPtr{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return Register(ctx, NilEvent, &eventPtr{})
|
||||
}
|
||||
|
||||
type nilEvent struct {
|
||||
IsEvent
|
||||
type nilEvent struct{}
|
||||
|
||||
func (*nilEvent) EventMeta() Meta {
|
||||
return Meta{}
|
||||
}
|
||||
func (*nilEvent) SetEventMeta(eventMeta Meta) {}
|
||||
|
||||
var NilEvent = &nilEvent{}
|
||||
|
||||
func (e *nilEvent) MarshalBinary() ([]byte, error) {
|
||||
return nil, nil
|
||||
return json.Marshal(e)
|
||||
}
|
||||
func (e *nilEvent) UnmarshalBinary(b []byte) error {
|
||||
return nil
|
||||
return json.Unmarshal(b, e)
|
||||
}
|
||||
|
||||
type EventPtr struct {
|
||||
StreamID string `json:"stream_id"`
|
||||
Pos uint64 `json:"pos"`
|
||||
type eventPtr struct {
|
||||
streamID string
|
||||
pos uint64
|
||||
|
||||
IsEvent
|
||||
eventMeta Meta
|
||||
}
|
||||
|
||||
var _ Event = (*EventPtr)(nil)
|
||||
var _ Event = (*eventPtr)(nil)
|
||||
|
||||
func NewPtr(streamID string, pos uint64) *EventPtr {
|
||||
return &EventPtr{StreamID: streamID, Pos: pos}
|
||||
func NewPtr(streamID string, pos uint64) *eventPtr {
|
||||
return &eventPtr{streamID: streamID, pos: pos}
|
||||
}
|
||||
|
||||
// MarshalBinary implements Event
|
||||
func (e *EventPtr) MarshalBinary() (data []byte, err error) {
|
||||
return []byte(fmt.Sprintf("%s@%d", e.StreamID, e.Pos)), nil
|
||||
func (e *eventPtr) MarshalBinary() (data []byte, err error) {
|
||||
return []byte(fmt.Sprintf("%s@%d", e.streamID, e.pos)), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements Event
|
||||
func (e *EventPtr) UnmarshalBinary(data []byte) error {
|
||||
func (e *eventPtr) UnmarshalBinary(data []byte) error {
|
||||
s := string(data)
|
||||
idx := strings.LastIndex(s, "@")
|
||||
if idx == -1 {
|
||||
return fmt.Errorf("missing @ in: %s", s)
|
||||
}
|
||||
e.StreamID = s[:idx]
|
||||
e.streamID = s[:idx]
|
||||
var err error
|
||||
e.Pos, err = strconv.ParseUint(s[idx+1:], 10, 64)
|
||||
e.pos, err = strconv.ParseUint(s[idx+1:], 10, 64)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *EventPtr) Values() any {
|
||||
// EventMeta implements Event
|
||||
func (e *eventPtr) EventMeta() Meta {
|
||||
if e == nil {
|
||||
return Meta{}
|
||||
}
|
||||
return e.eventMeta
|
||||
}
|
||||
|
||||
// SetEventMeta implements Event
|
||||
func (e *eventPtr) SetEventMeta(m Meta) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.eventMeta = m
|
||||
}
|
||||
|
||||
func (e *eventPtr) Values() any {
|
||||
return struct {
|
||||
StreamID string `json:"stream_id"`
|
||||
Pos uint64 `json:"pos"`
|
||||
}{
|
||||
e.StreamID,
|
||||
e.Pos,
|
||||
e.streamID,
|
||||
e.pos,
|
||||
}
|
||||
}
|
||||
|
||||
type FeedTruncated struct {
|
||||
IsEvent
|
||||
}
|
||||
|
||||
func (e *FeedTruncated) Values() any {
|
||||
return struct {
|
||||
}{}
|
||||
}
|
||||
|
||||
type property[T any] struct {
|
||||
v T
|
||||
}
|
||||
|
||||
type IsEvent = property[Meta]
|
||||
|
||||
func (p *property[T]) EventMeta() T {
|
||||
if p == nil {
|
||||
var t T
|
||||
return t
|
||||
}
|
||||
return p.v
|
||||
}
|
||||
|
||||
func (p *property[T]) SetEventMeta(x T) {
|
||||
if p != nil {
|
||||
p.v = x
|
||||
}
|
||||
}
|
||||
|
||||
func AsEvent[T any](e T) Event {
|
||||
return &asEvent[T]{payload: e}
|
||||
}
|
||||
|
||||
type asEvent[T any] struct {
|
||||
payload T
|
||||
IsEvent
|
||||
}
|
||||
|
||||
func (e asEvent[T]) Payload() T {
|
||||
return e.payload
|
||||
}
|
||||
|
||||
type AGG interface{ ApplyEvent(...Event) }
|
||||
|
||||
func AsAggregate[T AGG](e T) Aggregate {
|
||||
return &asAggregate[T]{payload: e}
|
||||
}
|
||||
|
||||
type asAggregate[T AGG] struct {
|
||||
payload T
|
||||
IsAggregate
|
||||
}
|
||||
|
||||
func (e *asAggregate[T]) Payload() T {
|
||||
return e.payload
|
||||
}
|
||||
func (e *asAggregate[T]) ApplyEvent(lis ...Event) {
|
||||
e.payload.ApplyEvent(lis...)
|
||||
}
|
|
@ -8,15 +8,27 @@ import (
|
|||
|
||||
"github.com/matryer/is"
|
||||
|
||||
"go.sour.is/ev/event"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
type DummyEvent struct {
|
||||
Value string
|
||||
|
||||
event.IsEvent
|
||||
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 (e *DummyEvent) MarshalBinary() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
@ -55,28 +67,3 @@ func TestEventEncode(t *testing.T) {
|
|||
is.Equal(lis[i], chk[i])
|
||||
}
|
||||
}
|
||||
|
||||
type exampleAgg struct{ value string }
|
||||
|
||||
func (a *exampleAgg) ApplyEvent(lis ...event.Event) {
|
||||
for _, e := range lis {
|
||||
switch e := e.(type) {
|
||||
case interface{ Payload() exampleEvSetValue }:
|
||||
a.value = e.Payload().value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type exampleEvSetValue struct{ value string }
|
||||
|
||||
func TestApplyEventGeneric(t *testing.T) {
|
||||
payload := &exampleAgg{}
|
||||
var agg = event.AsAggregate(payload)
|
||||
|
||||
agg.ApplyEvent(event.NewEvents(
|
||||
event.AsEvent(exampleEvSetValue{"hello"}),
|
||||
)...)
|
||||
|
||||
is := is.New(t)
|
||||
is.Equal(payload.value, "hello")
|
||||
}
|
|
@ -3,15 +3,14 @@ package event
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.sour.is/pkg/lg"
|
||||
"go.sour.is/pkg/locker"
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/locker"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
|
@ -26,7 +25,7 @@ type UnknownEvent struct {
|
|||
eventType string
|
||||
values map[string]json.RawMessage
|
||||
|
||||
IsEvent
|
||||
eventMeta Meta
|
||||
}
|
||||
|
||||
var _ Event = (*UnknownEvent)(nil)
|
||||
|
@ -49,16 +48,16 @@ func NewUnknownEventFromValues(eventType string, meta Meta, values url.Values) *
|
|||
}
|
||||
}
|
||||
|
||||
e := &UnknownEvent{eventType: eventType, values: jsonValues}
|
||||
e.SetEventMeta(meta)
|
||||
return e
|
||||
return &UnknownEvent{eventType: eventType, eventMeta: meta, values: jsonValues}
|
||||
}
|
||||
func NewUnknownEventFromRaw(eventType string, meta Meta, values map[string]json.RawMessage) *UnknownEvent {
|
||||
e := &UnknownEvent{eventType: eventType, values: values}
|
||||
e.SetEventMeta(meta)
|
||||
return e
|
||||
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) UnmarshalBinary(b []byte) error {
|
||||
return json.Unmarshal(b, &u.values)
|
||||
}
|
||||
|
@ -68,7 +67,7 @@ func (u *UnknownEvent) MarshalBinary() ([]byte, error) {
|
|||
|
||||
// Register a type container for Unmarshalling values into. The type must implement Event and not be a nil value.
|
||||
func Register(ctx context.Context, lis ...Event) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
for _, e := range lis {
|
||||
|
@ -85,7 +84,7 @@ func Register(ctx context.Context, lis ...Event) error {
|
|||
return nil
|
||||
}
|
||||
func RegisterName(ctx context.Context, name string, e Event) error {
|
||||
ctx, span := lg.Span(ctx)
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
if e == nil {
|
||||
|
@ -107,7 +106,7 @@ func RegisterName(ctx context.Context, name string, e Event) error {
|
|||
|
||||
span.AddEvent("register: " + name)
|
||||
|
||||
if err := eventTypes.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
if err := eventTypes.Modify(ctx, func(c *config) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
|
@ -120,12 +119,12 @@ func RegisterName(ctx context.Context, name string, e Event) error {
|
|||
return nil
|
||||
}
|
||||
func GetContainer(ctx context.Context, s string) Event {
|
||||
ctx, span := lg.Span(ctx)
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
var e Event
|
||||
|
||||
eventTypes.Use(ctx, func(ctx context.Context, c *config) error {
|
||||
eventTypes.Modify(ctx, func(c *config) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
|
@ -152,8 +151,7 @@ func GetContainer(ctx context.Context, s string) Event {
|
|||
return e
|
||||
}
|
||||
|
||||
func MarshalBinary(e Event) ([]byte, error) {
|
||||
var err error
|
||||
func MarshalBinary(e Event) (txt []byte, err error) {
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
m := e.EventMeta()
|
||||
|
@ -169,28 +167,16 @@ func MarshalBinary(e Event) ([]byte, error) {
|
|||
return nil, err
|
||||
}
|
||||
b.WriteRune('\t')
|
||||
switch e := e.(type) {
|
||||
case encoding.BinaryMarshaler:
|
||||
var txt []byte
|
||||
if txt, err = e.MarshalBinary(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = b.Write(txt)
|
||||
case encoding.TextMarshaler:
|
||||
var txt []byte
|
||||
if txt, err = e.MarshalText(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = b.Write(txt)
|
||||
default:
|
||||
err = json.NewEncoder(b).Encode(e)
|
||||
}
|
||||
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
func UnmarshalBinary(ctx context.Context, txt []byte, pos uint64) (e Event, err error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
sp := bytes.SplitN(txt, []byte{'\t'}, 4)
|
||||
|
@ -208,29 +194,16 @@ func UnmarshalBinary(ctx context.Context, txt []byte, pos uint64) (e Event, err
|
|||
|
||||
m.StreamID = string(sp[1])
|
||||
m.Position = pos
|
||||
m.ActualStreamID = string(sp[1])
|
||||
m.ActualPosition = pos
|
||||
|
||||
eventType := string(sp[2])
|
||||
e = GetContainer(ctx, eventType)
|
||||
span.AddEvent(fmt.Sprintf("%s == %T", eventType, e))
|
||||
switch e := e.(type) {
|
||||
case encoding.BinaryUnmarshaler:
|
||||
|
||||
if err = e.UnmarshalBinary(sp[3]); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
case encoding.TextUnmarshaler:
|
||||
if err = e.UnmarshalText(sp[3]); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
if err = json.Unmarshal(sp[3], e); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
e.SetEventMeta(m)
|
||||
|
||||
return e, nil
|
||||
|
@ -288,30 +261,11 @@ func Values(e Event) map[string]any {
|
|||
continue
|
||||
}
|
||||
|
||||
omitempty := false
|
||||
field := v.FieldByIndex(idx.Index)
|
||||
|
||||
name := idx.Name
|
||||
if n, ok := idx.Tag.Lookup("json"); ok {
|
||||
var (
|
||||
opt string
|
||||
found bool
|
||||
)
|
||||
|
||||
name, opt, found = strings.Cut(n, ",")
|
||||
if name == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
if found {
|
||||
if strings.Contains(opt, "omitempty") {
|
||||
omitempty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if omitempty && field.IsZero() {
|
||||
continue
|
||||
name = n
|
||||
}
|
||||
|
||||
m[name] = field.Interface()
|
132
pkg/es/graph.go
Normal file
132
pkg/es/graph.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
"github.com/sour-is/ev/pkg/gql"
|
||||
)
|
||||
|
||||
type EventResolver interface {
|
||||
Events(ctx context.Context, streamID string, paging *gql.PageInput) (*gql.Connection, error)
|
||||
EventAdded(ctx context.Context, streamID string, after int64) (<-chan *GQLEvent, error)
|
||||
}
|
||||
|
||||
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 {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edges := make([]gql.Edge, 0, len(lis))
|
||||
for i := range lis {
|
||||
span.AddEvent(fmt.Sprint("event ", i, " of ", len(lis)))
|
||||
edges = append(edges, &GQLEvent{lis[i]})
|
||||
}
|
||||
|
||||
var first, last uint64
|
||||
if first, err = es.FirstIndex(ctx, streamID); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
if last, err = es.LastIndex(ctx, streamID); err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gql.Connection{
|
||||
Paging: &gql.PageInfo{
|
||||
Next: lis.Last().EventMeta().Position < last,
|
||||
Prev: lis.First().EventMeta().Position > first,
|
||||
Begin: lis.First().EventMeta().Position,
|
||||
End: lis.Last().EventMeta().Position,
|
||||
},
|
||||
Edges: edges,
|
||||
}, nil
|
||||
}
|
||||
func (e *EventStore) EventAdded(ctx context.Context, streamID string, after int64) (<-chan *GQLEvent, error) {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
es := e.EventStream()
|
||||
if es == nil {
|
||||
return nil, fmt.Errorf("EventStore does not implement streaming")
|
||||
}
|
||||
|
||||
sub, err := es.Subscribe(ctx, streamID, after)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan *GQLEvent)
|
||||
|
||||
go func() {
|
||||
ctx, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
{
|
||||
ctx, span := lg.Fork(ctx)
|
||||
defer func() {
|
||||
defer span.End()
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
err := sub.Close(ctx)
|
||||
span.RecordError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
for sub.Recv(ctx) {
|
||||
events, err := sub.Events(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
break
|
||||
}
|
||||
span.AddEvent(fmt.Sprintf("received %d events", len(events)))
|
||||
|
||||
for i := range events {
|
||||
select {
|
||||
case ch <- &GQLEvent{events[i]}:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
func (*EventStore) RegisterHTTP(*http.ServeMux) {}
|
||||
|
||||
type GQLEvent struct {
|
||||
e event.Event
|
||||
}
|
||||
|
||||
func (e *GQLEvent) ID() string {
|
||||
return fmt.Sprint(e.e.EventMeta().StreamID, "@", e.e.EventMeta().Position)
|
||||
}
|
||||
func (e *GQLEvent) EventID() string {
|
||||
return e.e.EventMeta().GetEventID()
|
||||
}
|
||||
func (e *GQLEvent) Values() map[string]interface{} {
|
||||
return event.Values(e.e)
|
||||
}
|
||||
func (e *GQLEvent) Bytes() (string, error) {
|
||||
b, err := e.e.MarshalBinary()
|
||||
return string(b), err
|
||||
}
|
||||
func (e *GQLEvent) Meta() *event.Meta {
|
||||
meta := e.e.EventMeta()
|
||||
return &meta
|
||||
}
|
||||
func (e *GQLEvent) IsEdge() {}
|
36
pkg/gql/common.graphqls
Normal file
36
pkg/gql/common.graphqls
Normal file
|
@ -0,0 +1,36 @@
|
|||
scalar Time
|
||||
scalar Map
|
||||
|
||||
type Connection @goModel(model: "github.com/sour-is/ev/pkg/gql.Connection") {
|
||||
paging: PageInfo!
|
||||
edges: [Edge!]!
|
||||
}
|
||||
input PageInput @goModel(model: "github.com/sour-is/ev/pkg/gql.PageInput") {
|
||||
idx: Int = 0
|
||||
count: Int = 30
|
||||
}
|
||||
type PageInfo @goModel(model: "github.com/sour-is/ev/pkg/gql.PageInfo") {
|
||||
next: Boolean!
|
||||
prev: Boolean!
|
||||
|
||||
begin: Int!
|
||||
end: Int!
|
||||
}
|
||||
interface Edge @goModel(model: "github.com/sour-is/ev/pkg/gql.Edge"){
|
||||
id: ID!
|
||||
}
|
||||
|
||||
directive @goModel(
|
||||
model: String
|
||||
models: [String!]
|
||||
) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
|
||||
|
||||
directive @goField(
|
||||
forceResolver: Boolean
|
||||
name: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
||||
|
||||
directive @goTag(
|
||||
key: String!
|
||||
value: String
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
56
pkg/gql/connection.go
Normal file
56
pkg/gql/connection.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package gql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sour-is/ev/pkg/es/event"
|
||||
)
|
||||
|
||||
type Edge interface {
|
||||
IsEdge()
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
Paging *PageInfo `json:"paging"`
|
||||
Edges []Edge `json:"edges"`
|
||||
}
|
||||
|
||||
type PostEvent struct {
|
||||
ID string `json:"id"`
|
||||
Payload string `json:"payload"`
|
||||
Tags []string `json:"tags"`
|
||||
Meta *event.Meta `json:"meta"`
|
||||
}
|
||||
|
||||
func (PostEvent) IsEdge() {}
|
||||
|
||||
func (e *PostEvent) PayloadJSON(ctx context.Context) (m map[string]interface{}, err error) {
|
||||
err = json.Unmarshal([]byte(e.Payload), &m)
|
||||
return
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
Next bool `json:"next"`
|
||||
Prev bool `json:"prev"`
|
||||
Begin uint64 `json:"begin"`
|
||||
End uint64 `json:"end"`
|
||||
}
|
||||
|
||||
type PageInput struct {
|
||||
Idx *int64 `json:"idx"`
|
||||
Count *int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (p *PageInput) GetIdx(v int64) int64 {
|
||||
if p == nil || p.Idx == nil {
|
||||
return v
|
||||
}
|
||||
return *p.Idx
|
||||
}
|
||||
func (p *PageInput) GetCount(v int64) int64 {
|
||||
if p == nil || p.Count == nil {
|
||||
return v
|
||||
}
|
||||
return *p.Count
|
||||
}
|
14
pkg/gql/context.go
Normal file
14
pkg/gql/context.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package gql
|
||||
|
||||
import "context"
|
||||
|
||||
func ToContext[K comparable, V any](ctx context.Context, key K, value V) context.Context {
|
||||
return context.WithValue(ctx, key, value)
|
||||
}
|
||||
func FromContext[K comparable, V any](ctx context.Context, key K) V {
|
||||
var empty V
|
||||
if v, ok := ctx.Value(key).(V); ok {
|
||||
return v
|
||||
}
|
||||
return empty
|
||||
}
|
51
pkg/locker/locker.go
Normal file
51
pkg/locker/locker.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package locker
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sour-is/ev/internal/lg"
|
||||
)
|
||||
|
||||
type Locked[T any] struct {
|
||||
state chan *T
|
||||
}
|
||||
|
||||
// New creates a new locker for the given value.
|
||||
func New[T any](initial *T) *Locked[T] {
|
||||
s := &Locked[T]{}
|
||||
s.state = make(chan *T, 1)
|
||||
s.state <- initial
|
||||
return s
|
||||
}
|
||||
|
||||
// Modify will call the function with the locked value
|
||||
func (s *Locked[T]) Modify(ctx context.Context, fn func(*T) error) error {
|
||||
_, span := lg.Span(ctx)
|
||||
defer span.End()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// Copy will return a shallow copy of the locked object.
|
||||
func (s *Locked[T]) Copy(ctx context.Context) (T, error) {
|
||||
var t T
|
||||
|
||||
err := s.Modify(ctx, func(c *T) error {
|
||||
if c != nil {
|
||||
t = *c
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return t, err
|
||||
}
|
62
pkg/locker/locker_test.go
Normal file
62
pkg/locker/locker_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package locker_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
|
||||
"github.com/sour-is/ev/pkg/locker"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Value string
|
||||
Counter int
|
||||
}
|
||||
|
||||
func TestLocker(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
value := locker.New(&config{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err := value.Modify(ctx, func(c *config) error {
|
||||
c.Value = "one"
|
||||
c.Counter++
|
||||
return nil
|
||||
})
|
||||
is.NoErr(err)
|
||||
|
||||
c, err := value.Copy(context.Background())
|
||||
|
||||
is.NoErr(err)
|
||||
is.Equal(c.Value, "one")
|
||||
is.Equal(c.Counter, 1)
|
||||
|
||||
wait := make(chan struct{})
|
||||
|
||||
go value.Modify(ctx, func(c *config) error {
|
||||
c.Value = "two"
|
||||
c.Counter++
|
||||
close(wait)
|
||||
return nil
|
||||
})
|
||||
|
||||
<-wait
|
||||
cancel()
|
||||
|
||||
err = value.Modify(ctx, func(c *config) error {
|
||||
c.Value = "three"
|
||||
c.Counter++
|
||||
return nil
|
||||
})
|
||||
is.True(err != nil)
|
||||
|
||||
c, err = value.Copy(context.Background())
|
||||
|
||||
is.NoErr(err)
|
||||
is.Equal(c.Value, "two")
|
||||
is.Equal(c.Counter, 2)
|
||||
}
|
67
pkg/math/math.go
Normal file
67
pkg/math/math.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
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 T, candidates ...T) T {
|
||||
for _, j := range candidates {
|
||||
if i < j {
|
||||
i = j
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
func Min[T ordered](i T, candidates ...T) T {
|
||||
for _, j := range candidates {
|
||||
if i > j {
|
||||
i = j
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func PagerBox(first, last uint64, pos, count int64) (uint64, int64) {
|
||||
var start uint64
|
||||
|
||||
if pos >= 0 {
|
||||
start = first + uint64(pos)
|
||||
} else {
|
||||
start = uint64(int64(last) + pos + 1)
|
||||
}
|
||||
|
||||
switch {
|
||||
case count > 0:
|
||||
count = Min(count, int64(last-start)+1)
|
||||
|
||||
case pos >= 0 && count < 0:
|
||||
count = Max(count, int64(first-start))
|
||||
|
||||
case pos < 0 && count < 0:
|
||||
count = Max(count, int64(first-start)-1)
|
||||
}
|
||||
|
||||
if count == 0 || (start < first && count <= 0) || (start > last && count >= 0) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
return start, count
|
||||
}
|
92
pkg/math/math_test.go
Normal file
92
pkg/math/math_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package math_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"github.com/sour-is/ev/pkg/es"
|
||||
"github.com/sour-is/ev/pkg/math"
|
||||
)
|
||||
|
||||
func TestMath(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
is.Equal(5, math.Abs(-5))
|
||||
is.Equal(math.Abs(5), math.Abs(-5))
|
||||
|
||||
is.Equal(10, math.Max(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
|
||||
is.Equal(1, math.Min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
|
||||
|
||||
is.Equal(1, math.Min(89, 71, 54, 48, 49, 1, 72, 88, 25, 69))
|
||||
is.Equal(89, math.Max(89, 71, 54, 48, 49, 1, 72, 88, 25, 69))
|
||||
|
||||
is.Equal(0.9348207729, math.Max(
|
||||
0.3943310720,
|
||||
0.1090868377,
|
||||
0.9348207729,
|
||||
0.3525527584,
|
||||
0.4359833682,
|
||||
0.7958538081,
|
||||
0.1439352569,
|
||||
0.1547311967,
|
||||
0.6403818871,
|
||||
0.8618832818,
|
||||
))
|
||||
|
||||
is.Equal(0.1090868377, math.Min(
|
||||
0.3943310720,
|
||||
0.1090868377,
|
||||
0.9348207729,
|
||||
0.3525527584,
|
||||
0.4359833682,
|
||||
0.7958538081,
|
||||
0.1439352569,
|
||||
0.1547311967,
|
||||
0.6403818871,
|
||||
0.8618832818,
|
||||
))
|
||||
|
||||
}
|
||||
|
||||
func TestPagerBox(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
tests := []struct {
|
||||
first uint64
|
||||
last uint64
|
||||
pos int64
|
||||
n int64
|
||||
|
||||
start uint64
|
||||
count int64
|
||||
}{
|
||||
{1, 10, 0, 10, 1, 10},
|
||||
{1, 10, 0, 11, 1, 10},
|
||||
{1, 5, 0, 10, 1, 5},
|
||||
{1, 10, 4, 10, 5, 6},
|
||||
{1, 10, 5, 10, 6, 5},
|
||||
{1, 10, 0, -10, 0, 0},
|
||||
{1, 10, 1, -1, 2, -1},
|
||||
{1, 10, 1, -10, 2, -1},
|
||||
{1, 10, -1, 1, 10, 1},
|
||||
{1, 10, -2, 10, 9, 2},
|
||||
{1, 10, -1, -1, 10, -1},
|
||||
{1, 10, -2, -10, 9, -9},
|
||||
{1, 10, 0, -10, 0, 0},
|
||||
{1, 10, 10, 10, 0, 0},
|
||||
{1, 10, 0, es.AllEvents, 1, 10},
|
||||
{1, 10, -1, -es.AllEvents, 10, -10},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
start, count := math.PagerBox(tt.first, tt.last, tt.pos, tt.n)
|
||||
if count > 0 {
|
||||
t.Log(tt, "|", start, count, int64(start)+count-1)
|
||||
} else {
|
||||
t.Log(tt, "|", start, count, int64(start)+count+1)
|
||||
}
|
||||
|
||||
is.Equal(start, tt.start)
|
||||
is.Equal(count, tt.count)
|
||||
}
|
||||
}
|
35
pkg/set/set.go
Normal file
35
pkg/set/set.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package set
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Set[T comparable] map[T]struct{}
|
||||
|
||||
func New[T comparable](items ...T) Set[T] {
|
||||
s := make(map[T]struct{}, len(items))
|
||||
for i := range items {
|
||||
s[items[i]] = struct{}{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
func (s Set[T]) Has(v T) bool {
|
||||
_, ok := (s)[v]
|
||||
return ok
|
||||
}
|
||||
func (s Set[T]) String() string {
|
||||
if s == nil {
|
||||
return "set(<nil>)"
|
||||
}
|
||||
lis := make([]string, 0, len(s))
|
||||
for k := range s {
|
||||
lis = append(lis, fmt.Sprint(k))
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("set(")
|
||||
b.WriteString(strings.Join(lis, ","))
|
||||
b.WriteString(")")
|
||||
return b.String()
|
||||
}
|
25
pkg/set/set_test.go
Normal file
25
pkg/set/set_test.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package set_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"github.com/sour-is/ev/pkg/set"
|
||||
)
|
||||
|
||||
func TestStringSet(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
s := set.New(strings.Fields("one two three")...)
|
||||
|
||||
is.True(s.Has("one"))
|
||||
is.True(s.Has("two"))
|
||||
is.True(s.Has("three"))
|
||||
is.True(!s.Has("four"))
|
||||
|
||||
is.Equal(set.New("one").String(), "set(one)")
|
||||
|
||||
var n set.Set[string]
|
||||
is.Equal(n.String(), "set(<nil>)")
|
||||
}
|
Loading…
Reference in New Issue
Block a user