diff --git a/README.md b/README.md index c24f0d5..c0a5f84 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,9 @@ The codec receives `key` as context, but can only return transformed `value` and ### Support Commands +The public API exposes typed text- and meta-protocol commands only. There is no +arbitrary raw command passthrough; callers should use the methods below. + Now, we have implemented some commands, and we will implement more commands in the future. | Command | Status | API Usage | Description | @@ -186,7 +189,7 @@ Now, we have implemented some commands, and we will implement more commands in t #### Prerequisites -- Go 1.26 or higher +- Go 1.26.5 or higher - Python (for pre-commit hooks) or just `brew install pre-commit` on MacOS - Docker (for running memcached in tests) diff --git a/benchmark/README.md b/benchmark/README.md index ced8e80..56c6842 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,10 +1,9 @@ ## Benchmark differences -Comparing following three implementations: +Comparing following implementations: -- [yeqown/memcached](https://github.com/yeqown/memcahced) +- [yeqown/memcached](https://github.com/yeqown/memcached) - [bradfitz/gomemcache](https://github.com/bradfitz/gomemcache) -- [rainy/memcache](github.com/rainycape/memcache) ## How to run @@ -12,8 +11,8 @@ Comparing following three implementations: > And we can use `go build -gcflags="-m" ./...` to check the escape analysis. ```bash -# install benchcmp -go install golang.org/x/tools/cmd/benchcmp@latest +# install benchstat +go install golang.org/x/perf/cmd/benchstat@latest ``` To run benchmark and analysis: diff --git a/benchmark/client_test.go b/benchmark/client_test.go index 125719d..feba93f 100644 --- a/benchmark/client_test.go +++ b/benchmark/client_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/bradfitz/gomemcache/memcache" - rainycape "github.com/rainycape/memcache" "github.com/yeqown/memcached" ) @@ -15,29 +14,15 @@ func Test_Yeqown(t *testing.T) { if err != nil { t.Fatal(err) } - defer client.Close() - client.Set(context.Background(), testKey, testValue, 0, 0) - item, err := client.Get(context.Background(), testKey) - if err != nil { - t.Fatal(err) - } - if string(item.Value) != string(testValue) { - t.Fatalf("expect %s, got %s", string(testValue), string(item.Value)) - } -} - -func Test_Rainycape(t *testing.T) { - t.Skipf("It's a binary package, not support test") - - client, err := rainycape.New("127.0.0.1:11211") - if err != nil { - t.Fatal(err) - } - client.Set(&rainycape.Item{ - Key: testKey, - Value: testValue, + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Errorf("close client: %v", err) + } }) - item, err := client.Get(testKey) + if err := client.Set(context.Background(), testKey, testValue, 0, 0); err != nil { + t.Fatalf("set failed: %v", err) + } + item, err := client.Get(context.Background(), testKey) if err != nil { t.Fatal(err) } diff --git a/benchmark/concurrent_benchmark_test.go b/benchmark/concurrent_benchmark_test.go index 0448f5c..c964819 100644 --- a/benchmark/concurrent_benchmark_test.go +++ b/benchmark/concurrent_benchmark_test.go @@ -13,7 +13,11 @@ func BenchmarkYeqownMemcachedConcurrent(b *testing.B) { if err != nil { b.Fatal(err) } - defer client.Close() + b.Cleanup(func() { + if err := client.Close(); err != nil { + b.Errorf("close client: %v", err) + } + }) ctx := context.Background() diff --git a/benchmark/go.mod b/benchmark/go.mod index 7651c5b..1716bc3 100644 --- a/benchmark/go.mod +++ b/benchmark/go.mod @@ -1,16 +1,23 @@ -module benchmark +module github.com/yeqown/memcached/benchmark -go 1.26.0 +go 1.26.5 require ( - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 - github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 - github.com/yeqown/memcached v1.1.3 + github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c + github.com/yeqown/memcached v1.4.0 ) require ( - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/stretchr/testify v1.11.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/klauspost/compress v1.19.2 // indirect + github.com/pierrec/lz4/v4 v4.1.28 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/trace v1.45.0 // indirect ) + +replace github.com/yeqown/memcached => ../ diff --git a/benchmark/go.sum b/benchmark/go.sum index 0c42fcf..24421d5 100644 --- a/benchmark/go.sum +++ b/benchmark/go.sum @@ -1,20 +1,33 @@ -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8= +github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/pierrec/lz4/v4 v4.1.28 h1:pPEPwRJ4kybBTfGt28q7lQsRJQHhC08axprdLD5Ppio= +github.com/pierrec/lz4/v4 v4.1.28/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 h1:dq90+d51/hQRaHEqRAsQ1rE/pC1GUS4sc2rCbbFsAIY= -github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/yeqown/memcached v1.1.3 h1:/b5Nwxq/2xDPgido5+2818uwkdDQYja3Pzl8X6LQyC0= -github.com/yeqown/memcached v1.1.3/go.mod h1:1NndLxAOAhl9ecmycI8wIv4nIv+fcpAUN0vLlqM7jn4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= 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/benchmark/mem_benchmark_test.go b/benchmark/mem_benchmark_test.go index e8408c4..4970746 100644 --- a/benchmark/mem_benchmark_test.go +++ b/benchmark/mem_benchmark_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/bradfitz/gomemcache/memcache" - rainycape "github.com/rainycape/memcache" "github.com/yeqown/memcached" ) @@ -22,7 +21,11 @@ func BenchmarkYeqownMemcached(b *testing.B) { if err != nil { b.Fatal(err) } - defer client.Close() + b.Cleanup(func() { + if err := client.Close(); err != nil { + b.Errorf("close client: %v", err) + } + }) ctx := context.Background() @@ -56,27 +59,3 @@ func BenchmarkBradfitzGomemcache(b *testing.B) { } } } - -func BenchmarkRainycapeMemcache(b *testing.B) { - b.Skipf("It's a binary package, not support benchmark.") - - client, err := rainycape.New("localhost:11211") - if err != nil { - b.Fatal(err) - } - defer client.Close() - item := &rainycape.Item{ - Key: testKey, - Value: testValue, - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - if err := client.Set(item); err != nil { - b.Fatal(err) - } - if _, err := client.Get(testKey); err != nil { - b.Fatal(err) - } - } -} diff --git a/client.go b/client.go index 9790f24..3569357 100644 --- a/client.go +++ b/client.go @@ -3,12 +3,12 @@ package memcached import ( "bytes" "context" + "errors" + "fmt" "io" "sync" "time" - multierror "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" "go.opentelemetry.io/otel/trace" "github.com/yeqown/memcached/telemetry" @@ -21,8 +21,6 @@ type Client interface { basicTextProtocolCommander metaTextProtocolCommander statisticsTextProtocolCommander - // TODO: support rawTextProtocolCommander - // rawTextProtocolCommander } var _ Client = (*client)(nil) @@ -70,11 +68,11 @@ func newClientWithContext(_ context.Context, addr string, opts ...ClientOption) addrs, err := options.resolver.Resolve(addr) if err != nil { - return nil, errors.Wrap(err, "resolve failed") + return nil, fmt.Errorf("resolve failed: %w", err) } if len(addrs) == 0 { - return nil, errors.Wrap(ErrInvalidAddress, "empty address") + return nil, fmt.Errorf("empty address: %w", ErrInvalidAddress) } picker := options.pickBuilder.Build(addrs) @@ -100,7 +98,7 @@ func (c *client) Close() error { for _, pool := range c.connPools { if err := pool.close(); err != nil { - return errors.Wrap(err, "Close") + return fmt.Errorf("Close: %w", err) } } @@ -129,7 +127,7 @@ func (c *client) getConn(ctx context.Context, addr *Addr) (memcachedConn, error) cn, err = newConnContext(ctx2, addr, c.options.dialTimeout) if err != nil { - return nil, errors.Wrap(err, "newConnContext failed") + return nil, fmt.Errorf("newConnContext failed: %w", err) } // SASL auth if enabled @@ -196,12 +194,12 @@ func (c *client) broadcastRequest(ctx context.Context, call callFunc) error { wg.Wait() close(errCh) - var multiErr error + var errs []error for err := range errCh { - multiErr = multierror.Append(multiErr, err) + errs = append(errs, err) } - return multiErr + return errors.Join(errs...) } func (c *client) dispatchRequest(ctx context.Context, req *request, resp *response) error { @@ -213,7 +211,7 @@ func (c *client) dispatchRequest(ctx context.Context, req *request, resp *respon addr, err := c.picker.Pick(c.addrs, req.cmd, req.key) if err != nil { - return errors.Wrap(err, "pick node failed") + return fmt.Errorf("pick node failed: %w", err) } // START: Telemetry @@ -232,7 +230,7 @@ func (c *client) dispatchRequest(ctx context.Context, req *request, resp *respon if c.metrics != nil { c.metrics.RecordDuration(context.Background(), string(req.cmd), addr.Address, time.Since(start), err) } - return errors.Wrap(err, "alloc connection failed") + return fmt.Errorf("alloc connection failed: %w", err) } defer func() { _ = cn.release() }() @@ -245,7 +243,7 @@ func (c *client) dispatchRequest(ctx context.Context, req *request, resp *respon if c.metrics != nil { c.metrics.RecordDuration(context.Background(), string(req.cmd), addr.Address, time.Since(start), err) } - return errors.Wrap(err, "send failed") + return fmt.Errorf("send failed: %w", err) } recvErr := resp.recv(ctx, cn, c.options.readTimeout) @@ -275,13 +273,13 @@ func authSASL(conn memcachedConn, username, password string) error { // 1. first, list mechanisms the server supports req, resp := saslListMechanisms() if err := req.send(conn); err != nil { - return errors.Wrap(err, "authSASL send") + return fmt.Errorf("authSASL send: %w", err) } if err := resp.read(conn); err != nil { - return errors.Wrap(err, "authSASL recv") + return fmt.Errorf("authSASL recv: %w", err) } if err := resp.expect(_binaryStatusOK); err != nil { - return errors.Wrap(err, "authSASL") + return fmt.Errorf("authSASL: %w", err) } if !bytes.Contains(resp.value, []byte("PLAIN")) { @@ -291,13 +289,13 @@ func authSASL(conn memcachedConn, username, password string) error { // 2. choose one mechanism and send the authentication request req, resp = saslAuthRequestPlain(username, password) if err := req.send(conn); err != nil { - return errors.Wrap(err, "authSASL send") + return fmt.Errorf("authSASL send: %w", err) } if err := resp.read(conn); err != nil { - return errors.Wrap(err, "authSASL recv") + return fmt.Errorf("authSASL recv: %w", err) } if err := resp.expect(_binaryStatusOK); err != nil { - return errors.Wrap(err, "authSASL") + return fmt.Errorf("authSASL: %w", err) } return nil diff --git a/client_cluster.go b/client_cluster.go index c16f4a6..5a351e2 100644 --- a/client_cluster.go +++ b/client_cluster.go @@ -1,12 +1,12 @@ package memcached import ( + "fmt" "hash/crc32" + "math" "net" "strings" - "github.com/pkg/errors" - "github.com/yeqown/memcached/hash" ) @@ -52,7 +52,7 @@ type defaultResolver struct{} func (r defaultResolver) Resolve(addr string) ([]*Addr, error) { if addr == "" { - return nil, errors.Wrap(ErrInvalidAddress, "empty address") + return nil, fmt.Errorf("empty address: %w", ErrInvalidAddress) } addrs := strings.Split(addr, ",") @@ -73,7 +73,7 @@ func (r defaultResolver) Resolve(addr string) ([]*Addr, error) { } if len(result) == 0 { - return nil, errors.Wrap(ErrInvalidAddress, "no available address") + return nil, fmt.Errorf("no available address: %w", ErrInvalidAddress) } return result, nil @@ -83,7 +83,7 @@ func (r defaultResolver) Resolve(addr string) ([]*Addr, error) { func (r defaultResolver) resolveAddr(address string) (network, addr string, err error) { address = strings.TrimSpace(address) if address == "" { - return "", "", errors.Wrap(ErrInvalidAddress, "empty address") + return "", "", fmt.Errorf("empty address: %w", ErrInvalidAddress) } network = "tcp" @@ -108,7 +108,7 @@ func (r defaultResolver) resolveAddr(address string) (network, addr string, err } if err != nil { - return "", "", errors.Wrap(err, "invalid address: "+address) + return "", "", fmt.Errorf("invalid address: %s: %w", address, err) } return network, addr, nil @@ -121,14 +121,19 @@ type crc32HashPicker struct{} func (p *crc32HashPicker) Pick(addrs []*Addr, _, key []byte) (*Addr, error) { n := len(addrs) if n == 0 { - return nil, errors.Wrap(ErrInvalidAddress, "no available address") + return nil, fmt.Errorf("no available address: %w", ErrInvalidAddress) } if n == 1 { return addrs[0], nil } sum := crc32.ChecksumIEEE(key) - return addrs[sum%uint32(n)], nil + index64 := uint64(sum) % uint64(n) + if index64 > math.MaxInt { + return nil, fmt.Errorf("hash index exceeds int: %w", ErrInvalidAddress) + } + index := int(index64) + return addrs[index], nil } type crc32HashPickBuilder struct{} @@ -150,7 +155,7 @@ type murmur3HashPicker struct { func (p *murmur3HashPicker) Pick(addrs []*Addr, _, key []byte) (*Addr, error) { n := len(addrs) if n == 0 { - return nil, errors.Wrap(ErrInvalidAddress, "no available address") + return nil, fmt.Errorf("no available address: %w", ErrInvalidAddress) } if n == 1 { return addrs[0], nil diff --git a/client_commands.go b/client_commands.go index 017ca29..58eaf89 100644 --- a/client_commands.go +++ b/client_commands.go @@ -3,9 +3,10 @@ package memcached import ( "bytes" "context" + "errors" + "fmt" + "math" "time" - - "github.com/pkg/errors" ) type basicTextProtocolCommander interface { @@ -126,13 +127,6 @@ type statisticsTextProtocolCommander interface { Stats(ctx context.Context) (*Statistic, error) } -type rawTextProtocolCommander interface { - // Raw is used to send the raw command to the memcached server. - // Warning: this command is not recommended to use since it expects the server to return - // the `END` line, which is not guaranteed of all commands. For example, `version`. - Raw(ctx context.Context, cmd string) ([]string, error) -} - /** * Storage commands: set, add, replace, append, prepend, cas */ @@ -144,16 +138,16 @@ func (c *client) storageCommand(ctx context.Context, command, key string, value req, resp, err := buildStorageCommand(command, key, value, flag, expiry, c.options.noReply, c.options.codec) if err != nil { - return errors.Wrap(err, "build storage command failed") + return fmt.Errorf("build storage command failed: %w", err) } defer releaseReqAndResp(req, resp) if err = c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } if err = resp.expect(_StoredCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -191,11 +185,11 @@ func (c *client) Cas(ctx context.Context, key string, value []byte, flag uint32, defer releaseReqAndResp(req, resp) if err = c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } if err = resp.expect(_StoredCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -214,15 +208,15 @@ func (c *client) Get(ctx context.Context, key string) (*Item, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } items, err := parseValueItems(resp.rawLines, false, false, c.options.codec) if err != nil { - return nil, errors.Wrap(err, "parse values failed") + return nil, fmt.Errorf("parse values failed: %w", err) } if len(items) == 0 { - return nil, errors.Wrap(ErrNotFound, "no items found") + return nil, fmt.Errorf("no items found: %w", ErrNotFound) } return items[0], nil @@ -237,15 +231,15 @@ func (c *client) Gets(ctx context.Context, keys ...string) ([]*Item, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } items, err := parseValueItems(resp.rawLines, false, true, c.options.codec) if err != nil { - return nil, errors.Wrap(ErrMalformedResponse, "parse values failed") + return nil, fmt.Errorf("parse values failed: %w", ErrMalformedResponse) } if len(items) == 0 { - return nil, errors.Wrap(ErrNotFound, "no items found") + return nil, fmt.Errorf("no items found: %w", ErrNotFound) } return items, nil @@ -260,16 +254,16 @@ func (c *client) GetAndTouch(ctx context.Context, expiry time.Duration, key stri defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } items, err := parseValueItems(resp.rawLines, false, false, c.options.codec) if err != nil { - return nil, errors.Wrap(ErrMalformedResponse, "parse values failed") + return nil, fmt.Errorf("parse values failed: %w", ErrMalformedResponse) } if len(items) == 0 { - return nil, errors.Wrap(ErrNotFound, "no items found") + return nil, fmt.Errorf("no items found: %w", ErrNotFound) } return items[0], nil @@ -284,17 +278,17 @@ func (c *client) GetAndTouches(ctx context.Context, expiry time.Duration, keys . defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } // parse response items, err := parseValueItems(resp.rawLines, false, true, c.options.codec) if err != nil { - return nil, errors.Wrap(ErrMalformedResponse, "parse values failed") + return nil, fmt.Errorf("parse values failed: %w", ErrMalformedResponse) } if len(items) == 0 { - return nil, errors.Wrap(ErrNotFound, "no items found") + return nil, fmt.Errorf("no items found: %w", ErrNotFound) } return items, nil @@ -313,12 +307,12 @@ func (c *client) Delete(ctx context.Context, key string) error { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } // expect DELETED\r\n if err := resp.expect(_DeletedCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -331,13 +325,13 @@ func (c *client) Incr(ctx context.Context, key string, delta uint64) (uint64, er req, resp := buildArithmeticCommand("incr", key, delta, c.options.noReply) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return 0, errors.Wrap(err, "request failed") + return 0, fmt.Errorf("request failed: %w", err) } // parse response value, err := parseArithmetic(resp.rawLines[0]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, err.Error()) + return 0, fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return value, nil @@ -350,13 +344,13 @@ func (c *client) Decr(ctx context.Context, key string, delta uint64) (uint64, er req, resp := buildArithmeticCommand("decr", key, delta, c.options.noReply) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return 0, errors.Wrap(err, "request failed") + return 0, fmt.Errorf("request failed: %w", err) } // parse response value, err := parseArithmetic(resp.rawLines[0]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, err.Error()) + return 0, fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return value, nil @@ -371,12 +365,12 @@ func (c *client) Touch(ctx context.Context, key string, expiry time.Duration) er defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } // expect TOUCHED\r\n if err := resp.expect(_TouchedCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -387,14 +381,14 @@ func (c *client) Version(ctx context.Context) (string, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return "", errors.Wrap(err, "request") + return "", fmt.Errorf("request: %w", err) } // parse version number from response // VERSION 1.6.14 line := resp.rawLines[0] if !bytes.HasPrefix(line, _VersionBytes) { - return "", errors.Wrap(ErrMalformedResponse, string(line)) + return "", fmt.Errorf("%s: %w", string(line), ErrMalformedResponse) } return string(trimCRLF(line[8:])), nil @@ -408,22 +402,22 @@ func (c *client) FlushAll(ctx context.Context) error { c.autoSwitchToUDP(ctx, req, resp) if err := req.send(ctx, cn, c.options.writeTimeout); err != nil { - return errors.Wrap(err, "send failed") + return fmt.Errorf("send failed: %w", err) } if err := resp.recv(ctx, cn, c.options.readTimeout); err != nil { - return errors.Wrap(err, "recv failed") + return fmt.Errorf("recv failed: %w", err) } // expect OK\r\n if err := resp.expect(_OKCRLFBytes); err != nil { - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil } if err := c.broadcastRequest(ctx, call); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } return nil @@ -443,6 +437,9 @@ func (c *client) MetaSet(ctx context.Context, key, value []byte, msOptions ...Me for _, applyFn := range msOptions { applyFn(msFlags) } + if msFlags.T > math.MaxInt64 { + return nil, fmt.Errorf("TTL exceeds int64: %w", ErrInvalidArgument) + } clientFlags := msFlags.F req, resp, err := buildMetaSetCommand(key, value, msFlags, c.options.codec) @@ -451,7 +448,7 @@ func (c *client) MetaSet(ctx context.Context, key, value []byte, msOptions ...Me } defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItem{ @@ -486,7 +483,7 @@ func (c *client) MetaGet(ctx context.Context, key []byte, mgOptions ...MetaGetOp defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItem{ @@ -513,7 +510,7 @@ func (c *client) MetaDelete(ctx context.Context, key []byte, options ...MetaDele defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItem{ @@ -540,7 +537,7 @@ func (c *client) MetaArithmetic(ctx context.Context, key []byte, delta uint64, o defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItem{ @@ -579,7 +576,7 @@ func (c *client) MetaDebug(ctx context.Context, key []byte, options ...MetaDebug defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } item := &MetaItemDebug{ @@ -598,14 +595,14 @@ func (c *client) MetaNoOp(ctx context.Context) error { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return errors.Wrap(err, "request failed") + return fmt.Errorf("request failed: %w", err) } if err := resp.expect(_MetaMNCRLFBytes); err != nil { if errors.Is(err, ErrMalformedResponse) { return err } - return errors.Wrap(ErrMalformedResponse, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrMalformedResponse) } return nil @@ -621,24 +618,8 @@ func (c *client) Stats(ctx context.Context) (*Statistic, error) { defer releaseReqAndResp(req, resp) if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") + return nil, fmt.Errorf("request failed: %w", err) } return parseStats(resp.rawLines) } - -func (c *client) Raw(ctx context.Context, cmd string) ([]string, error) { - req, resp := buildRawCommand(cmd, endIndicatorSpecificEndLine, 0) - defer releaseReqAndResp(req, resp) - - if err := c.dispatchRequest(ctx, req, resp); err != nil { - return nil, errors.Wrap(err, "request failed") - } - - lines := make([]string, 0, len(resp.rawLines)) - for _, line := range resp.rawLines { - lines = append(lines, string(bytes.TrimSuffix(line, _CRLFBytes))) - } - - return lines, nil -} diff --git a/client_test.go b/client_test.go index 3f767cb..17108c6 100644 --- a/client_test.go +++ b/client_test.go @@ -2,11 +2,11 @@ package memcached import ( "context" + "errors" "sync" "testing" "time" - pkgerrors "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -137,7 +137,7 @@ func (su *clientTestSuite) Test_concurrent() { for counter <= 200 { item, err := su.client.Get(ctx, key) - if pkgerrors.Is(err, ErrNotFound) { + if errors.Is(err, ErrNotFound) { goto next } su.NoError(err) @@ -160,7 +160,7 @@ func (su *clientTestSuite) Test_concurrent() { for counter <= 100 { err := su.client.Touch(ctx, key, 3) - if !pkgerrors.Is(err, ErrNotFound) { + if !errors.Is(err, ErrNotFound) { su.NoError(err) } @@ -247,11 +247,11 @@ func TestCompressionDisablesAppendPrepend(t *testing.T) { err := client.Append(context.Background(), "key", []byte("value"), 0, 0) require.Error(t, err) - assert.True(t, pkgerrors.Is(err, ErrNotSupported)) + assert.True(t, errors.Is(err, ErrNotSupported)) err = client.Prepend(context.Background(), "key", []byte("value"), 0, 0) require.Error(t, err) - assert.True(t, pkgerrors.Is(err, ErrNotSupported)) + assert.True(t, errors.Is(err, ErrNotSupported)) } func TestCompressionDisablesMetaAppendPrepend(t *testing.T) { @@ -269,7 +269,7 @@ func TestCompressionDisablesMetaAppendPrepend(t *testing.T) { ) assert.Nil(t, item) require.Error(t, err) - assert.True(t, pkgerrors.Is(err, ErrNotSupported)) + assert.True(t, errors.Is(err, ErrNotSupported)) }) } } diff --git a/cmd/memcached-cli/commands.go b/cmd/memcached-cli/commands.go index 3735791..826ec39 100644 --- a/cmd/memcached-cli/commands.go +++ b/cmd/memcached-cli/commands.go @@ -1,11 +1,13 @@ package main import ( + "errors" "fmt" + "io" + "os" "strings" "time" - "github.com/pkg/errors" "github.com/samber/lo" "github.com/spf13/cobra" "github.com/yeqown/memcached" @@ -162,9 +164,9 @@ func newContextCurrentCommand() *cobra.Command { const ( magicFlags uint32 = 0x0705 - magicSeed = 0x2014 + magicSeed uint64 = 0x2014 - historyTimeFormat = "2006-01-02 15:04:05" + historyTimeFormat string = "2006-01-02 15:04:05" ) func newKVGetCommand() *cobra.Command { @@ -193,14 +195,12 @@ func newKVGetCommand() *cobra.Command { memcached.MetaGetFlagReturnHitBefore(), ) if err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(os.Stdout, err) } - history.addRecord("get", args) + recordHistory(cmd, history, "get", args) - printMetaItem(item) - - return nil + return printMetaItem(item) }, } } @@ -224,10 +224,10 @@ func newKVSetCommand() *cobra.Command { err = client.Set(cmd.Context(), args[0], []byte(args[1]), magicFlags, expiration) if err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(os.Stdout, err) } - history.addRecord("set", args) + recordHistory(cmd, history, "set", args) fmt.Printf("OK\n") return nil @@ -254,10 +254,10 @@ func newKVDeleteCommand() *cobra.Command { err = client.Delete(cmd.Context(), args[0]) if err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(os.Stdout, err) } - history.addRecord("delete", args) + recordHistory(cmd, history, "delete", args) fmt.Printf("OK\n") return nil @@ -295,18 +295,16 @@ func newKVGetsCommand() *cobra.Command { memcached.MetaGetFlagReturnHitBefore(), ) if err != nil { - fmt.Printf("Encounter an error while getting key '%s': %v\n", key, errors.Cause(err)) + fmt.Printf("Encounter an error while getting key '%s': %v\n", key, rootErr(err)) continue } items = append(items, item) } - history.addRecord("gets", args) - - printMetaItems(items) + recordHistory(cmd, history, "gets", args) - return nil + return printMetaItems(items) }, } } @@ -330,10 +328,10 @@ func newKVTouchCommand() *cobra.Command { } if err := client.Touch(cmd.Context(), args[0], expiration); err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(os.Stdout, err) } - history.addRecord("touch", args) + recordHistory(cmd, history, "touch", args) fmt.Println("OK") return nil @@ -392,10 +390,10 @@ func newKVFlushAllCommand() *cobra.Command { imme: if err := client.FlushAll(cmd.Context()); err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(os.Stdout, err) } - history.addRecord("flushall", args) + recordHistory(cmd, history, "flushall", args) fmt.Println("OK") return nil @@ -406,25 +404,45 @@ func newKVFlushAllCommand() *cobra.Command { return cmd } -func printMetaItems(items []*memcached.MetaItem) { +func printMetaItems(items []*memcached.MetaItem) error { + return writeMetaItems(os.Stdout, items) +} + +func printMetaItem(item *memcached.MetaItem) error { + return writeMetaItem(os.Stdout, item) +} + +func writeMetaItems(w io.Writer, items []*memcached.MetaItem) error { for idx, item := range items { - fmt.Printf(" ================= The [%d] item =================\n", idx) - printMetaItem(item) + if _, err := io.WriteString(w, fmt.Sprintf(" ================= The [%d] item =================\n", idx)); err != nil { + return err + } + if err := writeMetaItem(w, item); err != nil { + return err + } } + + return nil } -func printMetaItem(item *memcached.MetaItem) { +func writeMetaItem(w io.Writer, item *memcached.MetaItem) error { lastAccessAt := time.Now().Add(-time.Duration(item.LastAccessedTime) * time.Second) + output := fmt.Sprintf("Key: %s\n", item.Key) + + fmt.Sprintf("Flags: %d (0x%x)\n", item.Flags, item.Flags) + + fmt.Sprintf("CAS: %d (0x%x)\n", item.CAS, item.CAS) + + fmt.Sprintf("ClientFlags: %d (0x%x)\n", item.Flags, item.Flags) + + fmt.Sprintf("LastAccessedTime: %s (%s)\n", lastAccessAt.Format(time.RFC3339), formatSeconds(int(item.LastAccessedTime), "before", "never")) + + fmt.Sprintf("HitBefore: %s\n", map[bool]string{true: "✅", false: "❌"}[item.HitBefore]) + + fmt.Sprintf("TTL: %d (%s)\n", item.TTL, formatSeconds(int(item.TTL), "later", "never expires")) + + fmt.Sprintf("Value: %s\n\n", item.Value) + _, err := io.WriteString(w, output) + return err +} - fmt.Printf("Key: %s\n", item.Key) - fmt.Printf("Flags: %d (0x%x)\n", item.Flags, item.Flags) - fmt.Printf("CAS: %d (0x%x)\n", item.CAS, item.CAS) - fmt.Printf("ClientFlags: %d (0x%x)\n", item.Flags, item.Flags) - fmt.Printf("LastAccessedTime: %s (%s)\n", lastAccessAt.Format(time.RFC3339), formatSeconds(int(item.LastAccessedTime), "before", "never")) - fmt.Printf("HitBefore: %s\n", map[bool]string{true: "✅", false: "❌"}[item.HitBefore]) - fmt.Printf("TTL: %d (%s)\n", item.TTL, formatSeconds(int(item.TTL), "later", "never expires")) - fmt.Printf("Value: %s\n", item.Value) - fmt.Println() +func recordHistory(cmd *cobra.Command, history *kvCommandHistoryManager, command string, args []string) { + if err := history.addRecord(command, args); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to record %s command in history: %v\n", command, err) + } } func formatSeconds(seconds int, suffix, zeroString string) (readable string) { @@ -458,14 +476,14 @@ func formatSeconds(seconds int, suffix, zeroString string) (readable string) { return readable + " " + suffix } -func ignoreMemcachedError(err error) error { +func ignoreMemcachedError(w io.Writer, err error) error { if err == nil { return nil } logger.Debugf("ignoreMemcachedError handling err: %v", err) - var memErrs = []error{ + memErrs := []error{ memcached.ErrNonexistentCommand, memcached.ErrClientError, memcached.ErrServerError, @@ -486,8 +504,8 @@ func ignoreMemcachedError(err error) error { for _, memErr := range memErrs { if errors.Is(err, memErr) { - fmt.Printf("Memcached Error: %v\n", errors.Cause(err)) - return nil + _, writeErr := fmt.Fprintf(w, "Memcached Error: %v\n", rootErr(err)) + return writeErr } } @@ -510,7 +528,9 @@ func newHistoryEnableCommand() *cobra.Command { manager := getContextManager(cmd, false) manager.historyEnabled = true manager.historyMaxLines = int(historyMaxLines) - manager.save() + if err := manager.save(); err != nil { + return fmt.Errorf("save history settings: %w", err) + } fmt.Println("History enabled!") return nil }, @@ -530,7 +550,9 @@ func newHistoryDisableCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { manager := getContextManager(cmd, false) manager.historyEnabled = false - manager.save() + if err := manager.save(); err != nil { + return fmt.Errorf("save history settings: %w", err) + } fmt.Println("History disabled!") return nil }, diff --git a/cmd/memcached-cli/context_manager.go b/cmd/memcached-cli/context_manager.go index a46214f..d8ba276 100644 --- a/cmd/memcached-cli/context_manager.go +++ b/cmd/memcached-cli/context_manager.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -16,6 +17,7 @@ type contextManager struct { mu sync.RWMutex contexts map[string]*Context current string + configDir string path string historyMaxLines int historyEnabled bool @@ -32,8 +34,8 @@ func newContextManager() (*contextManager, error) { return nil, err } - path := filepath.Join(home, ".memcached-cli", "config.json") - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + configDir := filepath.Join(home, ".memcached-cli") + if err := os.MkdirAll(configDir, 0o700); err != nil { return nil, err } @@ -41,7 +43,8 @@ func newContextManager() (*contextManager, error) { mu: sync.RWMutex{}, contexts: make(map[string]*Context, 4), current: "", - path: path, + configDir: configDir, + path: "config.json", historyMaxLines: 10000, historyEnabled: true, currentClient: nil, @@ -55,19 +58,26 @@ func newContextManager() (*contextManager, error) { } func (m *contextManager) close() error { - m.save() - + saveErr := m.save() if m.currentClient != nil { - return m.currentClient.Close() + return errors.Join(saveErr, m.currentClient.Close()) } - return nil + return saveErr } // initialize loads CLI config file and initialize current client while // the current is not empty. -func (m *contextManager) initialize() error { - data, err := os.ReadFile(m.path) +func (m *contextManager) initialize() (retErr error) { + root, err := os.OpenRoot(m.configDir) + if err != nil { + return err + } + defer func() { + retErr = errors.Join(retErr, root.Close()) + }() + + data, err := root.ReadFile(m.path) if err != nil { return err } @@ -84,6 +94,7 @@ func (m *contextManager) initialize() error { } m.mu.Lock() + defer m.mu.Unlock() m.contexts = stored.Contexts m.current = stored.Current m.historyMaxLines = stored.HistoryMaxLines @@ -102,15 +113,13 @@ func (m *contextManager) initialize() error { } } - m.mu.Unlock() - return nil } // save writes contexts to disk -func (m *contextManager) save() error { - m.mu.RLock() - defer m.mu.RUnlock() +func (m *contextManager) save() (retErr error) { + m.mu.Lock() + defer m.mu.Unlock() if m.resetCurrent != nil { m.current = m.resetCurrent() @@ -134,7 +143,18 @@ func (m *contextManager) save() error { return err } - return os.WriteFile(m.path, data, 0644) + root, err := os.OpenRoot(m.configDir) + if err != nil { + return err + } + defer func() { + retErr = errors.Join(retErr, root.Close()) + }() + + if err := root.WriteFile(m.path, data, 0o600); err != nil { + return err + } + return root.Chmod(m.path, 0o600) } // addTemporaryContext creates a new temporary context for interactive use @@ -145,9 +165,8 @@ func (m *contextManager) save() error { // // The temporary context is used to store the current context name, so it can be // used to switch back to the original context when the CLI exits. -func (m *contextManager) addTemporaryContext(servers, hashStrategy string) { +func (m *contextManager) addTemporaryContext(servers, hashStrategy string) error { m.mu.Lock() - defer m.mu.Unlock() ctx := &Context{ Name: "temporary", // temporary context name @@ -157,11 +176,26 @@ func (m *contextManager) addTemporaryContext(servers, hashStrategy string) { LastUsed: time.Now(), } + originalCurrent := m.current + originalTemporary, hadTemporary := m.contexts[ctx.Name] + previousClient := m.currentClient + m.currentClient = nil m.contexts[ctx.Name] = ctx m.resetCurrent = func() string { - return m.current + if hadTemporary { + m.contexts[ctx.Name] = originalTemporary + } else { + delete(m.contexts, ctx.Name) + } + return originalCurrent } m.current = ctx.Name + m.mu.Unlock() + + if previousClient != nil { + return previousClient.Close() + } + return nil } // CreateContext creates a new context with the given name and configuration diff --git a/cmd/memcached-cli/context_manager_test.go b/cmd/memcached-cli/context_manager_test.go new file mode 100644 index 0000000..97f54fc --- /dev/null +++ b/cmd/memcached-cli/context_manager_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTemporaryContextIsNotPersisted(t *testing.T) { + t.Parallel() + + configDir := t.TempDir() + manager := &contextManager{ + contexts: map[string]*Context{ + "saved": {Name: "saved", Servers: "saved.example:11211"}, + }, + current: "saved", + configDir: configDir, + path: "config.json", + historyMaxLines: 100, + historyEnabled: true, + } + + require.NoError(t, manager.addTemporaryContext("temporary.example:11211", "rendezvous")) + assert.Equal(t, "temporary", manager.current) + require.NoError(t, manager.save()) + + data, err := os.ReadFile(filepath.Join(configDir, "config.json")) + require.NoError(t, err) + var stored struct { + Current string `json:"current"` + Contexts map[string]*Context `json:"contexts"` + } + require.NoError(t, json.Unmarshal(data, &stored)) + assert.Equal(t, "saved", stored.Current) + assert.Contains(t, stored.Contexts, "saved") + assert.NotContains(t, stored.Contexts, "temporary") +} diff --git a/cmd/memcached-cli/errors.go b/cmd/memcached-cli/errors.go new file mode 100644 index 0000000..5cdae31 --- /dev/null +++ b/cmd/memcached-cli/errors.go @@ -0,0 +1,14 @@ +package main + +import "errors" + +// rootErr returns the innermost error in an unwrap chain. +func rootErr(err error) error { + for { + next := errors.Unwrap(err) + if next == nil { + return err + } + err = next + } +} diff --git a/cmd/memcached-cli/go.mod b/cmd/memcached-cli/go.mod index 608f5ab..1f7fa9a 100644 --- a/cmd/memcached-cli/go.mod +++ b/cmd/memcached-cli/go.mod @@ -1,29 +1,53 @@ module github.com/yeqown/memcached/cmd/memcached-cli -go 1.26.0 +go 1.26.5 require ( + charm.land/bubbles/v2 v2.1.1 + charm.land/bubbletea/v2 v2.0.8 github.com/MakeNowJust/heredoc v1.0.0 - github.com/c-bata/go-prompt v0.2.6 - github.com/pkg/errors v0.9.1 - github.com/samber/lo v1.49.1 - github.com/spf13/cobra v1.9.1 - github.com/yeqown/log v1.1.1 - github.com/yeqown/memcached v1.3.4 + github.com/samber/lo v1.53.0 + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 + github.com/yeqown/log v1.3.1 + github.com/yeqown/memcached v1.4.0 ) require ( - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect + charm.land/lipgloss/v2 v2.0.4 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mattn/go-tty v0.0.3 // indirect - github.com/pkg/term v1.2.0-beta.2 // indirect + github.com/klauspost/compress v1.19.2 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/pierrec/lz4/v4 v4.1.28 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/stretchr/testify v1.11.1 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.23.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/trace v1.45.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.39.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/yeqown/memcached => ../.. diff --git a/cmd/memcached-cli/go.sum b/cmd/memcached-cli/go.sum index d924dca..aec5b83 100644 --- a/cmd/memcached-cli/go.sum +++ b/cmd/memcached-cli/go.sum @@ -1,70 +1,108 @@ +charm.land/bubbles/v2 v2.1.1 h1:7r55WzBxpo/R3z98hGmY7KKPd3ET6vsf0Fb9sDHOV60= +charm.land/bubbles/v2 v2.1.1/go.mod h1:GE6M31gaWZVXzGw73OeuTTgy4lX+OtkH0E5ymnNsHxo= +charm.land/bubbletea/v2 v2.0.8 h1:SxTJMhCAI3lbPmy4SgX5LWZ24AdINr4I6UEqzZvYJuY= +charm.land/bubbletea/v2 v2.0.8/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss= +charm.land/lipgloss/v2 v2.0.4 h1:lcPeVtcp23SNra7lHy8iYE4UC2aIipVQ47sbGyyxR5Q= +charm.land/lipgloss/v2 v2.0.4/go.mod h1:0653x8epbZSzdDfO/XPS1a/uYPOBeSsCssOpJOqDzik= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= -github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= -github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/pierrec/lz4/v4 v4.1.28 h1:pPEPwRJ4kybBTfGt28q7lQsRJQHhC08axprdLD5Ppio= +github.com/pierrec/lz4/v4 v4.1.28/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= -github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= -github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/yeqown/log v1.1.1 h1:DKw/ZeyJUTWPh4MDxYKXbBQeKHZJ69lndancmcQoERM= -github.com/yeqown/log v1.1.1/go.mod h1:RTslXFTg+8Uj5AizIxdfichvBZi/OOKao6yP3tMtTns= -github.com/yeqown/memcached v1.3.4 h1:v0/ZuojwP3u+3DtXXKjB/NInM8Ob/I3okh/6yQzFAFk= -github.com/yeqown/memcached v1.3.4/go.mod h1:1NndLxAOAhl9ecmycI8wIv4nIv+fcpAUN0vLlqM7jn4= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yeqown/log v1.3.1 h1:Omu/TAw/oMMW4aHBOOHrtEzYDxYE/T1TZVP1H/LqYNI= +github.com/yeqown/log v1.3.1/go.mod h1:RTslXFTg+8Uj5AizIxdfichvBZi/OOKao6yP3tMtTns= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/cmd/memcached-cli/history.go b/cmd/memcached-cli/history.go index be58db1..26f997e 100644 --- a/cmd/memcached-cli/history.go +++ b/cmd/memcached-cli/history.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "errors" "fmt" "os" "path/filepath" @@ -18,31 +19,42 @@ type kvCommandHistory struct { } type kvCommandHistoryManager struct { - path string maxSize int disabled bool file *os.File // 保持文件句柄打开 } +const maxHistoryRecordSize = 2 * 1024 * 1024 + func newHistoryManager(enabled bool, maxLines int) (*kvCommandHistoryManager, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } - path := filepath.Join(home, ".memcached-cli", ".history") - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + historyDir := filepath.Join(home, ".memcached-cli") + if err := os.MkdirAll(historyDir, 0o700); err != nil { return nil, err } - // 以追加模式打开文件 - file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644) + root, err := os.OpenRoot(historyDir) if err != nil { return nil, err } + // 以追加模式打开文件 + file, err := root.OpenFile(".history", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return nil, errors.Join(err, root.Close()) + } + if err := root.Close(); err != nil { + return nil, errors.Join(err, file.Close()) + } + if err := file.Chmod(0o600); err != nil { + return nil, errors.Join(err, file.Close()) + } + hm := &kvCommandHistoryManager{ - path: path, maxSize: maxLines, disabled: !enabled, file: file, @@ -80,10 +92,10 @@ func (hm *kvCommandHistoryManager) addRecord(cmd string, args []string) error { return hm.file.Sync() } -func (hm *kvCommandHistoryManager) search(keyword, since, until string, limit int) []kvCommandHistory { +func (hm *kvCommandHistoryManager) search(keyword, since, until string, limit int) ([]kvCommandHistory, error) { if hm == nil || hm.disabled || hm.file == nil { fmt.Println("History is not available.") - return []kvCommandHistory{} + return []kvCommandHistory{}, nil } now := time.Now() @@ -103,10 +115,11 @@ func (hm *kvCommandHistoryManager) search(keyword, since, until string, limit in var results []kvCommandHistory scanner := bufio.NewScanner(hm.file) + scanner.Buffer(make([]byte, 64*1024), maxHistoryRecordSize) // 重置文件指针到开始位置 if _, err := hm.file.Seek(0, 0); err != nil { - return nil + return nil, err } logger.Debugf("searching history, keyword=%s, since=%s(%d), until=%s(%d), limit=%d", keyword, since, startTime, until, endTime, limit) @@ -157,6 +170,10 @@ func (hm *kvCommandHistoryManager) search(keyword, since, until string, limit in results = append(results, history) } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("scan history: %w", err) + } + // 按时间戳倒序排序 sort.Slice(results, func(i, j int) bool { return results[i].Timestamp > results[j].Timestamp @@ -167,5 +184,5 @@ func (hm *kvCommandHistoryManager) search(keyword, since, until string, limit in results = results[:limit] } - return results + return results, nil } diff --git a/cmd/memcached-cli/history_test.go b/cmd/memcached-cli/history_test.go new file mode 100644 index 0000000..dda7533 --- /dev/null +++ b/cmd/memcached-cli/history_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "bytes" + "os" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHistorySearchSupportsRecordsLargerThanScannerDefault(t *testing.T) { + t.Parallel() + + file, err := os.CreateTemp(t.TempDir(), "history-*") + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, file.Close()) }) + + value := strings.Repeat("x", 128*1024) + _, err = file.WriteString("1 set key " + value + "\n") + require.NoError(t, err) + + manager := &kvCommandHistoryManager{file: file} + results, err := manager.search("", "", "", 10) + + require.NoError(t, err) + require.Len(t, results, 1) + assert.Equal(t, "set", results[0].Command) + assert.Equal(t, "key "+value, results[0].Args) +} + +func TestHistorySearchReportsReadErrors(t *testing.T) { + t.Parallel() + + file, err := os.CreateTemp(t.TempDir(), "history-*") + require.NoError(t, err) + require.NoError(t, file.Close()) + + manager := &kvCommandHistoryManager{file: file} + _, err = manager.search("", "", "", 10) + assert.Error(t, err) +} + +func TestRecordHistoryWarnsWithoutFailingCommand(t *testing.T) { + t.Parallel() + + file, err := os.CreateTemp(t.TempDir(), "history-*") + require.NoError(t, err) + require.NoError(t, file.Close()) + + var stderr bytes.Buffer + cmd := &cobra.Command{} + cmd.SetErr(&stderr) + recordHistory(cmd, &kvCommandHistoryManager{file: file}, "set", []string{"key", "value"}) + + assert.Contains(t, stderr.String(), "Warning: failed to record set command in history") +} diff --git a/cmd/memcached-cli/main.go b/cmd/memcached-cli/main.go index ac570ca..93c1ecd 100644 --- a/cmd/memcached-cli/main.go +++ b/cmd/memcached-cli/main.go @@ -2,14 +2,13 @@ package main import ( "context" + "errors" "fmt" "os" "strings" "time" "github.com/MakeNowJust/heredoc" - prompt "github.com/c-bata/go-prompt" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/yeqown/log" ) @@ -18,9 +17,7 @@ const ( version = "v1.3.1" ) -var ( - logger = newLogger() -) +var logger = newLogger() func main() { var ( @@ -73,7 +70,7 @@ func main() { } } -func runAsREPL(timeout time.Duration, servers, hashStrategy string) error { +func runAsREPL(timeout time.Duration, servers, hashStrategy string) (retErr error) { fmt.Println(heredoc.Doc(` Welcome to memcached-cli Type 'help' to see available commands @@ -85,13 +82,22 @@ func runAsREPL(timeout time.Duration, servers, hashStrategy string) error { manager, err := newContextManager() if err != nil { - return errors.Wrap(err, "failed to create context manager") + return fmt.Errorf("failed to create context manager: %w", err) } + defer func() { + var historyErr error + if history := manager.getHistoryManager(); history != nil { + historyErr = history.close() + } + retErr = errors.Join(retErr, historyErr, manager.close()) + }() // if servers are not empty, create a temporary context if servers = strings.TrimSpace(servers); servers != "" { logger.Debugf("adding servers: %v to temporary context as 'temporary'", servers) - manager.addTemporaryContext(servers, hashStrategy) + if err := manager.addTemporaryContext(servers, hashStrategy); err != nil { + return fmt.Errorf("failed to switch to temporary context: %w", err) + } } contexts := manager.listContexts() @@ -103,9 +109,9 @@ func runAsREPL(timeout time.Duration, servers, hashStrategy string) error { `)) } - current, err := manager.getCurrentContext() - if err != nil { - return errors.Wrap(err, "failed to get current context") + var current *Context + if len(contexts) > 0 { + current, _ = manager.getCurrentContext() } if len(contexts) > 0 && current == nil { @@ -127,16 +133,7 @@ func runAsREPL(timeout time.Duration, servers, hashStrategy string) error { return err } - p := prompt.New( - repl.commandExecutor, - repl.commandCompleter, - prompt.OptionTitle("memcached-cli"), - prompt.OptionPrefix(">>> "), - prompt.OptionInputTextColor(prompt.Yellow), - ) - - p.Run() - return nil + return runREPLTUI(repl) } func newVersionCommand() *cobra.Command { @@ -180,7 +177,6 @@ func newContextCommand() *cobra.Command { } func newKVCommand() *cobra.Command { - var contextName string cmd := &cobra.Command{ @@ -256,7 +252,10 @@ func newHistoryCommand() *cobra.Command { }, RunE: func(cmd *cobra.Command, args []string) error { history := getContextManager(cmd, false).getHistoryManager() - results := history.search(keyword, since, until, limit) + results, err := history.search(keyword, since, until, limit) + if err != nil { + return fmt.Errorf("search history: %w", err) + } if len(results) == 0 { fmt.Println("No history found.") diff --git a/cmd/memcached-cli/repl.go b/cmd/memcached-cli/repl.go index f74891e..ac2c98d 100644 --- a/cmd/memcached-cli/repl.go +++ b/cmd/memcached-cli/repl.go @@ -1,21 +1,21 @@ package main import ( + "bytes" "context" "fmt" - "os" + "io" "strconv" "strings" "time" - "github.com/c-bata/go-prompt" - "github.com/pkg/errors" "github.com/yeqown/memcached" ) type replCommander struct { cm *contextManager timeout time.Duration + out io.Writer } func newREPLCommander(manager *contextManager, timeout time.Duration) (*replCommander, error) { @@ -26,45 +26,53 @@ func newREPLCommander(manager *contextManager, timeout time.Duration) (*replComm return &replCommander{ cm: manager, timeout: timeout, + out: io.Discard, }, nil } -func (r *replCommander) commandCompleter(d prompt.Document) []prompt.Suggest { - suggestions := []prompt.Suggest{ - // context operations - {Text: "use", Description: "Switch to a different context"}, - {Text: "list", Description: "List all contexts"}, - {Text: "current", Description: "Show current context"}, - // key-value operations - {Text: "get", Description: "Get value by key"}, - {Text: "gets", Description: "Get multiple values by keys"}, - {Text: "set", Description: "Set key to value"}, - {Text: "delete", Description: "Delete key"}, - {Text: "incr", Description: "Increment value"}, - {Text: "decr", Description: "Decrement value"}, - {Text: "touch", Description: "Update expiration time"}, - // other - {Text: "version", Description: "Show version information"}, - {Text: "help", Description: "Show help message"}, - {Text: "exit", Description: "Exit the program"}, - {Text: "quit", Description: "Exit the program"}, - } - - sub := d.GetWordBeforeCursor() - - if sub == "" { - return []prompt.Suggest{} - } - - return prompt.FilterHasPrefix(suggestions, sub, true) +var replCommands = []string{ + "use", "list", "current", + "get", "gets", "set", "delete", "incr", "decr", "touch", + "version", "help", "exit", "quit", } -func (r *replCommander) commandExecutor(line string) { +// completeREPLCommands returns command suggestions matching the first-token prefix. +func completeREPLCommands(line string) []string { + fields := strings.Fields(line) + if len(fields) == 0 || strings.HasSuffix(line, " ") { + return nil + } + if len(fields) != 1 { + return nil + } + + prefix := strings.ToLower(fields[0]) + matches := make([]string, 0, len(replCommands)) + for _, cmd := range replCommands { + if strings.HasPrefix(cmd, prefix) { + matches = append(matches, cmd) + } + } + return matches +} + +type commandResult struct { + output string + quit bool +} + +// executeCommand runs a REPL command and captures its output. +func (r *replCommander) executeCommand(line string) commandResult { line = strings.TrimSpace(line) if line == "" { - return + return commandResult{} } + var buf bytes.Buffer + prev := r.out + r.out = &buf + defer func() { r.out = prev }() + args := strings.Fields(line) cmd := args[0] @@ -72,6 +80,7 @@ func (r *replCommander) commandExecutor(line string) { ctx, cancel := context.WithTimeout(context.Background(), r.timeout) defer cancel() + quit := false switch cmd { case "use": err = r.handleUse(ctx, args) @@ -79,7 +88,6 @@ func (r *replCommander) commandExecutor(line string) { err = r.handleList(ctx) case "current": err = r.handleCurrent(ctx) - case "get": err = r.handleGet(ctx, args) case "gets": @@ -94,36 +102,41 @@ func (r *replCommander) commandExecutor(line string) { err = r.handleDecr(ctx, args) case "touch": err = r.handleTouch(ctx, args) - case "version": err = r.handleVersion(ctx) case "help": - r.handleHelp() + err = r.handleHelp() case "exit", "quit": - r.handleExit() + err = r.handleExit() + quit = true default: - fmt.Printf("Unknown command: %s\n", cmd) + err = writeOutput(r.out, "Unknown command: %s\n", cmd) } if err != nil { - fmt.Printf("Execution `%s` failed: %v\n", cmd, err) + if writeErr := writeOutput(r.out, "Execution `%s` failed: %v\n", cmd, err); writeErr != nil { + return commandResult{ + output: fmt.Sprintf("Execution `%s` failed: %v (writing output failed: %v)\n", cmd, err, writeErr), + quit: quit, + } + } } -} -func (r *replCommander) getMemcachedClient() memcached.Client { - client, err := r.cm.getCurrentClient() - if err != nil { - panic(err) + return commandResult{ + output: buf.String(), + quit: quit, } +} - return client +func (r *replCommander) getMemcachedClient() (memcached.Client, error) { + return r.cm.getCurrentClient() } /** * context operations */ -func (r *replCommander) handleUse(ctx context.Context, args []string) error { +func (r *replCommander) handleUse(_ context.Context, args []string) error { if len(args) != 2 { return fmt.Errorf("usage: use ") } @@ -134,9 +147,13 @@ func (r *replCommander) handleList(_ context.Context) error { ctx, _ := r.cm.getCurrentContext() for _, name := range r.cm.listContexts() { if ctx != nil && ctx.Name == name { - fmt.Printf("* %s\n", name) + if err := writeOutput(r.out, "* %s\n", name); err != nil { + return err + } } else { - fmt.Printf(" %s\n", name) + if err := writeOutput(r.out, " %s\n", name); err != nil { + return err + } } } return nil @@ -147,9 +164,7 @@ func (r *replCommander) handleCurrent(_ context.Context) error { if err != nil { return err } - fmt.Printf("Current context: %s\n", ctx.Name) - fmt.Printf("Servers: %s\n", ctx.Servers) - return nil + return writeOutput(r.out, "Current context: %s\nServers: %s\n", ctx.Name, ctx.Servers) } /** @@ -161,7 +176,11 @@ func (r *replCommander) handleGet(ctx context.Context, args []string) error { return fmt.Errorf("usage: get ") } - item, err := r.getMemcachedClient().MetaGet( + client, err := r.getMemcachedClient() + if err != nil { + return err + } + item, err := client.MetaGet( ctx, []byte(args[1]), memcached.MetaGetFlagReturnTTL(), @@ -174,10 +193,9 @@ func (r *replCommander) handleGet(ctx context.Context, args []string) error { memcached.MetaGetFlagReturnHitBefore(), ) if err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(r.out, err) } - printMetaItem(item) - return nil + return writeMetaItem(r.out, item) } func (r *replCommander) handleSet(ctx context.Context, args []string) error { @@ -192,11 +210,15 @@ func (r *replCommander) handleSet(ctx context.Context, args []string) error { } } - if err := r.getMemcachedClient().Set(ctx, args[1], []byte(args[2]), magicFlags, expiration); err != nil { - return ignoreMemcachedError(err) + client, err := r.getMemcachedClient() + if err != nil { + return err } - fmt.Println("OK") - return nil + if err := client.Set(ctx, args[1], []byte(args[2]), magicFlags, expiration); err != nil { + return ignoreMemcachedError(r.out, err) + } + _, err = fmt.Fprintln(r.out, "OK") + return err } func (r *replCommander) handleDelete(ctx context.Context, args []string) error { @@ -204,11 +226,15 @@ func (r *replCommander) handleDelete(ctx context.Context, args []string) error { return fmt.Errorf("usage: delete ") } - if err := r.getMemcachedClient().Delete(ctx, args[1]); err != nil { - return ignoreMemcachedError(err) + client, err := r.getMemcachedClient() + if err != nil { + return err } - fmt.Println("OK") - return nil + if err := client.Delete(ctx, args[1]); err != nil { + return ignoreMemcachedError(r.out, err) + } + _, err = fmt.Fprintln(r.out, "OK") + return err } func (r *replCommander) handleIncr(ctx context.Context, args []string) error { @@ -222,12 +248,15 @@ func (r *replCommander) handleIncr(ctx context.Context, args []string) error { delta = d } } - newValue, err := r.getMemcachedClient().Incr(ctx, args[1], delta) + client, err := r.getMemcachedClient() if err != nil { - return ignoreMemcachedError(err) + return err } - fmt.Printf("%d\n", newValue) - return nil + newValue, err := client.Incr(ctx, args[1], delta) + if err != nil { + return ignoreMemcachedError(r.out, err) + } + return writeOutput(r.out, "%d\n", newValue) } func (r *replCommander) handleDecr(ctx context.Context, args []string) error { @@ -241,12 +270,15 @@ func (r *replCommander) handleDecr(ctx context.Context, args []string) error { delta = d } } - newValue, err := r.getMemcachedClient().Decr(ctx, args[1], delta) + client, err := r.getMemcachedClient() + if err != nil { + return err + } + newValue, err := client.Decr(ctx, args[1], delta) if err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(r.out, err) } - fmt.Printf("%d\n", newValue) - return nil + return writeOutput(r.out, "%d\n", newValue) } func (r *replCommander) handleTouch(ctx context.Context, args []string) error { @@ -258,24 +290,31 @@ func (r *replCommander) handleTouch(ctx context.Context, args []string) error { if err != nil { return fmt.Errorf("invalid expiration format: %v", err) } - if err := r.getMemcachedClient().Touch(ctx, args[1], time.Duration(expiration)*time.Second); err != nil { - return ignoreMemcachedError(err) + client, err := r.getMemcachedClient() + if err != nil { + return err } - fmt.Println("OK") - return nil + if err := client.Touch(ctx, args[1], time.Duration(expiration)*time.Second); err != nil { + return ignoreMemcachedError(r.out, err) + } + return writeOutput(r.out, "OK\n") } func (r *replCommander) handleMGet(ctx context.Context, args []string) error { if len(args) < 2 { - return fmt.Errorf("usage: mget [key2 ...]") + return fmt.Errorf("usage: gets [key2 ...]") } keys := make([]string, len(args)-1) copy(keys, args[1:]) items := make([]*memcached.MetaItem, 0, len(keys)) + client, err := r.getMemcachedClient() + if err != nil { + return err + } for _, key := range keys { - item, err := r.getMemcachedClient().MetaGet( + item, err := client.MetaGet( ctx, []byte(key), memcached.MetaGetFlagReturnTTL(), @@ -288,16 +327,16 @@ func (r *replCommander) handleMGet(ctx context.Context, args []string) error { memcached.MetaGetFlagReturnHitBefore(), ) if err != nil { - fmt.Printf("Encounter error while getting key '%s': %v\n", key, errors.Cause(err)) + if err := writeOutput(r.out, "Encounter error while getting key '%s': %v\n", key, rootErr(err)); err != nil { + return err + } continue } items = append(items, item) } - printMetaItems(items) - - return nil + return writeMetaItems(r.out, items) } /** @@ -305,29 +344,31 @@ func (r *replCommander) handleMGet(ctx context.Context, args []string) error { */ func (r *replCommander) handleVersion(_ context.Context) error { - fmt.Printf("Version: %s\n", version) - return nil + return writeOutput(r.out, "Version: %s\n", version) +} + +func (r *replCommander) handleHelp() error { + return writeOutput(r.out, `Available commands: + use Switch to a different context + list List all contexts + current Show current context + get Get value by key + gets Get multiple values by keys + set Set key to value + delete Delete key + incr [delta] Increment value + decr [delta] Decrement value + touch Update expiration time + help Show this help message + exit, quit Exit the program +`) } -func (r *replCommander) handleHelp() { - fmt.Println("Available commands:") - fmt.Println(" use Switch to a different context") - fmt.Println(" list List all contexts") - fmt.Println(" current Show current context") - - fmt.Println(" get Get value by key") - fmt.Println(" gets Get multiple values by keys") - fmt.Println(" set Set key to value") - fmt.Println(" delete Delete key") - fmt.Println(" incr [delta] Increment value") - fmt.Println(" decr [delta] Decrement value") - fmt.Println(" touch Update expiration time") - - fmt.Println(" help Show this help message") - fmt.Println(" exit, quit Exit the program") +func (r *replCommander) handleExit() error { + return writeOutput(r.out, "Bye!\n") } -func (r *replCommander) handleExit() { - fmt.Println("Bye!") - os.Exit(0) +func writeOutput(w io.Writer, format string, args ...any) error { + _, err := fmt.Fprintf(w, format, args...) + return err } diff --git a/cmd/memcached-cli/repl_test.go b/cmd/memcached-cli/repl_test.go new file mode 100644 index 0000000..c836290 --- /dev/null +++ b/cmd/memcached-cli/repl_test.go @@ -0,0 +1,220 @@ +package main + +import ( + "bytes" + "fmt" + "testing" + + tea "charm.land/bubbletea/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yeqown/memcached" +) + +func TestCompleteREPLCommands(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + line string + want []string + }{ + {name: "empty", line: "", want: nil}, + {name: "whitespace", line: " ", want: nil}, + {name: "known prefix", line: "ge", want: []string{"get", "gets"}}, + {name: "exact command", line: "help", want: []string{"help"}}, + {name: "unknown prefix", line: "zzz", want: []string{}}, + {name: "after first token", line: "get foo", want: nil}, + {name: "trailing space", line: "get ", want: nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := completeREPLCommands(tt.line) + if tt.want == nil { + assert.Nil(t, got) + return + } + assert.Equal(t, tt.want, got) + }) + } +} + +func TestExecuteCommandHelpAndQuit(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + + help := repl.executeCommand("help") + assert.False(t, help.quit) + assert.Contains(t, help.output, "Available commands:") + assert.Contains(t, help.output, "exit, quit") + + quit := repl.executeCommand("exit") + assert.True(t, quit.quit) + assert.Contains(t, quit.output, "Bye!") +} + +func TestExecuteCommandEmptyAndUnknown(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + + empty := repl.executeCommand(" ") + assert.False(t, empty.quit) + assert.Empty(t, empty.output) + + unknown := repl.executeCommand("nope") + assert.False(t, unknown.quit) + assert.Contains(t, unknown.output, "Unknown command: nope") +} + +func TestExecuteCommandWithoutContextReturnsError(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(&contextManager{contexts: map[string]*Context{}}, 0) + require.NoError(t, err) + + result := repl.executeCommand("get missing") + assert.False(t, result.quit) + assert.Contains(t, result.output, "Execution `get` failed: no context selected") +} + +func TestIgnoreMemcachedErrorWritesToProvidedWriter(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + err := ignoreMemcachedError(&output, fmt.Errorf("request failed: %w", memcached.ErrNotFound)) + + require.NoError(t, err) + assert.Contains(t, output.String(), "Memcached Error:") + assert.Contains(t, output.String(), memcached.ErrNotFound.Error()) +} + +func TestREPLModelEnterRunsCommand(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + m := newREPLModel(repl) + m.input.SetValue("help") + + next, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + require.NotNil(t, cmd) + updated := next.(replModel) + assert.True(t, updated.busy) + + msg := cmd() + result, ok := msg.(commandResultMsg) + require.True(t, ok) + assert.Contains(t, result.output, "Available commands:") + assert.False(t, result.quit) + + next, cmd = updated.Update(result) + updated = next.(replModel) + assert.False(t, updated.busy) + assert.Empty(t, updated.input.Value()) + require.NotNil(t, cmd) +} + +func TestREPLModelHistoryNavigation(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + m := newREPLModel(repl) + m.appendHistory("help") + m.appendHistory("version") + m.input.SetValue("draft") + + m.historyUp() + assert.Equal(t, "version", m.input.Value()) + m.historyUp() + assert.Equal(t, "help", m.input.Value()) + m.historyDown() + assert.Equal(t, "version", m.input.Value()) + m.historyDown() + assert.Equal(t, "draft", m.input.Value()) + assert.Equal(t, -1, m.histIdx) +} + +func TestREPLModelCtrlCQuits(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + m := newREPLModel(repl) + + _, cmd := m.Update(tea.KeyPressMsg{ + Code: 'c', + Mod: tea.ModCtrl, + }) + require.NotNil(t, cmd) +} + +func TestREPLModelCtrlDQuitsWhenEmpty(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + m := newREPLModel(repl) + + _, cmd := m.Update(tea.KeyPressMsg{ + Code: 'd', + Mod: tea.ModCtrl, + }) + require.NotNil(t, cmd) +} + +func TestREPLModelBusyIgnoresTyping(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + m := newREPLModel(repl) + m.busy = true + m.input.SetValue("help") + + next, cmd := m.Update(tea.KeyPressMsg{Code: 'a', Text: "a"}) + updated := next.(replModel) + assert.True(t, updated.busy) + assert.Equal(t, "help", updated.input.Value()) + assert.Nil(t, cmd) +} + +func TestREPLModelExitCommandQuits(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + m := newREPLModel(repl) + m.input.SetValue("quit") + + next, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + updated := next.(replModel) + require.NotNil(t, cmd) + assert.True(t, updated.busy) + + result := cmd().(commandResultMsg) + assert.True(t, result.quit) + assert.Contains(t, result.output, "Bye!") + + _, cmd = updated.Update(result) + require.NotNil(t, cmd) +} + +func TestREPLModelViewBusy(t *testing.T) { + t.Parallel() + + repl, err := newREPLCommander(nil, 0) + require.NoError(t, err) + m := newREPLModel(repl) + m.busy = true + + view := m.View() + assert.Contains(t, view.Content, "running") + assert.False(t, view.AltScreen) +} diff --git a/cmd/memcached-cli/repl_tui.go b/cmd/memcached-cli/repl_tui.go new file mode 100644 index 0000000..65596da --- /dev/null +++ b/cmd/memcached-cli/repl_tui.go @@ -0,0 +1,175 @@ +package main + +import ( + "strings" + + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" +) + +type commandResultMsg commandResult + +type replModel struct { + repl *replCommander + input textinput.Model + busy bool + history []string + histIdx int // -1 means browsing is inactive + draft string + width int +} + +func newREPLModel(repl *replCommander) replModel { + ti := textinput.New() + ti.Prompt = ">>> " + ti.ShowSuggestions = true + ti.SetSuggestions(replCommands) + ti.SetVirtualCursor(false) + ti.Focus() + ti.CharLimit = 0 + ti.SetWidth(80) + + return replModel{ + repl: repl, + input: ti, + histIdx: -1, + width: 80, + } +} + +func runREPLTUI(repl *replCommander) error { + p := tea.NewProgram(newREPLModel(repl)) + _, err := p.Run() + return err +} + +func (m replModel) Init() tea.Cmd { + return textinput.Blink +} + +func (m replModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + if m.width > 4 { + m.input.SetWidth(m.width - 4) + } + return m, nil + + case commandResultMsg: + m.busy = false + m.input.SetValue("") + m.input.Focus() + cmds := make([]tea.Cmd, 0, 2) + if msg.output != "" { + cmds = append(cmds, tea.Println(strings.TrimRight(msg.output, "\n"))) + } + if msg.quit { + cmds = append(cmds, tea.Quit) + return m, tea.Batch(cmds...) + } + return m, tea.Batch(cmds...) + + case tea.KeyPressMsg: + if m.busy { + if msg.String() == "ctrl+c" { + return m, tea.Batch(tea.Println("Bye!"), tea.Quit) + } + return m, nil + } + + switch msg.String() { + case "ctrl+c": + return m, tea.Batch(tea.Println("Bye!"), tea.Quit) + case "ctrl+d": + if m.input.Value() == "" { + return m, tea.Batch(tea.Println("Bye!"), tea.Quit) + } + case "enter": + line := strings.TrimSpace(m.input.Value()) + if line == "" { + return m, nil + } + m.appendHistory(line) + m.busy = true + m.input.Blur() + return m, m.runCommand(line) + case "up": + m.historyUp() + return m, nil + case "down": + m.historyDown() + return m, nil + } + } + + if m.busy { + return m, nil + } + + var cmd tea.Cmd + m.input, cmd = m.input.Update(msg) + return m, cmd +} + +func (m replModel) View() tea.View { + content := m.input.View() + if m.busy { + content = "… running" + } + + v := tea.NewView(content) + if !m.busy { + if c := m.input.Cursor(); c != nil { + v.Cursor = c + } + } + return v +} + +func (m *replModel) appendHistory(line string) { + if n := len(m.history); n > 0 && m.history[n-1] == line { + m.histIdx = -1 + m.draft = "" + return + } + m.history = append(m.history, line) + m.histIdx = -1 + m.draft = "" +} + +func (m *replModel) historyUp() { + if len(m.history) == 0 { + return + } + if m.histIdx == -1 { + m.draft = m.input.Value() + m.histIdx = len(m.history) - 1 + } else if m.histIdx > 0 { + m.histIdx-- + } + m.input.SetValue(m.history[m.histIdx]) + m.input.CursorEnd() +} + +func (m *replModel) historyDown() { + if m.histIdx == -1 { + return + } + if m.histIdx < len(m.history)-1 { + m.histIdx++ + m.input.SetValue(m.history[m.histIdx]) + m.input.CursorEnd() + return + } + m.histIdx = -1 + m.input.SetValue(m.draft) + m.input.CursorEnd() +} + +func (m replModel) runCommand(line string) tea.Cmd { + repl := m.repl + return func() tea.Msg { + return commandResultMsg(repl.executeCommand(line)) + } +} diff --git a/conn.go b/conn.go index f5fc7d9..15e8cbc 100644 --- a/conn.go +++ b/conn.go @@ -3,14 +3,14 @@ package memcached import ( "bufio" "context" + "errors" + "fmt" "io" "net" "strconv" "sync" "sync/atomic" "time" - - "github.com/pkg/errors" ) type nowFuncType func() time.Time @@ -121,7 +121,7 @@ type conn struct { func newConnContext(ctx context.Context, addr *Addr, dialTimeout time.Duration) (*conn, error) { rawConn, err := addr.dial(ctx, dialTimeout) if err != nil { - return nil, errors.Wrap(err, "dialContext") + return nil, fmt.Errorf("dialContext: %w", err) } cn := &conn{ @@ -244,6 +244,8 @@ func (c *conn) release() error { return c.pool.put(c) } +var errPoolClosed = errors.New("connection pool is closed") + // The connPool holds a pool of connections to one memcached server instance // and provides a way to get a connection from the pool. // @@ -268,7 +270,10 @@ type connPool struct { numOpen atomic.Int32 // Indicate if the pool is closed, if true, no new connections will be created // and all existing connections will be closed. - closed bool + closed bool + // closedCh is closed exactly once when the pool closes, so waiting get() + // callers can abort without receiving from a closed idle channel. + closedCh chan struct{} cleanerCh chan struct{} maxIdleClosed int64 // the number of connections closed due to maxIdle @@ -292,6 +297,7 @@ func newConnPool( createConn: createConn, numOpen: atomic.Int32{}, closed: false, + closedCh: make(chan struct{}), cleanerCh: nil, // created when needed in startCleaner maxIdleClosed: 0, @@ -310,11 +316,26 @@ func (p *connPool) close() error { } p.closed = true - // close all existing connections, so that the get() couldn't - // create a new connection. - close(p.conns) + close(p.closedCh) + + // Drain idle connections without closing the idle channel itself. + // Closing the channel would race with lock-free get() receives. + for { + select { + case cn := <-p.conns: + _ = cn.Close() + p.numOpen.Add(-1) + default: + goto drained + } + } +drained: + if p.cleanerCh != nil { - p.cleanerCh <- struct{}{} + select { + case p.cleanerCh <- struct{}{}: + default: + } } p.mu.Unlock() @@ -322,39 +343,48 @@ func (p *connPool) close() error { } func (p *connPool) get(ctx context.Context) (memcachedConn, error) { - if p.closed { - return nil, errors.New("connection pool is closed") - } - - // try to get a connection from the pool first if there is any + // Try to get an idle connection first if there is any, // otherwise create a new connection. select { case cn := <-p.conns: return cn, nil default: - p.mu.Lock() - // no available connection, check if we can create a new one. - if int(p.numOpen.Load()) >= p.maxConns { - p.mu.Unlock() - // the pool is full, wait for a connection to be returned - select { - case cn := <-p.conns: - return cn, nil - case <-ctx.Done(): - return nil, ctx.Err() - } - } - p.mu.Unlock() + } - cn, err := p.createConn(ctx) - if err != nil { - return nil, err + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return nil, errPoolClosed + } + // No available connection, check if we can create a new one. + if int(p.numOpen.Load()) >= p.maxConns { + p.mu.Unlock() + // The pool is full, wait for a connection to be returned or for close. + select { + case cn := <-p.conns: + return cn, nil + case <-p.closedCh: + return nil, errPoolClosed + case <-ctx.Done(): + return nil, ctx.Err() } - cn.setConnPool(p) - p.numOpen.Add(1) + } + p.mu.Unlock() - return cn, nil + cn, err := p.createConn(ctx) + if err != nil { + return nil, err } + cn.setConnPool(p) + + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return nil, errors.Join(errPoolClosed, cn.Close()) + } + p.numOpen.Add(1) + p.mu.Unlock() + return cn, nil } func (p *connPool) put(cn memcachedConn) error { @@ -408,7 +438,7 @@ func (p *connPool) connectionsCleaner(d time.Duration) { for { select { case <-t.C: - case <-p.cleanerCh: // db was closed + case <-p.cleanerCh: // pool was closed } p.mu.Lock() @@ -423,7 +453,7 @@ func (p *connPool) connectionsCleaner(d time.Duration) { return } - // make a copy of the connections those need to be closed. + // Snapshot idle connections that need to be closed. d, closing := p.connectionCleanerRunLocked(d) p.mu.Unlock() @@ -461,55 +491,65 @@ func (p *connPool) shortestIdleTimeLocked() time.Duration { // // 1. if the connection is expired (exceeds maxLifeTime since created). // 2. if the connection idle time exceeds the idle connection limit(maxIdleTime). +// +// The idle channel is kept intact: currently available connections are drained +// with non-blocking receives, filtered in one pass, and survivors are put back. func (p *connPool) connectionCleanerRunLocked(d time.Duration) (time.Duration, []memcachedConn) { var idleClosing int64 closing := make([]memcachedConn, 0, p.maxIdle/2) - newConns := make(chan memcachedConn, p.maxConns) - - if p.maxIdleTime > 0 { - idleSince := nowFunc().Add(-p.maxIdleTime) - close(p.conns) - // TODO(@yeqown): could we optimize the loop here? - // we re-allocate the newConns channel every time we clean up the connections. - for c := range p.conns { - if d2, ok := c.idle(idleSince); !ok { - if d2 < d { - // Ensure idle connections are cleaned up as soon - // as possible. - d = d2 - } - newConns <- c // put back - continue - } - closing = append(closing, c) - idleClosing++ + kept := make([]memcachedConn, 0, len(p.conns)) + + var idleSince, expiredSince time.Time + checkIdle := p.maxIdleTime > 0 + checkLife := p.maxLifeTime > 0 + if checkIdle { + idleSince = nowFunc().Add(-p.maxIdleTime) + } + if checkLife { + expiredSince = nowFunc().Add(-p.maxLifeTime) + } + + for { + var c memcachedConn + select { + case c = <-p.conns: + default: + goto drained } - p.conns = newConns - p.maxIdleTimeClosed += idleClosing - } - - newConns = make(chan memcachedConn, p.maxConns) - if p.maxLifeTime > 0 { - expiredSince := nowFunc().Add(-p.maxLifeTime) - close(p.conns) - for c := range p.conns { - if d2, ok := c.expired(expiredSince); !ok { - if d2 < d { - // Prevents connections staying in the pool when they - // have expired. - d = d2 - } - newConns <- c // put back - continue + shouldClose := false + if checkIdle { + if d2, ok := c.idle(idleSince); ok { + shouldClose = true + idleClosing++ + } else if d2 < d { + // Ensure idle connections are cleaned up as soon as possible. + d = d2 + } + } + if !shouldClose && checkLife { + if d2, ok := c.expired(expiredSince); ok { + shouldClose = true + } else if d2 < d { + // Prevents connections staying in the pool when they have expired. + d = d2 } + } + + if shouldClose { closing = append(closing, c) + } else { + kept = append(kept, c) } + } - p.conns = newConns - p.maxLifeTimeClosed += int64(len(closing)) - idleClosing +drained: + for _, c := range kept { + p.conns <- c } + p.maxIdleTimeClosed += idleClosing + p.maxLifeTimeClosed += int64(len(closing)) - idleClosing return d, closing } diff --git a/conn_test.go b/conn_test.go index 8db1732..823b83f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -2,11 +2,12 @@ package memcached import ( "context" + "errors" "sync" + "sync/atomic" "testing" "time" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -21,6 +22,16 @@ type mockConn struct { pool *connPool } +type closeTrackingConn struct { + *mockConn + closed atomic.Bool +} + +func (m *closeTrackingConn) Close() error { + m.closed.Store(true) + return nil +} + func newMockConn() *mockConn { return &mockConn{ createdAt: time.Now(), @@ -131,14 +142,14 @@ func Test_connPool_new(t *testing.T) { t.Run(tt.name, func(t *testing.T) { pool := newConnPool(tt.maxIdle, tt.maxConn, tt.maxLifeTime, tt.maxIdleTimeout, createConn) - // 验证基本属性 assert.NotNil(t, pool) assert.Equal(t, tt.maxIdle, pool.maxIdle) assert.Equal(t, tt.maxConn, pool.maxConns) assert.Equal(t, tt.maxIdleTimeout, pool.maxIdleTime) - assert.Equal(t, tt.maxConn, cap(pool.conns)) + assert.Equal(t, 0, pool.stats().IdleConns) assert.Equal(t, 0, int(pool.numOpen.Load())) assert.False(t, pool.closed) + assert.NotNil(t, pool.closedCh) assert.Nil(t, pool.cleanerCh) assert.Equal(t, int64(0), pool.maxIdleClosed) assert.Equal(t, int64(0), pool.maxIdleTimeClosed) @@ -172,9 +183,9 @@ func Test_connPool_get_put(t *testing.T) { wg.Wait() - // idle connections reached maxConn + // idle connections reached maxIdle assert.Equal(t, 5, int(pool.numOpen.Load())) - assert.Equal(t, 5, len(pool.conns)) + assert.Equal(t, 5, pool.stats().IdleConns) } // Test_connPool_get_timeout_case1 mocking the case that the createConn function @@ -271,7 +282,7 @@ func Test_connPool_get_oversize(t *testing.T) { wg.Wait() assert.Equal(t, 5, int(pool.numOpen.Load())) - assert.Equal(t, 5, len(pool.conns)) + assert.Equal(t, 5, pool.stats().IdleConns) ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() @@ -317,7 +328,7 @@ func Test_connPool_cleanup_maxIdle(t *testing.T) { // connections to 5. time.Sleep(3 * time.Second) assert.Equal(t, 0, int(pool.numOpen.Load())) - assert.Equal(t, 0, len(pool.conns)) + assert.Equal(t, 0, pool.stats().IdleConns) assert.False(t, pool.closed) stat = pool.stats() @@ -368,16 +379,146 @@ func Test_connPool_cleanup_maxLife(t *testing.T) { // connections to 5. time.Sleep(3 * time.Second) assert.Equal(t, 0, int(pool.numOpen.Load())) - assert.Equal(t, 0, len(pool.conns)) + assert.Equal(t, 0, pool.stats().IdleConns) assert.False(t, pool.closed) stat = pool.stats() t.Logf("After cleanup: %+v", stat) assert.Equal(t, 0, stat.TotalConns) - assert.Equal(t, 0, stat.IdleConns) // since all connections are cleaned up(idle=1s) + assert.Equal(t, 0, stat.IdleConns) // since all connections are cleaned up(life=2s) assert.Equal(t, 10, stat.MaxConns) assert.Equal(t, 5, stat.MaxIdle) assert.Equal(t, int64(5), stat.maxIdleClosed) assert.Equal(t, int64(5), stat.maxLifeTimeClosed) assert.Equal(t, int64(0), stat.maxIdleTimeClosed) } + +func Test_connectionCleanerRunLocked_both_limits(t *testing.T) { + pool := newConnPool(10, 10, time.Hour, time.Hour, createConn) + fixedNow := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + origNow := nowFunc + nowFunc = func() time.Time { return fixedNow } + t.Cleanup(func() { nowFunc = origNow }) + + idleExpired := newMockConn() + idleExpired.createdAt = fixedNow.Add(-30 * time.Minute) + idleExpired.returnedAt = fixedNow.Add(-2 * time.Hour) + + lifeExpired := newMockConn() + lifeExpired.createdAt = fixedNow.Add(-2 * time.Hour) + lifeExpired.returnedAt = fixedNow.Add(-time.Minute) + + fresh := newMockConn() + fresh.createdAt = fixedNow.Add(-time.Minute) + fresh.returnedAt = fixedNow.Add(-time.Minute) + + pool.conns <- idleExpired + pool.conns <- lifeExpired + pool.conns <- fresh + pool.numOpen.Store(3) + connsBefore := pool.conns + + pool.maxIdleTime = time.Hour + pool.maxLifeTime = time.Hour + + d, closing := pool.connectionCleanerRunLocked(time.Hour) + assert.True(t, connsBefore == pool.conns, "cleaner must keep the idle channel") + assert.Len(t, closing, 2) + assert.Equal(t, int64(1), pool.maxIdleTimeClosed) + assert.Equal(t, int64(1), pool.maxLifeTimeClosed) + assert.Equal(t, 1, len(pool.conns)) + assert.Greater(t, d, time.Duration(0)) + + kept := <-pool.conns + assert.Same(t, fresh, kept) +} + +func Test_connPool_cleanup_concurrent_get(t *testing.T) { + pool := newConnPool(8, 8, time.Hour, 100*time.Millisecond, createConn) + ctx := context.Background() + + for i := 0; i < 8; i++ { + cn, err := pool.get(ctx) + assert.NoError(t, err) + assert.NoError(t, pool.put(cn)) + } + + done := make(chan struct{}) + go func() { + defer close(done) + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + cn, err := pool.get(ctx) + if err != nil { + assert.Fail(t, "get failed during cleanup", err.Error()) + return + } + assert.NotNil(t, cn) + _ = pool.put(cn) + } + }() + + <-done + assert.NoError(t, pool.close()) + assert.Equal(t, 0, int(pool.numOpen.Load())) +} + +func Test_connPool_close_wakes_waiters(t *testing.T) { + pool := newConnPool(1, 1, time.Hour, time.Hour, createConn) + ctx := context.Background() + + cn, err := pool.get(ctx) + assert.NoError(t, err) + assert.NotNil(t, cn) + + errCh := make(chan error, 1) + go func() { + _, getErr := pool.get(ctx) + errCh <- getErr + }() + + time.Sleep(50 * time.Millisecond) + assert.NoError(t, pool.close()) + + select { + case getErr := <-errCh: + assert.ErrorIs(t, getErr, errPoolClosed) + case <-time.After(time.Second): + t.Fatal("waiting get was not woken by close") + } + + assert.NoError(t, pool.put(cn)) + assert.Equal(t, 0, int(pool.numOpen.Load())) + assert.Equal(t, 0, pool.stats().IdleConns) +} + +func Test_connPool_get_closes_connection_created_during_close(t *testing.T) { + creationStarted := make(chan struct{}) + releaseCreation := make(chan struct{}) + createdConn := &closeTrackingConn{mockConn: newMockConn()} + pool := newConnPool(1, 1, time.Hour, time.Hour, func(context.Context) (memcachedConn, error) { + close(creationStarted) + <-releaseCreation + return createdConn, nil + }) + + type getResult struct { + conn memcachedConn + err error + } + resultCh := make(chan getResult, 1) + go func() { + conn, err := pool.get(context.Background()) + resultCh <- getResult{conn: conn, err: err} + }() + + <-creationStarted + assert.NoError(t, pool.close()) + close(releaseCreation) + + result := <-resultCh + assert.Nil(t, result.conn) + assert.ErrorIs(t, result.err, errPoolClosed) + assert.True(t, createdConn.closed.Load()) + assert.Equal(t, 0, int(pool.numOpen.Load())) +} diff --git a/errors.go b/errors.go index 8dce902..4e59271 100644 --- a/errors.go +++ b/errors.go @@ -1,7 +1,7 @@ package memcached import ( - "github.com/pkg/errors" + "errors" ) var ( diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..ea140c0 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,27 @@ +package memcached + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWrappedErrorChainPreservesSentinel(t *testing.T) { + err := fmt.Errorf("request failed: %w", fmt.Errorf("send failed: %w", ErrNotFound)) + assert.True(t, errors.Is(err, ErrNotFound)) + assert.Contains(t, err.Error(), "request failed") + assert.Contains(t, err.Error(), "send failed") +} + +func TestErrorsJoinPreservesJoinedSentinels(t *testing.T) { + joined := errors.Join( + fmt.Errorf("node-a: %w", ErrNotFound), + fmt.Errorf("node-b: %w", ErrServerError), + ) + require.Error(t, joined) + assert.True(t, errors.Is(joined, ErrNotFound)) + assert.True(t, errors.Is(joined, ErrServerError)) +} diff --git a/example/cas.go b/example/cas/main.go similarity index 100% rename from example/cas.go rename to example/cas/main.go diff --git a/example/cocurrent.go b/example/concurrent/main.go similarity index 84% rename from example/cocurrent.go rename to example/concurrent/main.go index b8a01ed..35760cb 100644 --- a/example/cocurrent.go +++ b/example/concurrent/main.go @@ -28,13 +28,12 @@ func main() { wg.Add(1) go func(idx int) { defer wg.Done() - defer func() { fmt.Printf("routine(%d) done\n", i) }() + defer func() { fmt.Printf("routine(%d) done\n", idx) }() for counter := 0; counter < limits; counter++ { key := "example:cas" // set - err = client.Set(ctx, key, []byte("value2"), uint32(123), 10*time.Second) - if err != nil { + if err := client.Set(ctx, key, []byte("value2"), uint32(123), 10*time.Second); err != nil { panic("write: " + err.Error()) } // get diff --git a/example/flushall.go b/example/flushall/main.go similarity index 100% rename from example/flushall.go rename to example/flushall/main.go diff --git a/example/go.mod b/example/go.mod index c230878..264abcb 100644 --- a/example/go.mod +++ b/example/go.mod @@ -1,28 +1,28 @@ module github.com/yeqown/memcached/example -go 1.26.0 +go 1.26.5 require ( - github.com/yeqown/memcached v1.1.2 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 - go.opentelemetry.io/otel/sdk v1.40.0 - go.opentelemetry.io/otel/sdk/metric v1.40.0 + github.com/yeqown/memcached v1.4.0 + go.opentelemetry.io/otel v1.45.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.45.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0 + go.opentelemetry.io/otel/sdk v1.45.0 + go.opentelemetry.io/otel/sdk/metric v1.45.0 ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/klauspost/compress v1.19.2 // indirect + github.com/pierrec/lz4/v4 v4.1.28 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect - golang.org/x/sys v0.40.0 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/trace v1.45.0 // indirect + golang.org/x/sys v0.47.0 // indirect ) replace github.com/yeqown/memcached => ../ diff --git a/example/go.sum b/example/go.sum index bad0612..987b263 100644 --- a/example/go.sum +++ b/example/go.sum @@ -3,43 +3,45 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL 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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/pierrec/lz4/v4 v4.1.28 h1:pPEPwRJ4kybBTfGt28q7lQsRJQHhC08axprdLD5Ppio= +github.com/pierrec/lz4/v4 v4.1.28/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.45.0 h1:dm9iyzn6tioYZtwqaiBSU0TSI8Yu/8dTIbfG0+B49DY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.45.0/go.mod h1:xAvxYjYK28qvt+yu4BYZ/zMmAjwMXINXD6JiMyeB8iI= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0 h1:lsA/S1bxgdbyFGkTj+3meEdJ6ADVU7QoFstV6MXgE68= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0/go.mod h1:L7u+MirGoB1bjeLH66+xDykF4RC8C3RN7lIFpBiewUo= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns= +go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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/example/noreply.go b/example/noreply/main.go similarity index 94% rename from example/noreply.go rename to example/noreply/main.go index 9e06cc7..c9bb946 100644 --- a/example/noreply.go +++ b/example/noreply/main.go @@ -14,7 +14,7 @@ func main() { panic(err) } - key := string("noReply") + key := "noReply" value := []byte("noReply") ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) diff --git a/example/options.go b/example/options/main.go similarity index 83% rename from example/options.go rename to example/options/main.go index 98b6608..2404208 100644 --- a/example/options.go +++ b/example/options/main.go @@ -10,9 +10,10 @@ import ( func main() { addrs := "localhost:11211" - // Create a new client with the default options. - client, err := memcached.New(addrs, - memcached.WithDialTimeout(time.Second*5), + // Create a new client with custom timeout options. + client, err := memcached.New( + addrs, + memcached.WithDialTimeout(5*time.Second), memcached.WithReadTimeout(100*time.Second), memcached.WithWriteTimeout(100*time.Second), ) diff --git a/example/otel/main.go b/example/otel/main.go index b3dc51c..525f155 100644 --- a/example/otel/main.go +++ b/example/otel/main.go @@ -35,13 +35,21 @@ func main() { tp := trace.NewTracerProvider( trace.WithBatcher(traceExporter), ) - defer tp.Shutdown(context.Background()) + defer func() { + if err := tp.Shutdown(context.Background()); err != nil { + log.Printf("Shutting down tracer provider: %v", err) + } + }() // Create meter provider mp := sdkmetric.NewMeterProvider( sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter)), ) - defer mp.Shutdown(context.Background()) + defer func() { + if err := mp.Shutdown(context.Background()); err != nil { + log.Printf("Shutting down meter provider: %v", err) + } + }() // Set global providers otel.SetTracerProvider(tp) @@ -57,7 +65,11 @@ func main() { if err != nil { log.Fatalf("Failed to create client: %v", err) } - defer client.Close() + defer func() { + if err := client.Close(); err != nil { + log.Printf("Closing client: %v", err) + } + }() ctx := context.Background() diff --git a/example/simple.go b/example/simple/main.go similarity index 80% rename from example/simple.go rename to example/simple/main.go index f88018f..480f2d7 100644 --- a/example/simple.go +++ b/example/simple/main.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "fmt" "time" "github.com/yeqown/memcached" @@ -22,7 +23,7 @@ func main() { if err != nil { panic(err) } - println("Version: ", version) + fmt.Println("Version:", version) // get first item, err := client.Get(ctx, "key") @@ -31,9 +32,9 @@ func main() { panic(err) } - println("'key' not found") + fmt.Println("'key' not found") } else { - println("key: ", item.Key, " value: ", string(item.Value)) + fmt.Println("key:", item.Key, "value:", string(item.Value)) } if err = client.Set(ctx, "key", []byte("value"), 0, 0); err != nil { @@ -49,6 +50,6 @@ func main() { } for _, item := range items { - println("key: ", item.Key, " value: ", string(item.Value)) + fmt.Println("key:", item.Key, "value:", string(item.Value)) } } diff --git a/example/udp.go b/example/udp/main.go similarity index 81% rename from example/udp.go rename to example/udp/main.go index cf0f91c..6328810 100644 --- a/example/udp.go +++ b/example/udp/main.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "fmt" "time" "github.com/yeqown/memcached" @@ -22,19 +23,19 @@ func main() { if err != nil { panic(err) } - println("Version: ", version) + fmt.Println("Version:", version) key := "key:udp" value := "This is a value for key:udp" // get first - item, err := client.Get(ctx, key) + _, err = client.Get(ctx, key) if err != nil { if !errors.Is(err, memcached.ErrNotFound) { panic(err) } - println("'key' not found") + fmt.Println("'key' not found") } // set @@ -43,10 +44,10 @@ func main() { } // get again - item, err = client.Get(ctx, key) + item, err := client.Get(ctx, key) if err != nil { panic(err) } - println("key: ", item.Key, " value: ", string(item.Value)) + fmt.Println("key:", item.Key, "value:", string(item.Value)) } diff --git a/go.mod b/go.mod index d097580..65fd5fb 100644 --- a/go.mod +++ b/go.mod @@ -1,210 +1,23 @@ module github.com/yeqown/memcached -go 1.26.0 +go 1.26.5 require ( - github.com/hashicorp/go-multierror v1.1.1 - github.com/pkg/errors v0.9.1 + github.com/golang/snappy v1.0.0 + github.com/klauspost/compress v1.19.2 + github.com/pierrec/lz4/v4 v4.1.28 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/metric v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 + go.opentelemetry.io/otel v1.45.0 + go.opentelemetry.io/otel/metric v1.45.0 + go.opentelemetry.io/otel/trace v1.45.0 ) require ( - 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect - 4d63.com/gochecknoglobals v0.2.2 // indirect - github.com/4meepo/tagalign v1.4.2 // indirect - github.com/Abirdcfly/dupword v0.1.3 // indirect - github.com/Antonboom/errname v1.0.0 // indirect - github.com/Antonboom/nilnil v1.0.1 // indirect - github.com/Antonboom/testifylint v1.5.2 // indirect - github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect - github.com/Crocmagnon/fatcontext v0.7.1 // indirect - github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect - github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect - github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect - github.com/alecthomas/go-check-sumtype v0.3.1 // indirect - github.com/alexkohler/nakedret/v2 v2.0.5 // indirect - github.com/alexkohler/prealloc v1.0.0 // indirect - github.com/alingse/asasalint v0.0.11 // indirect - github.com/alingse/nilnesserr v0.1.2 // indirect - github.com/ashanbrown/forbidigo v1.6.0 // indirect - github.com/ashanbrown/makezero v1.2.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/bkielbasa/cyclop v1.2.3 // indirect - github.com/blizzy78/varnamelen v0.8.0 // indirect - github.com/bombsimon/wsl/v4 v4.5.0 // indirect - github.com/breml/bidichk v0.3.2 // indirect - github.com/breml/errchkjson v0.4.0 // indirect - github.com/butuzov/ireturn v0.3.1 // indirect - github.com/butuzov/mirror v1.3.0 // indirect - github.com/catenacyber/perfsprint v0.8.2 // indirect - github.com/ccojocar/zxcvbn-go v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charithe/durationcheck v0.0.10 // indirect - github.com/chavacava/garif v0.1.0 // indirect - github.com/ckaznocha/intrange v0.3.0 // indirect - github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/denis-tingaikin/go-header v0.5.0 // indirect - github.com/ettle/strcase v0.2.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fatih/structtag v1.2.0 // indirect - github.com/firefart/nonamedreturns v1.0.5 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/ghostiam/protogetter v0.3.9 // indirect - github.com/go-critic/go-critic v0.12.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-toolsmith/astcast v1.1.0 // indirect - github.com/go-toolsmith/astcopy v1.1.0 // indirect - github.com/go-toolsmith/astequal v1.2.0 // indirect - github.com/go-toolsmith/astfmt v1.1.0 // indirect - github.com/go-toolsmith/astp v1.1.0 // indirect - github.com/go-toolsmith/strparse v1.1.0 // indirect - github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect - github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect - github.com/gobwas/glob v0.2.3 // indirect - github.com/gofrs/flock v0.12.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/golang/snappy v1.0.0 // indirect - github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect - github.com/golangci/go-printf-func-name v0.1.0 // indirect - github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect - github.com/golangci/golangci-lint v1.64.8 // indirect - github.com/golangci/misspell v0.6.0 // indirect - github.com/golangci/plugin-module-register v0.1.1 // indirect - github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/gordonklaus/ineffassign v0.1.0 // indirect - github.com/gostaticanalysis/analysisutil v0.7.1 // indirect - github.com/gostaticanalysis/comment v1.5.0 // indirect - github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect - github.com/gostaticanalysis/nilerr v0.1.1 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jgautheron/goconst v1.7.1 // indirect - github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jjti/go-spancheck v0.6.4 // indirect - github.com/julz/importas v0.2.0 // indirect - github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect - github.com/kisielk/errcheck v1.9.0 // indirect - github.com/kkHAIKE/contextcheck v1.1.6 // indirect - github.com/klauspost/compress v1.18.6 // indirect - github.com/kulti/thelper v0.6.3 // indirect - github.com/kunwardeep/paralleltest v1.0.10 // indirect - github.com/lasiar/canonicalheader v1.1.2 // indirect - github.com/ldez/exptostd v0.4.2 // indirect - github.com/ldez/gomoddirectives v0.6.1 // indirect - github.com/ldez/grignotin v0.9.0 // indirect - github.com/ldez/tagliatelle v0.7.1 // indirect - github.com/ldez/usetesting v0.4.2 // indirect - github.com/leonklingele/grouper v1.1.2 // indirect - github.com/macabu/inamedparam v0.1.3 // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/maratori/testableexamples v1.0.0 // indirect - github.com/maratori/testpackage v1.1.1 // indirect - github.com/matoous/godox v1.1.0 // indirect - github.com/matryer/is v1.4.1 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.7.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moricho/tparallel v0.3.2 // indirect - github.com/nakabonne/nestif v0.3.1 // indirect - github.com/nishanths/exhaustive v0.12.0 // indirect - github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.19.1 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/polyfloyd/go-errorlint v1.7.1 // indirect - github.com/prometheus/client_golang v1.12.1 // 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/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect - github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect - github.com/quasilyte/gogrep v0.5.0 // indirect - github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect - github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/raeperd/recvcheck v0.2.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/ryancurrah/gomodguard v1.3.5 // indirect - github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect - github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect - github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect - github.com/securego/gosec/v2 v2.22.2 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sivchari/tenv v1.12.1 // indirect - github.com/sonatard/noctx v0.1.0 // indirect - github.com/sourcegraph/go-diff v0.7.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/spf13/viper v1.12.0 // indirect - github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect - github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - github.com/tdakkota/asciicheck v0.4.1 // indirect - github.com/tetafro/godot v1.5.0 // indirect - github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect - github.com/timonwong/loggercheck v0.10.1 // indirect - github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect - github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect - github.com/ultraware/funlen v0.2.0 // indirect - github.com/ultraware/whitespace v0.2.0 // indirect - github.com/uudashr/gocognit v1.2.0 // indirect - github.com/uudashr/iface v1.3.1 // indirect - github.com/xen0n/gosmopolitan v1.2.2 // indirect - github.com/yagipy/maintidx v1.0.0 // indirect - github.com/yeya24/promlinter v0.3.0 // indirect - github.com/ykadowak/zerologlint v0.1.5 // indirect - gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.13.0 // indirect - go-simpler.org/sloglint v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/goleak v1.3.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - honnef.co/go/tools v0.6.1 // indirect - mvdan.cc/gofumpt v0.7.0 // indirect - mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect ) - -tool github.com/golangci/golangci-lint/cmd/golangci-lint diff --git a/go.work b/go.work index a4b482a..cc5a23f 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.26.0 +go 1.26.5 use ( . diff --git a/gui/.gitignore b/gui/.gitignore index 129d522..1a4eb65 100644 --- a/gui/.gitignore +++ b/gui/.gitignore @@ -1,3 +1,4 @@ build/bin node_modules +frontend/.svelte-check frontend/dist diff --git a/gui/frontend/package-lock.json b/gui/frontend/package-lock.json index 779dac9..c4ec669 100644 --- a/gui/frontend/package-lock.json +++ b/gui/frontend/package-lock.json @@ -8,48 +8,38 @@ "name": "frontend", "version": "0.0.0", "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^1.0.1", - "@tsconfig/svelte": "^3.0.0", - "svelte": "^3.49.0", - "svelte-check": "^2.8.0", - "svelte-preprocess": "^4.10.7", - "tslib": "^2.4.0", - "typescript": "^4.6.4", - "vite": "^3.0.7" + "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.56.8", + "svelte-check": "^4.7.5", + "tslib": "^2.8.1", + "typescript": "^6.0.3", + "vite": "^8.2.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", - "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", - "cpu": [ - "arm" - ], + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", - "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", - "cpu": [ - "loong64" - ], + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -80,324 +70,20 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.4.0.tgz", - "integrity": "sha512-6QupI/jemMfK+yI2pMtJcu5iO2gtgTfcBdGwMZZt+lgbFELhszbDl6Qjh000HgAV8+XUA+8EY8DusOFk8WhOIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "deepmerge": "^4.2.2", - "kleur": "^4.1.5", - "magic-string": "^0.26.7", - "svelte-hmr": "^0.15.1", - "vitefu": "^0.2.2" - }, - "engines": { - "node": "^14.18.0 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.44.0", - "vite": "^3.0.0" - } - }, - "node_modules/@tsconfig/svelte": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-3.0.0.tgz", - "integrity": "sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.19.0" - } - }, - "node_modules/@types/pug": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", - "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sass": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.43.1.tgz", - "integrity": "sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==", + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", - "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.15.18", - "@esbuild/linux-loong64": "0.15.18", - "esbuild-android-64": "0.15.18", - "esbuild-android-arm64": "0.15.18", - "esbuild-darwin-64": "0.15.18", - "esbuild-darwin-arm64": "0.15.18", - "esbuild-freebsd-64": "0.15.18", - "esbuild-freebsd-arm64": "0.15.18", - "esbuild-linux-32": "0.15.18", - "esbuild-linux-64": "0.15.18", - "esbuild-linux-arm": "0.15.18", - "esbuild-linux-arm64": "0.15.18", - "esbuild-linux-mips64le": "0.15.18", - "esbuild-linux-ppc64le": "0.15.18", - "esbuild-linux-riscv64": "0.15.18", - "esbuild-linux-s390x": "0.15.18", - "esbuild-netbsd-64": "0.15.18", - "esbuild-openbsd-64": "0.15.18", - "esbuild-sunos-64": "0.15.18", - "esbuild-windows-32": "0.15.18", - "esbuild-windows-64": "0.15.18", - "esbuild-windows-arm64": "0.15.18" - } - }, - "node_modules/esbuild-android-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", - "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-android-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", - "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", "cpu": [ "arm64" ], @@ -408,15 +94,15 @@ "android" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-darwin-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", - "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -425,15 +111,15 @@ "darwin" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", - "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", @@ -442,13 +128,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-freebsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", - "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", "cpu": [ "x64" ], @@ -459,166 +145,184 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", - "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-linux-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", - "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-linux-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", - "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-linux-arm": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", - "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", "cpu": [ - "arm" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-linux-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", - "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", "cpu": [ - "arm64" + "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", - "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", "cpu": [ - "mips64el" + "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", - "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", "cpu": [ - "ppc64" + "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-linux-riscv64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", - "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-linux-s390x": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", - "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-netbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", - "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", "cpu": [ "x64" ], @@ -626,413 +330,522 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "win32" ], "engines": { - "node": ">=12" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/esbuild-openbsd-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", - "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", - "cpu": [ - "x64" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/esbuild-sunos-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", - "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", - "cpu": [ - "x64" - ], + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.12.tgz", + "integrity": "sha512-J1jNYG23QWd67UfrQSFHtjhV37r9mVi0gdc12A3MWPldOjRK35Xk+um+qACPVjgw3AleiqoyEAhok8Wm3q46NA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "acorn": "^8.9.0" } }, - "node_modules/esbuild-windows-32": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", - "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", - "cpu": [ - "ia32" - ], + "node_modules/@sveltejs/load-config": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.2.tgz", + "integrity": "sha512-K7dsJDQxBOF+f+epuhMactcjK2VP4MRkLKtwSykNtEI+cKVEyzrmmhQ1pmoxI800m4JKcyJ05L2M4yPwAGiBNw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">= 18.0.0" } }, - "node_modules/esbuild-windows-64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", - "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", - "cpu": [ - "x64" - ], + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.0.tgz", + "integrity": "sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^1.0.0", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, "engines": { - "node": ">=12" + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" } }, - "node_modules/esbuild-windows-arm64": { - "version": "0.15.18", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", - "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", - "cpu": [ - "arm64" - ], + "node_modules/@tsconfig/svelte": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", + "integrity": "sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=12" + "node": ">=0.4.0" } }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, + "license": "Apache-2.0", "engines": { - "node": ">=8.6.0" + "node": ">= 0.4" } }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=8" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/devalue": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz", + "integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==", "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } + "license": "MIT" }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "node_modules/esrap": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.2.tgz", + "integrity": "sha512-40GyiEJevYKXzYTHtZkFqAgTjLOuFcaXMao8TPyOlnWTlkHDlvZ6mPMJaJyOqVwrVCgomEG1WhJd81w0X+IcCw==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "@jridgewell/sourcemap-codec": "^1.4.15" }, - "engines": { - "node": ">= 0.4" + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, "engines": { - "node": ">=6" + "node": ">=12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dev": true, "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" + "@types/estree": "^1.0.6" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "hasown": "^2.0.2" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=0.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/magic-string": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", - "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - }, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "*" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.1.0.tgz", + "integrity": "sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/mri": { @@ -1045,17 +858,10 @@ "node": ">=4" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -1071,56 +877,20 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12.20.0" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1129,22 +899,22 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -1162,7 +932,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1170,135 +940,51 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, "engines": { - "node": ">= 0.4" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", "dev": true, "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", "dependencies": { - "glob": "^7.1.3" + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rollup": { - "version": "2.80.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", - "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=10.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" } }, "node_modules/sade": { @@ -1314,35 +1000,6 @@ "node": ">=6" } }, - "node_modules/sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - } - }, - "node_modules/sorcery": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", - "integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0", - "sourcemap-codec": "^1.3.0" - }, - "bin": { - "sorcery": "bin/index.js" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1353,174 +1010,84 @@ "node": ">=0.10.0" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "node_modules/svelte": { + "version": "5.56.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz", + "integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==", "dev": true, "license": "MIT", "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svelte": { - "version": "3.59.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz", - "integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==", - "dev": true, - "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=18" } }, "node_modules/svelte-check": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-2.10.3.tgz", - "integrity": "sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.5.tgz", + "integrity": "sha512-NnkHGCTPH6k4ka1E9IpTuNv40uLArHnX52kLEuaHSGqRlPYTnkbFs529jSWG+y+wDy+v+jA2PQ0soN1umVK+OA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.9", - "chokidar": "^3.4.1", - "fast-glob": "^3.2.7", - "import-fresh": "^3.2.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.2", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^4.0.0", - "typescript": "*" + "sade": "^1.7.4" }, "bin": { "svelte-check": "bin/svelte-check" }, - "peerDependencies": { - "svelte": "^3.24.0" - } - }, - "node_modules/svelte-hmr": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", - "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, - "node_modules/svelte-preprocess": { - "version": "4.10.7", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz", - "integrity": "sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@types/pug": "^2.0.4", - "@types/sass": "^1.16.0", - "detect-indent": "^6.0.0", - "magic-string": "^0.25.7", - "sorcery": "^0.10.0", - "strip-indent": "^3.0.0" - }, "engines": { - "node": ">= 9.11.2" + "node": ">= 18.0.0" }, "peerDependencies": { - "@babel/core": "^7.10.2", - "coffeescript": "^2.5.1", - "less": "^3.11.3 || ^4.0.0", - "postcss": "^7 || ^8", - "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0", - "pug": "^3.0.0", - "sass": "^1.26.8", - "stylus": "^0.55.0", - "sugarss": "^2.0.0", - "svelte": "^3.23.0", - "typescript": "^3.9.5 || ^4.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "coffeescript": { - "optional": true - }, - "less": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "postcss": { - "optional": true - }, - "postcss-load-config": { - "optional": true - }, - "pug": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "typescript": { - "optional": true - } + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.0.0 || ^6.0.0" } }, - "node_modules/svelte-preprocess/node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "node_modules/svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "sourcemap-codec": "^1.4.8" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tslib": { @@ -1531,9 +1098,9 @@ "license": "0BSD" }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1541,55 +1108,70 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "dev": true, - "license": "MIT" - }, "node_modules/vite": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", - "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.15.9", - "postcss": "^8.4.18", - "resolve": "^1.22.1", - "rollup": "^2.79.1" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, "less": { "optional": true }, "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -1598,17 +1180,28 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "dev": true, "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "vite": { @@ -1616,12 +1209,12 @@ } } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "dev": true, - "license": "ISC" + "license": "MIT" } } } diff --git a/gui/frontend/package.json b/gui/frontend/package.json index 2ee69ea..aec1114 100644 --- a/gui/frontend/package.json +++ b/gui/frontend/package.json @@ -3,6 +3,9 @@ "private": true, "version": "0.0.0", "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, "scripts": { "dev": "vite", "build": "vite build", @@ -10,13 +13,12 @@ "check": "svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^1.0.1", - "@tsconfig/svelte": "^3.0.0", - "svelte": "^3.49.0", - "svelte-check": "^2.8.0", - "svelte-preprocess": "^4.10.7", - "tslib": "^2.4.0", - "typescript": "^4.6.4", - "vite": "^3.0.7" + "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.56.8", + "svelte-check": "^4.7.5", + "tslib": "^2.8.1", + "typescript": "^6.0.3", + "vite": "^8.2.1" } -} \ No newline at end of file +} diff --git a/gui/frontend/package.json.md5 b/gui/frontend/package.json.md5 index b063894..4f02c61 100755 --- a/gui/frontend/package.json.md5 +++ b/gui/frontend/package.json.md5 @@ -1 +1 @@ -ad9da3c17151b053a4d2fda8e3578901 \ No newline at end of file +20b1bd7601fb6f4dd889f341eefb63c0 diff --git a/gui/frontend/src/components/CommandSelector.svelte b/gui/frontend/src/components/CommandSelector.svelte index c91943c..4bc5333 100644 --- a/gui/frontend/src/components/CommandSelector.svelte +++ b/gui/frontend/src/components/CommandSelector.svelte @@ -5,7 +5,7 @@ export let selected: CommandId = 'get' export let disabled = false - const dispatch = createEventDispatcher<'change', CommandId>() + const dispatch = createEventDispatcher<{ change: CommandId }>() let open = false diff --git a/gui/frontend/src/components/InputArea.svelte b/gui/frontend/src/components/InputArea.svelte index 521c02d..b359fc1 100644 --- a/gui/frontend/src/components/InputArea.svelte +++ b/gui/frontend/src/components/InputArea.svelte @@ -6,13 +6,14 @@ export let disabled = false // Store values for each possible input + let key = '' let values: Record = { - key: '', value: '', flags: 0, expiry: 0, delta: 1, } + $: values.key = key export function getValues(): Record { const cmd = getCommand(command) @@ -26,11 +27,11 @@ } export function getKey(): string { - return values.key as string + return key } export function clearKey() { - values.key = '' + key = '' } // Reset default values when command changes @@ -39,7 +40,11 @@ if (cmd) { for (const input of cmd.inputs) { if (input.defaultValue !== undefined) { - values[input.id] = input.defaultValue + if (input.id === 'key') { + key = String(input.defaultValue) + } else { + values[input.id] = input.defaultValue + } } } } @@ -54,7 +59,7 @@ @@ -149,4 +154,4 @@ @media (prefers-reduced-motion: reduce) { textarea, input[type="number"] { transition: none; } } - \ No newline at end of file + diff --git a/gui/frontend/src/components/JsonTree.svelte b/gui/frontend/src/components/JsonTree.svelte index 06f6020..158eeb1 100644 --- a/gui/frontend/src/components/JsonTree.svelte +++ b/gui/frontend/src/components/JsonTree.svelte @@ -25,9 +25,9 @@ $: kind = typeOf(data) $: isContainer = kind === 'object' || kind === 'array' - $: entries = isContainer + $: entries = (isContainer ? (kind === 'array' ? data.map((v: any, i: number) => [String(i), v]) : Object.entries(data)) - : [] + : []) as Array<[string, any]> $: count = entries.length $: effectiveExpanded = forceExpanded ? true : (forceCollapsed ? false : expanded) $: matchesSearch = !searchQuery || nodeMatches(data, searchQuery, kind) @@ -46,9 +46,9 @@ if (key.toLowerCase().includes(query.toLowerCase())) return true const childKind = typeOf(value) if (childKind === 'object' || childKind === 'array') { - const childEntries = childKind === 'array' + const childEntries = (childKind === 'array' ? value.map((v: any, i: number) => [String(i), v]) - : Object.entries(value) + : Object.entries(value)) as Array<[string, any]> return childEntries.some(([k, v]) => childNodeMatches(k, v, query)) } return nodeMatches(value, query, childKind) diff --git a/gui/frontend/src/components/KeyInput.svelte b/gui/frontend/src/components/KeyInput.svelte index 2c345f1..7368650 100644 --- a/gui/frontend/src/components/KeyInput.svelte +++ b/gui/frontend/src/components/KeyInput.svelte @@ -1,5 +1,4 @@