diff --git a/Makefile b/Makefile index fd59183..05a14c3 100755 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ pre-commite: setup lint build tests ci: install setup lint build tests run_local_server: - go run cmd/loop/main.go --config=config/config.dev.yaml + go run cmd/loop/main.go -race --config=config/config.dev.yaml run_local_cli: go run cmd/loopcli/main.go --server=127.0.0.1:9500 kv set "k1/k2/k3" "{\"data\":\"aaa\"}" @@ -51,9 +51,9 @@ run_local_cli: go run cmd/loopcli/main.go --server=127.0.0.1:9500 kv list "k1/bbb/" go run cmd/loopcli/main.go --server=127.0.0.1:9500 kv list "users/" -run_local_cli_watch: +run_local_template: go run cmd/loopcli/main.go --server=127.0.0.1:9500 kv set "users/demo" "Mike" - go run cmd/loopcli/main.go --server=127.0.0.1:9500 template \ + go run cmd/loopcli/main.go -race --server=127.0.0.1:9500 template \ test_data/template.tmpl:test_data/template.out run_local_cli_watch_data: go run cmd/loopcli/main.go --server=127.0.0.1:9500 kv set "users/demo" "Mike1" diff --git a/README.md b/README.md index 89ce334..9581d4c 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ -# loopy \ No newline at end of file +# Loopy + +Loopy is designed to store and manage keys and values that are used by various services and applications. It provides a reliable system for tracking changes and updating text templates in real time. + +## Install with go + +```shell +go install github.com/arwos/loopy/...@latest +``` + +## Server + +Run server: + +```shell +loppy --config=./config.yaml +``` + +## Console client + +### Work with template + +```shell +loopcli --server=127.0.0.1:9500 template \ + test_data/template.tmpl:test_data/template.out \ + test_data/template2.tmpl:test_data/template2.out +``` + +#### Template functions: + +* __key__ - returns the key value or an empty string +```json +{{key "key/name"}} +``` +* __key_or_default__ - returns the key value or default value +```json +{{key_or_default "key/name" "default value"}} +``` +* __tree__ - returns a list of keys and value by prefix +```json +{{range $index, $data := tree "key/"}} +index: {{$index}} key: {{$data.Key}} val: {{$data.Value}} +{{end}} +``` + +### Work with keys + +* Set key +```shell +loopcli --server=127.0.0.1:9500 kv set "key/name" "key_value" +``` +* Get key +```shell +loopcli --server=127.0.0.1:9500 kv get "key/name" +``` +* Delete key +```shell +loopcli --server=127.0.0.1:9500 kv del "key/name" +``` +* List key by prefix (with empty value) +```shell +loopcli --server=127.0.0.1:9500 kv list "key/" +``` +* Search key by prefix (without empty value) +```shell +loopcli --server=127.0.0.1:9500 kv search "key/" +``` \ No newline at end of file diff --git a/api/client.go b/api/client.go index 1e39a75..2efb874 100644 --- a/api/client.go +++ b/api/client.go @@ -12,13 +12,13 @@ import ( "net/http" "net/url" - "go.osspkg.com/goppy/sdk/webutil" + "go.osspkg.com/goppy/web" ) type ( _client struct { conf *Config - cli *webutil.ClientHttp + cli *web.ClientHttp } Client interface { Get(ctx context.Context, key string) (string, error) @@ -30,12 +30,12 @@ type ( ) func NewKV(c *Config) (Client, error) { - opts := make([]webutil.ClientHttpOption, 0, 2) + opts := make([]web.ClientHttpOption, 0, 2) if len(c.AuthToken) > 0 { - opts = append(opts, webutil.ClientHttpOptionHeaders(AuthTokenHeaderName, c.AuthToken)) + opts = append(opts, web.ClientHttpOptionHeaders(AuthTokenHeaderName, c.AuthToken)) } opts = append(opts, clientHttpOptionCodec()) - cli := webutil.NewClientHttp(opts...) + cli := web.NewClientHttp(opts...) if err := c.Validate(); err != nil { return nil, err @@ -59,8 +59,8 @@ func (v *_client) buildUri(path string) string { return uri.String() } -func clientHttpOptionCodec() webutil.ClientHttpOption { - return webutil.ClientHttpOptionCodec( +func clientHttpOptionCodec() web.ClientHttpOption { + return web.ClientHttpOptionCodec( func(in interface{}) (body []byte, contentType string, err error) { switch v := in.(type) { case []byte: diff --git a/api/client_keys.go b/api/client_keys.go index 56a9c47..0e897ba 100644 --- a/api/client_keys.go +++ b/api/client_keys.go @@ -21,14 +21,14 @@ func (v *_client) Get(ctx context.Context, key string) (string, error) { if len(data) == 0 { return "", errRequestEmpty } - return data[0].Value.String(), nil + return data[0].ValueStrOrNull(), nil } func (v *_client) Set(ctx context.Context, key, value string) error { data := make(EntitiesKV, 0, 1) data = append(data, EntityKV{ - Key: key, - Value: []byte(value), + Key: key, + Val: &value, }) return v.cli.Call(ctx, http.MethodPut, v.buildUri(PathApiV1KV), &data, nil) } diff --git a/api/consts.go b/api/consts.go index 029777f..c862ea5 100644 --- a/api/consts.go +++ b/api/consts.go @@ -6,8 +6,8 @@ package api import ( - "go.osspkg.com/goppy/sdk/errors" - "go.osspkg.com/goppy/sdk/netutil/websocket" + "go.osspkg.com/goppy/errors" + "go.osspkg.com/goppy/ws/event" ) const ( @@ -25,7 +25,7 @@ const ( ) const ( - EventKVGet websocket.EventID = iota + 1 + EventKVGet event.Id = iota + 1 EventKVSet EventKVDel EventKVWatch diff --git a/api/go.mod b/api/go.mod index af4b0f3..9efb434 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,14 +4,20 @@ go 1.20 require ( github.com/mailru/easyjson v0.7.7 - go.osspkg.com/goppy v0.14.1 + go.osspkg.com/goppy/errors v0.1.0 + go.osspkg.com/goppy/web v0.1.5 + go.osspkg.com/goppy/ws v0.1.6 + go.osspkg.com/goppy/xlog v0.1.4 ) require ( - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - go.osspkg.com/algorithms v1.3.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + go.osspkg.com/goppy/iosync v0.1.2 // indirect + go.osspkg.com/goppy/ioutil v0.1.0 // indirect + go.osspkg.com/goppy/plugins v0.1.1 // indirect + go.osspkg.com/goppy/xc v0.1.0 // indirect + go.osspkg.com/goppy/xnet v0.1.1 // indirect + go.osspkg.com/static v1.4.0 // indirect + golang.org/x/net v0.17.0 // indirect ) diff --git a/api/go.sum b/api/go.sum index b34def2..b3763f3 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,21 +1,33 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -go.osspkg.com/algorithms v1.3.0 h1:rrKO440aNTofYJwGnOVsW00w0VRtZtu+BQrgXMXw7j4= -go.osspkg.com/algorithms v1.3.0/go.mod h1:J2SXxHdqBK9ALGYJomA9XGvTOhIwMK0fvVw+KusYyoo= -go.osspkg.com/goppy v0.14.1 h1:bPYGep07AlvABuEcJIz6ySF7kMJYhticodycz6DNIOE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +go.osspkg.com/goppy/errors v0.1.0 h1:2q8gdRZMEEDk7y/sneblQxpHhsi3pqF1BbFFHS7+FnE= +go.osspkg.com/goppy/errors v0.1.0/go.mod h1:SvA8dgErO9e2i3TR8hgJV4uMAzs4TkL4FxXBSnw2UG4= +go.osspkg.com/goppy/iosync v0.1.2 h1:w0BxqBa7PAdxFM2P8pZn3ToHK6ilX5IG+1nwIXJEoGg= +go.osspkg.com/goppy/iosync v0.1.2/go.mod h1:huJpHhpIQ2DgzY3wVt72RUsJaje0uqYiUvMRovb1/Dg= +go.osspkg.com/goppy/ioutil v0.1.0 h1:Z9CF1nzrjbcHJV1EIcLqOPAotePScuCjmTSyU7BoLzk= +go.osspkg.com/goppy/ioutil v0.1.0/go.mod h1:WpZGj1HpuBlDDH5k8mn+2QwPssMP83jKj59U8qLsBoU= +go.osspkg.com/goppy/plugins v0.1.1 h1:ly/g8LyGQNhT9BLKLbhHejUuPho5atd4uJmitorQyvM= +go.osspkg.com/goppy/plugins v0.1.1/go.mod h1:oolaNTq9VCWBAApLUFCHvWZ/7tMUhzLaqQEIxmLviNQ= +go.osspkg.com/goppy/web v0.1.5 h1:Q9ySetPvN5o8Pj5uiUl5pXMNnxSUxqq1B7kI1UU6SMw= +go.osspkg.com/goppy/web v0.1.5/go.mod h1:e0XC5Kk+d+mFrOj2eViXsPooKc2SSc/9CWWXUJ9MWXY= +go.osspkg.com/goppy/ws v0.1.6 h1:6xuu9NZsB5YlrWidDIOy/4UMlzddXbD2gI8TczxFt7U= +go.osspkg.com/goppy/ws v0.1.6/go.mod h1:5jsI1DdoNUVchcylaHGCrw5+qFObf9lQ2qWAXYiB//c= +go.osspkg.com/goppy/xc v0.1.0 h1:e2231FumnLEf1OjqEtbRaUxz3FT9M8pZVKg0C0aTf7g= +go.osspkg.com/goppy/xc v0.1.0/go.mod h1:ocKrJbO+EADhuClTbOqzDfCqnUO9+ikEW0M7pqLl1Y4= +go.osspkg.com/goppy/xlog v0.1.4 h1:3+o71O3Jb8UgfSA6nfpfQHfhHLIploHhlQ4p+Yfj5So= +go.osspkg.com/goppy/xlog v0.1.4/go.mod h1:AtYBxgKaxQxFWmb/SbmLvYp7ysuE2i1YYSE3Pu29nNQ= +go.osspkg.com/goppy/xnet v0.1.1 h1:nysNyS5O7nHXIN/IjP9HGfa6Qh5BTTSYLULijk+Sv9M= +go.osspkg.com/goppy/xnet v0.1.1/go.mod h1:eB5pFfZTCrcaIOHzt4RlTgBVF5dRUV/u52qz/2hY3qk= +go.osspkg.com/goppy/xtest v0.1.2 h1:rbUzEnWZW9vkGa24owydA9icQcfOaROJWSym1l0mCtU= +go.osspkg.com/static v1.4.0 h1:2uy4/11c0QP+QLMucKQZbAU+e6lhVHKw5dWJPTk/DBg= +go.osspkg.com/static v1.4.0/go.mod h1:94YydVU3qUtb1J534486lpm+qg6CviQjqtxKlkpSppM= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/models.go b/api/models.go index a08afc2..b335852 100644 --- a/api/models.go +++ b/api/models.go @@ -5,54 +5,27 @@ package api -import ( - "bytes" - "fmt" -) - //go:generate easyjson //easyjson:json -type EntityKV struct { - Key string `json:"k"` - Value RawValue `json:"v,omitempty"` -} - -//easyjson:json -type EntitiesKV []EntityKV - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -var ( - esc = []byte("\\\"") - unesc = []byte("\"") -) - -type RawValue []byte - -func (m RawValue) MarshalJSON() ([]byte, error) { - if m == nil { - return []byte("null"), nil +type ( + EntitiesKV []EntityKV + EntityKV struct { + Key string `json:"k"` + Val *string `json:"v"` } - buf := bytes.NewBuffer(nil) - buf.WriteString("\"") - buf.Write(bytes.ReplaceAll(m, unesc, esc)) - buf.WriteString("\"") - return buf.Bytes(), nil -} +) -func (m *RawValue) UnmarshalJSON(data []byte) error { - if m == nil { - return fmt.Errorf("json.RawMessage: UnmarshalJSON on nil pointer") +func (v EntityKV) ValueStrOrNull() string { + if v.Val == nil { + return "null" } - out := bytes.ReplaceAll(data[1:len(data)-1], esc, unesc) - *m = append((*m)[0:0], out...) - return nil + return *v.Val } -func (m RawValue) String() string { - if m == nil { - return "null" +func (v EntityKV) Value() string { + if v.Val == nil { + return "" } - return string(m) + return *v.Val } diff --git a/api/models_easyjson.go b/api/models_easyjson.go index 2fea762..8da9fad 100644 --- a/api/models_easyjson.go +++ b/api/models_easyjson.go @@ -39,8 +39,14 @@ func easyjsonD2b7633eDecodeGoArwosOrgLoopyApi(in *jlexer.Lexer, out *EntityKV) { case "k": out.Key = string(in.String()) case "v": - if data := in.Raw(); in.Ok() { - in.AddError((out.Value).UnmarshalJSON(data)) + if in.IsNull() { + in.Skip() + out.Val = nil + } else { + if out.Val == nil { + out.Val = new(string) + } + *out.Val = string(in.String()) } default: in.SkipRecursive() @@ -61,10 +67,14 @@ func easyjsonD2b7633eEncodeGoArwosOrgLoopyApi(out *jwriter.Writer, in EntityKV) out.RawString(prefix[1:]) out.String(string(in.Key)) } - if len(in.Value) != 0 { + { const prefix string = ",\"v\":" out.RawString(prefix) - out.Raw((in.Value).MarshalJSON()) + if in.Val == nil { + out.RawString("null") + } else { + out.String(string(*in.Val)) + } } out.RawByte('}') } @@ -101,7 +111,7 @@ func easyjsonD2b7633eDecodeGoArwosOrgLoopyApi1(in *jlexer.Lexer, out *EntitiesKV in.Delim('[') if *out == nil { if !in.IsDelim(']') { - *out = make(EntitiesKV, 0, 1) + *out = make(EntitiesKV, 0, 2) } else { *out = EntitiesKV{} } diff --git a/api/watch.go b/api/watch.go index 8a8df49..9fe27f0 100644 --- a/api/watch.go +++ b/api/watch.go @@ -9,25 +9,27 @@ import ( "context" "net/url" - "go.osspkg.com/goppy/sdk/log" - "go.osspkg.com/goppy/sdk/netutil/websocket" + "go.osspkg.com/goppy/ws/client" + "go.osspkg.com/goppy/xlog" ) type ( _watch struct { conf *Config - log log.Logger - cli websocket.Client + log xlog.Logger + cli client.Client } Watch interface { Open() error + AfterOpened(call func()) Close() + AfterClosed(call func()) KeyHandler(call func(e EntitiesKV)) KeySubscribe(keys ...string) } ) -func NewWatch(ctx context.Context, c *Config, l log.Logger) (Watch, error) { +func NewWatch(ctx context.Context, c *Config, l xlog.Logger) (Watch, error) { if err := c.Validate(); err != nil { return nil, err } @@ -35,13 +37,13 @@ func NewWatch(ctx context.Context, c *Config, l log.Logger) (Watch, error) { conf: c, log: l, } - opts := make([]func(websocket.ClientOption), 0) + opts := make([]func(client.Option), 0) if len(c.AuthToken) > 0 { - opts = append(opts, func(co websocket.ClientOption) { + opts = append(opts, func(co client.Option) { co.Header(AuthTokenHeaderName, c.AuthToken) }) } - w.cli = websocket.NewClient(ctx, w.buildUri(PathApiV1Watch), l, opts...) + w.cli = client.New(ctx, w.buildUri(PathApiV1Watch), l, opts...) return w, nil } @@ -49,6 +51,18 @@ func (v *_watch) Open() error { return v.cli.DialAndListen() } +func (v *_watch) AfterOpened(call func()) { + v.cli.OnOpen(func(cid string) { + call() + }) +} + +func (v *_watch) AfterClosed(call func()) { + v.cli.OnClose(func(cid string) { + call() + }) +} + func (v *_watch) Close() { v.cli.Close() } @@ -66,7 +80,7 @@ func (v *_watch) buildUri(path string) string { } func (v *_watch) KeyHandler(call func(e EntitiesKV)) { - v.cli.SetHandler(func(w websocket.CRequest, r websocket.CResponse, m websocket.CMeta) { + v.cli.SetHandler(func(w client.Request, r client.Response, m client.Meta) { var entities EntitiesKV if err := r.Decode(&entities); err != nil { v.log.WithError("err", err).Errorf("decode event") diff --git a/cmd/loop/main.go b/cmd/loopy/main.go similarity index 78% rename from cmd/loop/main.go rename to cmd/loopy/main.go index 80cd6bd..7c9b6b3 100644 --- a/cmd/loop/main.go +++ b/cmd/loopy/main.go @@ -9,7 +9,9 @@ import ( "go.arwos.org/loopy/internal/pkg/db" "go.arwos.org/loopy/internal/server" "go.osspkg.com/goppy" - "go.osspkg.com/goppy/plugins/web" + "go.osspkg.com/goppy/web" + "go.osspkg.com/goppy/ws" + "go.osspkg.com/goppy/xdns" ) func main() { @@ -17,7 +19,8 @@ func main() { app.Plugins( web.WithHTTP(), web.WithHTTPDebug(), - web.WithWebsocketServer(), + ws.WithWebsocketServer(), + xdns.WithDNSServer(), ) app.Plugins( server.Plugin, diff --git a/cmd/loopcli/main.go b/cmd/loopycli/main.go similarity index 94% rename from cmd/loopcli/main.go rename to cmd/loopycli/main.go index f1c1d2b..d4e0f06 100644 --- a/cmd/loopcli/main.go +++ b/cmd/loopycli/main.go @@ -7,7 +7,7 @@ package main import ( "go.arwos.org/loopy/internal/cli" - "go.osspkg.com/goppy/sdk/console" + "go.osspkg.com/goppy/console" ) func main() { diff --git a/config/config.yaml b/config/config.yaml index 07d119b..cc15daa 100755 --- a/config/config.yaml +++ b/config/config.yaml @@ -9,5 +9,9 @@ http: debug: addr: 127.0.0.1:12000 +dns: + addr: 0.0.0.0:9553 + timeout: 5s + database: folder: /var/lib/loopy/database diff --git a/go.mod b/go.mod index 5136cc5..a9bf948 100644 --- a/go.mod +++ b/go.mod @@ -2,20 +2,40 @@ module go.arwos.org/loopy go 1.20 +replace go.arwos.org/loopy/api => ./api + require ( github.com/mailru/easyjson v0.7.7 - go.arwos.org/loopy/api v0.0.0-00010101000000-000000000000 + go.arwos.org/loopy/api v0.1.1 go.etcd.io/bbolt v1.3.8 - go.osspkg.com/goppy v0.14.1 + go.osspkg.com/goppy v0.15.9 + go.osspkg.com/goppy/console v0.1.0 + go.osspkg.com/goppy/errors v0.1.0 + go.osspkg.com/goppy/iofile v0.1.3 + go.osspkg.com/goppy/iosync v0.1.2 + go.osspkg.com/goppy/plugins v0.1.1 + go.osspkg.com/goppy/routine v0.1.2 + go.osspkg.com/goppy/syscall v0.1.1 + go.osspkg.com/goppy/web v0.1.5 + go.osspkg.com/goppy/ws v0.1.6 + go.osspkg.com/goppy/xc v0.1.0 + go.osspkg.com/goppy/xdns v0.1.0 + go.osspkg.com/goppy/xlog v0.1.4 + go.osspkg.com/goppy/xtest v0.1.2 ) require ( - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/miekg/dns v1.1.57 // indirect go.osspkg.com/algorithms v1.3.0 // indirect + go.osspkg.com/goppy/app v0.1.5 // indirect + go.osspkg.com/goppy/ioutil v0.1.0 // indirect + go.osspkg.com/goppy/xnet v0.1.1 // indirect go.osspkg.com/static v1.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/tools v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace go.arwos.org/loopy/api => ./api diff --git a/go.sum b/go.sum index 652f4ca..4541f2f 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -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/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= @@ -14,12 +16,51 @@ go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.osspkg.com/algorithms v1.3.0 h1:rrKO440aNTofYJwGnOVsW00w0VRtZtu+BQrgXMXw7j4= go.osspkg.com/algorithms v1.3.0/go.mod h1:J2SXxHdqBK9ALGYJomA9XGvTOhIwMK0fvVw+KusYyoo= -go.osspkg.com/goppy v0.14.1 h1:bPYGep07AlvABuEcJIz6ySF7kMJYhticodycz6DNIOE= -go.osspkg.com/goppy v0.14.1/go.mod h1:NAWYk3WylEMTTcEgFiFEQsL69T/ox614gpuzlWyxlzg= +go.osspkg.com/goppy v0.15.9 h1:kn2XahQ6LlB698WWf9+ajoQJMsSxW7TzlJlBKcVMhkc= +go.osspkg.com/goppy v0.15.9/go.mod h1:8cBbW6b5KlgS1x7fR2N5I66qPV7h8hC8Xwe32Yo3uxs= +go.osspkg.com/goppy/app v0.1.5 h1:BchRXuvCdvqPl8dyAf5C5Vf6D/M+BzGFZbrd/9rheHM= +go.osspkg.com/goppy/app v0.1.5/go.mod h1:Fvh6qNaw1VB6luICKGoEErE5efETRxZ3mU2diqFVXy4= +go.osspkg.com/goppy/console v0.1.0 h1:ksQzyPFJlp9EY48tyAU6fOzKLfKkGa0KIB+fPkhTnJE= +go.osspkg.com/goppy/console v0.1.0/go.mod h1:x4MxAqsjTygGXyu1QkDLTvRaeQVU3/0euCxSQjtXzic= +go.osspkg.com/goppy/errors v0.1.0 h1:2q8gdRZMEEDk7y/sneblQxpHhsi3pqF1BbFFHS7+FnE= +go.osspkg.com/goppy/errors v0.1.0/go.mod h1:SvA8dgErO9e2i3TR8hgJV4uMAzs4TkL4FxXBSnw2UG4= +go.osspkg.com/goppy/iofile v0.1.3 h1:JxKOeysFUCB9JDvqgwQTBrQz4mISdSE2ouRxX8RJrKA= +go.osspkg.com/goppy/iofile v0.1.3/go.mod h1:bJTlDCHoZ3rENuu7ZE4KzzJJNv1CVq1TRZmj9phhXC8= +go.osspkg.com/goppy/iosync v0.1.2 h1:w0BxqBa7PAdxFM2P8pZn3ToHK6ilX5IG+1nwIXJEoGg= +go.osspkg.com/goppy/iosync v0.1.2/go.mod h1:huJpHhpIQ2DgzY3wVt72RUsJaje0uqYiUvMRovb1/Dg= +go.osspkg.com/goppy/ioutil v0.1.0 h1:Z9CF1nzrjbcHJV1EIcLqOPAotePScuCjmTSyU7BoLzk= +go.osspkg.com/goppy/ioutil v0.1.0/go.mod h1:WpZGj1HpuBlDDH5k8mn+2QwPssMP83jKj59U8qLsBoU= +go.osspkg.com/goppy/plugins v0.1.1 h1:ly/g8LyGQNhT9BLKLbhHejUuPho5atd4uJmitorQyvM= +go.osspkg.com/goppy/plugins v0.1.1/go.mod h1:oolaNTq9VCWBAApLUFCHvWZ/7tMUhzLaqQEIxmLviNQ= +go.osspkg.com/goppy/routine v0.1.2 h1:B93Q8OrEDt2xlCUzwKvOXU04dL3y2xR9SDIg461DM54= +go.osspkg.com/goppy/routine v0.1.2/go.mod h1:QxwA2bVqJc0gV6kbG6gkq6QP7g0ykmFtX2EIWPF3dhA= +go.osspkg.com/goppy/syscall v0.1.1 h1:9xOAw9yucVk6ZdPvvF6WIiDyARJX4UsjxyWUubV8bXM= +go.osspkg.com/goppy/syscall v0.1.1/go.mod h1:8MsNFOYAzNzGI6FE+0hmqLINQ5cxVkhqHUyirzENG9A= +go.osspkg.com/goppy/web v0.1.5 h1:Q9ySetPvN5o8Pj5uiUl5pXMNnxSUxqq1B7kI1UU6SMw= +go.osspkg.com/goppy/web v0.1.5/go.mod h1:e0XC5Kk+d+mFrOj2eViXsPooKc2SSc/9CWWXUJ9MWXY= +go.osspkg.com/goppy/ws v0.1.6 h1:6xuu9NZsB5YlrWidDIOy/4UMlzddXbD2gI8TczxFt7U= +go.osspkg.com/goppy/ws v0.1.6/go.mod h1:5jsI1DdoNUVchcylaHGCrw5+qFObf9lQ2qWAXYiB//c= +go.osspkg.com/goppy/xc v0.1.0 h1:e2231FumnLEf1OjqEtbRaUxz3FT9M8pZVKg0C0aTf7g= +go.osspkg.com/goppy/xc v0.1.0/go.mod h1:ocKrJbO+EADhuClTbOqzDfCqnUO9+ikEW0M7pqLl1Y4= +go.osspkg.com/goppy/xdns v0.1.0 h1:Y6lsvaAAN68DCWLmyWLkuSiNt+vquiTD1jaou+3zEZY= +go.osspkg.com/goppy/xdns v0.1.0/go.mod h1:wUErRNhELemL5Wj2gbrZHE7+kSwPy3szV08Gg/NP7e4= +go.osspkg.com/goppy/xlog v0.1.4 h1:3+o71O3Jb8UgfSA6nfpfQHfhHLIploHhlQ4p+Yfj5So= +go.osspkg.com/goppy/xlog v0.1.4/go.mod h1:AtYBxgKaxQxFWmb/SbmLvYp7ysuE2i1YYSE3Pu29nNQ= +go.osspkg.com/goppy/xnet v0.1.1 h1:nysNyS5O7nHXIN/IjP9HGfa6Qh5BTTSYLULijk+Sv9M= +go.osspkg.com/goppy/xnet v0.1.1/go.mod h1:eB5pFfZTCrcaIOHzt4RlTgBVF5dRUV/u52qz/2hY3qk= +go.osspkg.com/goppy/xtest v0.1.2 h1:rbUzEnWZW9vkGa24owydA9icQcfOaROJWSym1l0mCtU= +go.osspkg.com/goppy/xtest v0.1.2/go.mod h1:Ljsd0YZGq/QuiKR+KhNKBU/c1ZQSfKP4ZNhytGy8ZXM= go.osspkg.com/static v1.4.0 h1:2uy4/11c0QP+QLMucKQZbAU+e6lhVHKw5dWJPTk/DBg= go.osspkg.com/static v1.4.0/go.mod h1:94YydVU3qUtb1J534486lpm+qg6CviQjqtxKlkpSppM= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go.work.sum b/go.work.sum index eb6f435..c24754b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -4,6 +4,7 @@ cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdi cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= @@ -18,6 +19,7 @@ github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= @@ -30,24 +32,30 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgc github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= +go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.osspkg.com/goppy/xtest v0.1.2/go.mod h1:Ljsd0YZGq/QuiKR+KhNKBU/c1ZQSfKP4ZNhytGy8ZXM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -63,4 +71,5 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/cli/kv.go b/internal/cli/kv.go index 2c9a549..c7af837 100644 --- a/internal/cli/kv.go +++ b/internal/cli/kv.go @@ -11,7 +11,7 @@ import ( "time" "go.arwos.org/loopy/api" - "go.osspkg.com/goppy/sdk/console" + "go.osspkg.com/goppy/console" ) func CommandKVCommon() console.CommandGetter { @@ -128,7 +128,7 @@ func CommandKVSearch() console.CommandGetter { console.FatalIfErr(err, "kv search data") for _, re := range res { console.Rawf(">> %s", re.Key) - console.Rawf(re.Value.String()) + console.Rawf("%+v", re.ValueStrOrNull()) } }) }) @@ -156,7 +156,7 @@ func CommandKVList() console.CommandGetter { console.FatalIfErr(err, "kv list data") for _, re := range res { console.Rawf(">> %s", re.Key) - console.Rawf(re.Value.String()) + console.Rawf("%+v", re.ValueStrOrNull()) } }) }) diff --git a/internal/cli/watch.go b/internal/cli/watch.go index cdf3d70..7973132 100644 --- a/internal/cli/watch.go +++ b/internal/cli/watch.go @@ -6,45 +6,61 @@ package cli import ( - "context" + "os" "strings" + "time" "go.arwos.org/loopy/api" "go.arwos.org/loopy/internal/pkg/tmpl" - "go.osspkg.com/goppy/sdk/console" - "go.osspkg.com/goppy/sdk/iosync" - "go.osspkg.com/goppy/sdk/log" - "go.osspkg.com/goppy/sdk/syscall" + "go.osspkg.com/goppy/console" + "go.osspkg.com/goppy/iosync" + "go.osspkg.com/goppy/syscall" + "go.osspkg.com/goppy/xc" + "go.osspkg.com/goppy/xlog" ) func CommandTemplate() console.CommandGetter { return console.NewCommand(func(setter console.CommandSetter) { - setter.Setup("template", "Update template with Loop KV") + setter.Setup("template", "Update template with Loop") setter.Flag(func(fs console.FlagsSetter) { fs.StringVar("server", "127.0.0.1:8080", "Set Loopy address") - fs.Bool("ssl", "Set use ssl for Loopy address") + fs.StringVar("pid", "/tmp/loopy_template.pid", "Set Loopy PID file") + fs.IntVar("uptime", 5, "Template update check interval") + fs.Bool("ssl", "Set to use SSL for Loopy address") }) - setter.ExecFunc(func(args []string, server string, ssl bool) { - logger := log.Default() + setter.ExecFunc(func(args []string, server, pidfile string, uptime int64, ssl bool) { + console.FatalIfErr(syscall.Pid(pidfile), "Write PID file %s", pidfile) + + logger := xlog.Default() + logger.SetOutput(os.Stdout) + logger.SetLevel(xlog.LevelDebug) + logger.SetFormatter(xlog.NewFormatString()) + wg := iosync.NewGroup() - ctx, cncl := context.WithCancel(context.TODO()) - cli, err := api.NewWatch(ctx, &api.Config{SSL: ssl, HostPort: server}, logger) + closeCtx := xc.New() + openCtx := xc.New() + + cli, err := api.NewWatch(closeCtx.Context(), &api.Config{SSL: ssl, HostPort: server}, logger) console.FatalIfErr(err, "Connect to server %s", server) + cli.AfterOpened(openCtx.Close) + cli.AfterClosed(closeCtx.Close) + t := tmpl.New(logger) cli.KeyHandler(func(e api.EntitiesKV) { for _, kv := range e { - t.SetKey(kv.Key, string(kv.Value)) + t.SetKey(kv.Key, kv.Val) } }) wg.Background(func() { - if err := cli.Open(); err != nil { - console.Errorf("Open connect to Loopy: %v", err.Error()) + if err0 := cli.Open(); err0 != nil { + console.Errorf("Open connect to Loopy: %v", err0.Error()) } - cncl() }) - t.KeysHandler(func(keys ...string) { - cli.KeySubscribe(keys...) + <-openCtx.Done() + t.KeysHandler(func(key string) { + cli.KeySubscribe(key) }) + for _, arg := range args { f := strings.Split(arg, ":") if len(f) != 2 { @@ -52,9 +68,9 @@ func CommandTemplate() console.CommandGetter { } console.FatalIfErr(t.Add(f[0], f[1]), "Add file path template") } - t.Watch(ctx) - go syscall.OnStop(cncl) - <-ctx.Done() + go syscall.OnStop(closeCtx.Close) + t.Watch(closeCtx, time.Duration(uptime)*time.Second) + cli.Close() wg.Wait() }) }) diff --git a/internal/pkg/cachekeys/cachekeys.go b/internal/pkg/cachekeys/cachekeys.go index 75a8ec5..6ad95f6 100644 --- a/internal/pkg/cachekeys/cachekeys.go +++ b/internal/pkg/cachekeys/cachekeys.go @@ -1,24 +1,62 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package cachekeys -import "go.osspkg.com/goppy/sdk/iosync" +import "go.osspkg.com/goppy/iosync" type ( Value struct { Key string - Value []byte + Value *string } Map struct { - data map[string][]byte - bus chan Value - mux iosync.Lock + data map[string]*string + bus chan Value + withBus bool + mux iosync.Lock + } + + MapGetter interface { + Get(key string) (value *string) + Has(key string) (ok bool) + Each(call func(key string, value *string)) + } + + MapSetter interface { + Set(key string, value *string) + Del(key string) + } + + Mapper interface { + MapGetter + MapSetter + } + + MapperWithBus interface { + Bus() <-chan Value + MapGetter + MapSetter } ) -func New() *Map { +func NewWithBus() MapperWithBus { + return &Map{ + data: make(map[string]*string, 100), + bus: make(chan Value, 1000), + withBus: true, + mux: iosync.NewLock(), + } +} + +func NewWithoutBus() Mapper { return &Map{ - data: make(map[string][]byte, 100), - bus: make(chan Value, 1000), - mux: iosync.NewLock(), + data: make(map[string]*string, 100), + bus: make(chan Value, 1000), + withBus: false, + mux: iosync.NewLock(), } } @@ -26,29 +64,30 @@ func (v *Map) Bus() <-chan Value { return v.bus } -func (v *Map) toBus(key string, value []byte) { +func (v *Map) toBus(key string, value *string) { + if !v.withBus { + return + } select { case v.bus <- Value{Key: key, Value: value}: default: } } -func (v *Map) Set(key string, value []byte) { +func (v *Map) Set(key string, value *string) { v.mux.Lock(func() { - tmp := make([]byte, 0, len(value)) - copy(tmp, value) - v.data[key] = tmp + v.data[key] = value v.toBus(key, value) }) } -func (v *Map) Get(key string) (out []byte) { +func (v *Map) Get(key string) (value *string) { v.mux.RLock(func() { - value, ok := v.data[key] + d, ok := v.data[key] if !ok { return } - out = append(out, value...) + value = d }) return } @@ -60,6 +99,15 @@ func (v *Map) Has(key string) (ok bool) { return } +func (v *Map) Each(call func(key string, value *string)) { + v.mux.RLock(func() { + for key, val := range v.data { + call(key, val) + } + }) + return +} + func (v *Map) Del(key string) { v.mux.Lock(func() { delete(v.data, key) diff --git a/internal/pkg/clientshub/clientshub.go b/internal/pkg/clientshub/clientshub.go index 233b324..533d852 100644 --- a/internal/pkg/clientshub/clientshub.go +++ b/internal/pkg/clientshub/clientshub.go @@ -1,7 +1,14 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package clientshub import ( - "go.osspkg.com/goppy/sdk/iosync" + "strings" + + "go.osspkg.com/goppy/iosync" ) type Hub struct { @@ -58,11 +65,11 @@ func (v *Hub) Del(cid string, keys []string) { func (v *Hub) GetClients(key string) (out []string) { v.mux.RLock(func() { - km, ok := v.keys[key] - if !ok { - return + for k, km := range v.keys { + if key == k || strings.HasPrefix(key, k) { + out = append(out, km.Get()...) + } } - out = km.Get() }) return } diff --git a/internal/pkg/clientshub/maps.go b/internal/pkg/clientshub/maps.go index 430eeb2..48302dd 100644 --- a/internal/pkg/clientshub/maps.go +++ b/internal/pkg/clientshub/maps.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package clientshub type Maps struct { diff --git a/internal/pkg/db/db.go b/internal/pkg/db/db.go index 368115c..79939c5 100644 --- a/internal/pkg/db/db.go +++ b/internal/pkg/db/db.go @@ -10,8 +10,8 @@ import ( "time" "go.etcd.io/bbolt" - "go.osspkg.com/goppy/sdk/app" - "go.osspkg.com/goppy/sdk/iofile" + "go.osspkg.com/goppy/iofile" + "go.osspkg.com/goppy/xc" ) const databaseName = "loop.db" @@ -28,7 +28,7 @@ func New(c *Config) *DB { } } -func (v *DB) Up(_ app.Context) error { +func (v *DB) Up(_ xc.Context) error { if !iofile.Exist(v.conf.Folder) { if err := os.MkdirAll(v.conf.Folder, 0744); err != nil { return err diff --git a/internal/pkg/tmpl/funcs.go b/internal/pkg/tmpl/funcs.go new file mode 100644 index 0000000..1aff3fe --- /dev/null +++ b/internal/pkg/tmpl/funcs.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + +package tmpl + +import ( + "strings" + "text/template" +) + +func TemplateFuncMap(a Action, s State) template.FuncMap { + return template.FuncMap{ + "key": KeyFunc(a, s), + "key_or_default": KeyOrDefaultFunc(a, s), + "tree": TreeFunc(a, s), + } +} + +func KeyFunc(a Action, s State) func(string) string { + return func(key string) string { + if !s.IsPrepared() { + a.Query(key) + return "" + } + if value := a.Keys().Get(key); value != nil { + return *value + } + return "" + } +} + +func KeyOrDefaultFunc(a Action, s State) func(string, string) string { + return func(key string, def string) string { + if !s.IsPrepared() { + a.Query(key) + return def + } + if value := a.Keys().Get(key); value != nil { + return *value + } + return def + } +} + +type TreeModel struct { + Key string + Value string +} + +func TreeFunc(a Action, s State) func(string) []TreeModel { + return func(prefix string) []TreeModel { + data := make([]TreeModel, 0, 10) + if !s.IsPrepared() { + a.Query(prefix) + return data + } + a.Keys().Each(func(key string, value *string) { + if strings.HasPrefix(key, prefix) && value != nil { + data = append(data, TreeModel{ + Key: key, + Value: *value, + }) + } + }) + return data + } +} diff --git a/internal/pkg/tmpl/item.go b/internal/pkg/tmpl/item.go index dd96205..9d8e7f9 100644 --- a/internal/pkg/tmpl/item.go +++ b/internal/pkg/tmpl/item.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package tmpl import ( @@ -5,31 +10,78 @@ import ( "fmt" "os" "text/template" + + "go.arwos.org/loopy/internal/pkg/cachekeys" + "go.osspkg.com/goppy/iosync" ) -type Item struct { - src string - dst string - tmpl *template.Template -} +type ( + Item struct { + src string + dst string + mt int64 + tmpl *template.Template + state iosync.Switch + } + + Action interface { + Query(key string) + Keys() cachekeys.MapGetter + } + + State interface { + IsPrepared() bool + } +) -func NewItem(in, out string, funcs template.FuncMap) (*Item, error) { +func NewItem(in, out string, a Action) (*Item, error) { + fi, err := os.Lstat(in) + if err != nil { + return nil, fmt.Errorf("get file info %s: %w", in, err) + } b, err := os.ReadFile(in) if err != nil { return nil, fmt.Errorf("read %s: %w", in, err) } - t, err := template.New("_").Funcs(funcs).Parse(string(b)) + obj := &Item{ + src: in, + dst: out, + mt: fi.ModTime().Unix(), + state: iosync.NewSwitch(), + } + obj.tmpl, err = template.New("_").Funcs(TemplateFuncMap(a, obj)).Parse(string(b)) if err != nil { return nil, fmt.Errorf("parse %s: %w", in, err) } - return &Item{ - src: in, - dst: out, - tmpl: t, - }, nil + return obj, nil +} + +func (v *Item) IsChanged() bool { + fi, err := os.Lstat(v.src) + if err != nil { + return false + } + return v.mt < fi.ModTime().Unix() +} + +func (v *Item) IsPrepared() bool { + return v.state.IsOn() +} + +func (v *Item) Prepare() error { + var buf bytes.Buffer + err := v.tmpl.Execute(&buf, nil) + if err != nil { + return fmt.Errorf("execute %s: %w", v.src, err) + } + v.state.On() + return nil } func (v *Item) Update() error { + if v.state.IsOff() { + return fmt.Errorf("template is not prepared: %s", v.src) + } var buf bytes.Buffer err := v.tmpl.Execute(&buf, nil) if err != nil { diff --git a/internal/pkg/tmpl/tmpl.go b/internal/pkg/tmpl/tmpl.go index 15fd929..49a4d5c 100644 --- a/internal/pkg/tmpl/tmpl.go +++ b/internal/pkg/tmpl/tmpl.go @@ -1,68 +1,77 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package tmpl import ( "context" - "text/template" + "strings" + "time" - "go.osspkg.com/goppy/sdk/iosync" - "go.osspkg.com/goppy/sdk/log" + "go.arwos.org/loopy/internal/pkg/cachekeys" + "go.osspkg.com/goppy/iosync" + "go.osspkg.com/goppy/routine" + "go.osspkg.com/goppy/xc" + "go.osspkg.com/goppy/xlog" ) type Tmpl struct { - keys map[string]string - tmpls []*Item - keyH func(keys ...string) - log log.Logger + keys cachekeys.Mapper + tmpls map[string]*Item + handler func(key string) trigger chan struct{} + log xlog.Logger mux iosync.Lock + wg iosync.Group } -func New(l log.Logger) *Tmpl { +func New(l xlog.Logger) *Tmpl { return &Tmpl{ - keys: make(map[string]string, 100), - tmpls: make([]*Item, 0, 2), - keyH: func(keys ...string) {}, + keys: cachekeys.NewWithoutBus(), + tmpls: make(map[string]*Item, 10), + handler: func(key string) {}, log: l, trigger: make(chan struct{}, 2), mux: iosync.NewLock(), + wg: iosync.NewGroup(), } } -func (v *Tmpl) KeysHandler(call func(keys ...string)) { - v.keyH = call +func (v *Tmpl) KeysHandler(call func(key string)) { + v.handler = call } -func (v *Tmpl) SetKey(key, value string) { +func (v *Tmpl) SetKey(key string, value *string) { v.mux.Lock(func() { - v.keys[key] = value + v.keys.Set(key, value) }) v.sendTrigger() } -func (v *Tmpl) funcs() template.FuncMap { - return template.FuncMap{ - "key": func(k string) (out string) { - v.mux.RLock(func() { - var ok bool - out, ok = v.keys[k] - if ok { - return - } - out = "" - v.keyH(k) - }) - return - }, - } +func (v *Tmpl) Query(key string) { + v.handler(key) } -func (v *Tmpl) Add(in, out string) error { - t, err := NewItem(in, out, v.funcs()) - if err != nil { - return err - } - v.tmpls = append(v.tmpls, t) - return nil +func (v *Tmpl) Keys() cachekeys.MapGetter { + return v.keys +} + +func (v *Tmpl) Add(in, out string) (err error) { + v.mux.Lock(func() { + var t *Item + if t, err = NewItem(in, out, v); err != nil { + return + } + if err = t.Prepare(); err != nil { + v.log.WithError("err", err).Errorf("prepare template") + return + } + v.tmpls[in+":"+out] = t + }) + + return } func (v *Tmpl) sendTrigger() { @@ -72,20 +81,46 @@ func (v *Tmpl) sendTrigger() { } } -func (v *Tmpl) Watch(ctx context.Context) { - go func() { +func (v *Tmpl) Watch(ctx xc.Context, interval time.Duration) { + v.wg.Background(func() { for { select { case <-ctx.Done(): return case <-v.trigger: - for _, tmpl := range v.tmpls { - if err := tmpl.Update(); err != nil { - v.log.WithError("err", err).Errorf("update template") + v.mux.RLock(func() { + for _, tmpl := range v.tmpls { + if err := tmpl.Update(); err != nil { + v.log.WithError("err", err).Errorf("update template") + } } + }) + } + } + }) + routine.Interval(ctx.Context(), interval, func(ctx context.Context) { + var ( + links []string + ) + v.mux.RLock(func() { + for link, tmpl := range v.tmpls { + if tmpl.IsChanged() { + v.log.WithField("path", link).Infof("reload template") + links = append(links, link) } } + }) + for _, link := range links { + f := strings.Split(link, ":") + if len(f) != 2 { + v.log.WithField("path", link).Errorf("invalid template path") + } + err := v.Add(f[0], f[1]) + if err != nil { + v.log.WithError("err", err).Errorf("reload template") + continue + } } - }() - v.sendTrigger() + }) + v.wg.Wait() } diff --git a/internal/pkg/utils/bytes.go b/internal/pkg/utils/bytes.go index 4990086..2f3406f 100644 --- a/internal/pkg/utils/bytes.go +++ b/internal/pkg/utils/bytes.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package utils import "bytes" diff --git a/internal/pkg/utils/bytes_test.go b/internal/pkg/utils/bytes_test.go index bd98c89..a404c97 100644 --- a/internal/pkg/utils/bytes_test.go +++ b/internal/pkg/utils/bytes_test.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package utils import ( diff --git a/internal/server/app.go b/internal/server/app.go index 980b22b..b8555a1 100644 --- a/internal/server/app.go +++ b/internal/server/app.go @@ -10,34 +10,34 @@ import ( "go.arwos.org/loopy/internal/pkg/cachekeys" "go.arwos.org/loopy/internal/pkg/clientshub" "go.arwos.org/loopy/internal/pkg/db" - "go.osspkg.com/goppy/sdk/app" - "go.osspkg.com/goppy/sdk/log" - "go.osspkg.com/goppy/sdk/netutil/websocket" + "go.osspkg.com/goppy/ws/event" + "go.osspkg.com/goppy/xc" + "go.osspkg.com/goppy/xlog" ) type AppV1 struct { db *db.DB hub *clientshub.Hub - cacheKV *cachekeys.Map - log log.Logger - bh func(eid websocket.EventID, m interface{}, cids ...string) + cacheKV cachekeys.MapperWithBus + log xlog.Logger + bh func(eid event.Id, m interface{}, cids ...string) } -func New(db *db.DB, l log.Logger) *AppV1 { +func New(db *db.DB, l xlog.Logger) *AppV1 { return &AppV1{ db: db, hub: clientshub.New(), - cacheKV: cachekeys.New(), + cacheKV: cachekeys.NewWithBus(), log: l, - bh: func(eid websocket.EventID, m interface{}, cids ...string) {}, + bh: func(eid event.Id, m interface{}, cids ...string) {}, } } -func (v *AppV1) SetBroadcastHandler(call func(eid websocket.EventID, m interface{}, cids ...string)) { +func (v *AppV1) SetBroadcastHandler(call func(eid event.Id, m interface{}, cids ...string)) { v.bh = call } -func (v *AppV1) Up(ctx app.Context) error { +func (v *AppV1) Up(ctx xc.Context) error { go func() { for { select { diff --git a/internal/server/consts.go b/internal/server/consts.go index 7c490d9..6c41396 100644 --- a/internal/server/consts.go +++ b/internal/server/consts.go @@ -5,7 +5,7 @@ package server -import "go.osspkg.com/goppy/sdk/errors" +import "go.osspkg.com/goppy/errors" var ( errRequestEmpty = errors.New("request is empty") diff --git a/internal/server/models.go b/internal/server/models.go index 8031655..418e6a9 100644 --- a/internal/server/models.go +++ b/internal/server/models.go @@ -8,25 +8,24 @@ package server //go:generate easyjson import ( - "go.arwos.org/loopy/api" "go.arwos.org/loopy/internal/pkg/db" ) //easyjson:json -type EntitiesKV []EntityKV - -//easyjson:json -type EntityKV struct { - Key string `json:"k"` - Value api.RawValue `json:"v,omitempty"` -} +type ( + EntitiesKV []EntityKV + EntityKV struct { + Key string `json:"k"` + Value *string `json:"v"` + } +) func (v EntityKV) ToDB() db.EntityKV { kvi := db.EntityKV{ Key: []byte(v.Key), } if v.Value != nil { - kvi.Value = v.Value + kvi.Value = []byte(*v.Value) } return kvi } @@ -36,7 +35,8 @@ func (v *EntityKV) FromDB(item db.EntityKV) { if len(item.Value) == 0 { v.Value = nil } else { - v.Value = item.Value + s := string(item.Value) + v.Value = &s } } @@ -45,3 +45,14 @@ func (v *EntityKV) UseEmptyValue() { v.Value = nil } } + +//easyjson:json +type ( + EntitiesService []EntityService + EntityService struct { + Name string `json:"n"` + Address string `json:"a,omitempty"` + Tags []string `json:"t,omitempty"` + Health string `json:"h,omitempty"` + } +) diff --git a/internal/server/models_easyjson.go b/internal/server/models_easyjson.go index b8510ef..7696393 100644 --- a/internal/server/models_easyjson.go +++ b/internal/server/models_easyjson.go @@ -17,7 +17,124 @@ var ( _ easyjson.Marshaler ) -func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer(in *jlexer.Lexer, out *EntityKV) { +func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer(in *jlexer.Lexer, out *EntityService) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "n": + out.Name = string(in.String()) + case "a": + out.Address = string(in.String()) + case "t": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]string, 0, 4) + } else { + out.Tags = []string{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.Tags = append(out.Tags, v1) + in.WantComma() + } + in.Delim(']') + } + case "h": + out.Health = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer(out *jwriter.Writer, in EntityService) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"n\":" + out.RawString(prefix[1:]) + out.String(string(in.Name)) + } + if in.Address != "" { + const prefix string = ",\"a\":" + out.RawString(prefix) + out.String(string(in.Address)) + } + if len(in.Tags) != 0 { + const prefix string = ",\"t\":" + out.RawString(prefix) + { + out.RawByte('[') + for v2, v3 := range in.Tags { + if v2 > 0 { + out.RawByte(',') + } + out.String(string(v3)) + } + out.RawByte(']') + } + } + if in.Health != "" { + const prefix string = ",\"h\":" + out.RawString(prefix) + out.String(string(in.Health)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v EntityService) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v EntityService) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *EntityService) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *EntityService) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer(l, v) +} +func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(in *jlexer.Lexer, out *EntityKV) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -39,8 +156,14 @@ func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer(in *jlexer.Lexer, out * case "k": out.Key = string(in.String()) case "v": - if data := in.Raw(); in.Ok() { - in.AddError((out.Value).UnmarshalJSON(data)) + if in.IsNull() { + in.Skip() + out.Value = nil + } else { + if out.Value == nil { + out.Value = new(string) + } + *out.Value = string(in.String()) } default: in.SkipRecursive() @@ -52,7 +175,7 @@ func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer(in *jlexer.Lexer, out * in.Consumed() } } -func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer(out *jwriter.Writer, in EntityKV) { +func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer1(out *jwriter.Writer, in EntityKV) { out.RawByte('{') first := true _ = first @@ -61,10 +184,14 @@ func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer(out *jwriter.Writer, in out.RawString(prefix[1:]) out.String(string(in.Key)) } - if len(in.Value) != 0 { + { const prefix string = ",\"v\":" out.RawString(prefix) - out.Raw((in.Value).MarshalJSON()) + if in.Value == nil { + out.RawString("null") + } else { + out.String(string(*in.Value)) + } } out.RawByte('}') } @@ -72,27 +199,27 @@ func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer(out *jwriter.Writer, in // MarshalJSON supports json.Marshaler interface func (v EntityKV) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer(&w, v) + easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer1(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EntityKV) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer(w, v) + easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer1(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EntityKV) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer(&r, v) + easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EntityKV) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer(l, v) + easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(l, v) } -func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(in *jlexer.Lexer, out *EntitiesKV) { +func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer2(in *jlexer.Lexer, out *EntitiesService) { isTopLevel := in.IsStart() if in.IsNull() { in.Skip() @@ -101,7 +228,73 @@ func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(in *jlexer.Lexer, out in.Delim('[') if *out == nil { if !in.IsDelim(']') { - *out = make(EntitiesKV, 0, 1) + *out = make(EntitiesService, 0, 0) + } else { + *out = EntitiesService{} + } + } else { + *out = (*out)[:0] + } + for !in.IsDelim(']') { + var v4 EntityService + (v4).UnmarshalEasyJSON(in) + *out = append(*out, v4) + in.WantComma() + } + in.Delim(']') + } + if isTopLevel { + in.Consumed() + } +} +func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer2(out *jwriter.Writer, in EntitiesService) { + if in == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v5, v6 := range in { + if v5 > 0 { + out.RawByte(',') + } + (v6).MarshalEasyJSON(out) + } + out.RawByte(']') + } +} + +// MarshalJSON supports json.Marshaler interface +func (v EntitiesService) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v EntitiesService) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *EntitiesService) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *EntitiesService) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer2(l, v) +} +func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer3(in *jlexer.Lexer, out *EntitiesKV) { + isTopLevel := in.IsStart() + if in.IsNull() { + in.Skip() + *out = nil + } else { + in.Delim('[') + if *out == nil { + if !in.IsDelim(']') { + *out = make(EntitiesKV, 0, 2) } else { *out = EntitiesKV{} } @@ -109,9 +302,9 @@ func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(in *jlexer.Lexer, out *out = (*out)[:0] } for !in.IsDelim(']') { - var v1 EntityKV - (v1).UnmarshalEasyJSON(in) - *out = append(*out, v1) + var v7 EntityKV + (v7).UnmarshalEasyJSON(in) + *out = append(*out, v7) in.WantComma() } in.Delim(']') @@ -120,16 +313,16 @@ func easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(in *jlexer.Lexer, out in.Consumed() } } -func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer1(out *jwriter.Writer, in EntitiesKV) { +func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer3(out *jwriter.Writer, in EntitiesKV) { if in == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { out.RawString("null") } else { out.RawByte('[') - for v2, v3 := range in { - if v2 > 0 { + for v8, v9 := range in { + if v8 > 0 { out.RawByte(',') } - (v3).MarshalEasyJSON(out) + (v9).MarshalEasyJSON(out) } out.RawByte(']') } @@ -138,23 +331,23 @@ func easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer1(out *jwriter.Writer, i // MarshalJSON supports json.Marshaler interface func (v EntitiesKV) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer1(&w, v) + easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer3(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EntitiesKV) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer1(w, v) + easyjsonD2b7633eEncodeGoArwosOrgLoopyInternalServer3(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EntitiesKV) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(&r, v) + easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer3(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EntitiesKV) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer1(l, v) + easyjsonD2b7633eDecodeGoArwosOrgLoopyInternalServer3(l, v) } diff --git a/internal/server/models_test.go b/internal/server/models_test.go new file mode 100644 index 0000000..5f94ab8 --- /dev/null +++ b/internal/server/models_test.go @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + +package server + +import ( + "encoding/json" + "testing" + + "go.osspkg.com/goppy/xtest" +) + +func TestUnit_EntityKV(t *testing.T) { + m := EntityKV{ + Key: "1", + } + b, err := json.Marshal(m) + xtest.NoError(t, err) + xtest.Equal(t, string(b), `{"k":"1","v":null}`) + + d := "123" + m = EntityKV{ + Key: "1", + Value: &d, + } + b, err = json.Marshal(m) + xtest.NoError(t, err) + xtest.Equal(t, string(b), `{"k":"1","v":"123"}`) +} diff --git a/internal/server/proxy.go b/internal/server/proxy.go index 80e7613..58069a7 100644 --- a/internal/server/proxy.go +++ b/internal/server/proxy.go @@ -1,11 +1,16 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package server import ( "net/http" - "go.osspkg.com/goppy/plugins/web" - "go.osspkg.com/goppy/sdk/log" - "go.osspkg.com/goppy/sdk/netutil/websocket" + "go.osspkg.com/goppy/web" + "go.osspkg.com/goppy/ws/server" + "go.osspkg.com/goppy/xlog" ) type Props struct { @@ -14,8 +19,8 @@ type Props struct { Log func(key string, err error, suffix string) } -func (v *AppV1) ProxyWS(call func(p *Props) error) func(websocket.Response, websocket.Request, websocket.Meta) error { - return func(w websocket.Response, r websocket.Request, _ websocket.Meta) error { +func (v *AppV1) ProxyWS(call func(p *Props) error) func(server.Response, server.Request, server.Meta) error { + return func(w server.Response, r server.Request, _ server.Meta) error { return call(&Props{ Decode: func(in interface{}) error { return r.Decode(in) @@ -24,7 +29,7 @@ func (v *AppV1) ProxyWS(call func(p *Props) error) func(websocket.Response, webs w.Encode(in) }, Log: func(key string, err error, suffix string) { - v.log.WithFields(log.Fields{ + v.log.WithFields(xlog.Fields{ "key": key, "err": err.Error(), }).Warnf("ws %s key error", suffix) @@ -43,7 +48,7 @@ func (v *AppV1) ProxyRest(call func(p *Props) error) func(ctx web.Context) { ctx.JSON(http.StatusOK, in) }, Log: func(key string, err error, suffix string) { - v.log.WithFields(log.Fields{ + v.log.WithFields(xlog.Fields{ "key": key, "err": err.Error(), }).Warnf("rest %s key error", suffix) diff --git a/internal/server/routes.go b/internal/server/routes.go index 6569f3d..8cccfd0 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -8,12 +8,13 @@ package server import ( "go.arwos.org/loopy/api" "go.osspkg.com/goppy/plugins" - "go.osspkg.com/goppy/plugins/web" + "go.osspkg.com/goppy/web" + "go.osspkg.com/goppy/ws" ) var Plugin = plugins.Plugin{ Inject: New, - Resolve: func(r web.RouterPool, c *AppV1, ws web.WebsocketServer) { + Resolve: func(r web.RouterPool, c *AppV1, ws ws.WebsocketServer) { router := r.Main() c.SetBroadcastHandler(ws.SendEvent) diff --git a/internal/server/watch.go b/internal/server/watch.go index 0b01c7e..82dff7e 100644 --- a/internal/server/watch.go +++ b/internal/server/watch.go @@ -1,11 +1,17 @@ +/* + * Copyright (c) 2023 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD-3-Clause license that can be found in the LICENSE file. + */ + package server import ( "go.arwos.org/loopy/api" - "go.osspkg.com/goppy/sdk/netutil/websocket" + "go.osspkg.com/goppy/ws/event" + "go.osspkg.com/goppy/ws/server" ) -func (v *AppV1) Broadcast(key string, eid websocket.EventID, m interface{}) { +func (v *AppV1) Broadcast(key string, eid event.Id, m interface{}) { cids := v.hub.GetClients(key) if len(cids) == 0 { return @@ -13,17 +19,17 @@ func (v *AppV1) Broadcast(key string, eid websocket.EventID, m interface{}) { v.bh(eid, m, cids...) } -func (v *AppV1) KVWatchV1(w websocket.Response, r websocket.Request, m websocket.Meta) error { - var data EntitiesKV - if err := r.Decode(&data); err != nil { +func (v *AppV1) KVWatchV1(w server.Response, r server.Request, m server.Meta) error { + var req EntitiesKV + if err := r.Decode(&req); err != nil { return err } - if len(data) == 0 { + if len(req) == 0 { return errRequestEmpty } - keys := make([]string, 0, len(data)) - for _, datum := range data { + keys := make([]string, 0, len(req)) + for _, datum := range req { keys = append(keys, datum.Key) } @@ -33,16 +39,21 @@ func (v *AppV1) KVWatchV1(w websocket.Response, r websocket.Request, m websocket }) } - for i := 0; i < len(data); i++ { - entity := data[i].ToDB() - if err := v.db.GetKV(&entity); err != nil { - (&data[i]).UseEmptyValue() - } else { - (&data[i]).FromDB(entity) + resp := make(EntitiesKV, 0, len(req)*2) + for i := 0; i < len(req); i++ { + entity := req[i].ToDB() + list, err := v.db.SearchKV(entity.Key) + if err != nil { + continue + } + for _, kv := range list { + item := EntityKV{} + item.FromDB(kv) + resp = append(resp, item) } } v.hub.Add(m.ConnectID(), keys) - w.EncodeEvent(api.EventKVWatchValue, data) + w.EncodeEvent(api.EventKVWatchValue, resp) return nil } diff --git a/test_data/template.tmpl b/test_data/template.tmpl index 36db9a5..eb1a9f8 100644 --- a/test_data/template.tmpl +++ b/test_data/template.tmpl @@ -1,3 +1,17 @@ Hello '{{key "users/demo"}}' -{{key "k1/aaa/bbb"}} \ No newline at end of file +{{key "k1/aaa/bbb"}} + +-- {{key_or_default "k1/aaa22/bbb22" "bbb22"}} -- +-- {{key_or_default "key/1" "1"}} -- +-- {{key_or_default "key/2" "2"}} -- +-- {{key_or_default "key/3" "3"}} -- +-- {{key_or_default "key/4" "4"}} -- +-- {{key_or_default "key/5" "5"}} -- +-- {{key_or_default "key/6" "6"}} -- +-- {{key_or_default "key/7" "7"}} -- +-- {{key_or_default "key/8" "8"}} -- + +{{range $id, $data := tree "k1/"}} +id: {{$id}} key: {{$data.Key}} val: {{$data.Value}} +{{end}} \ No newline at end of file