From 027edff43a962d49f41921da787fcf0c92441d0c Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:11:08 +0200 Subject: [PATCH 01/10] chore: migrate root module to stdlib errors Replace pkg/errors and go-multierror with fmt.Errorf, errors.Is, and errors.Join, tidy the root module to Go 1.26.5, and add error-chain tests. Co-authored-by: Cursor --- README.md | 2 +- client.go | 36 ++++---- client_cluster.go | 15 ++-- client_commands.go | 84 +++++++++--------- client_test.go | 12 +-- conn.go | 6 +- errors.go | 2 +- errors_test.go | 27 ++++++ go.mod | 202 ++----------------------------------------- go.work | 2 +- protocol.go | 34 ++++---- protocol_bin.go | 15 ++-- protocol_bin_test.go | 2 +- protocol_builder.go | 14 +-- protocol_meta.go | 20 ++--- protocol_test.go | 2 +- 16 files changed, 155 insertions(+), 320 deletions(-) create mode 100644 errors_test.go diff --git a/README.md b/README.md index c24f0d5..ba30e9c 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,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/client.go b/client.go index 9790f24..438aa3e 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" @@ -70,11 +70,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 +100,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 +129,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 +196,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 +213,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 +232,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 +245,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 +275,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 +291,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..11845f8 100644 --- a/client_cluster.go +++ b/client_cluster.go @@ -1,12 +1,11 @@ package memcached import ( + "fmt" "hash/crc32" "net" "strings" - "github.com/pkg/errors" - "github.com/yeqown/memcached/hash" ) @@ -52,7 +51,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 +72,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 +82,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 +107,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,7 +120,7 @@ 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 @@ -150,7 +149,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..1544a36 100644 --- a/client_commands.go +++ b/client_commands.go @@ -3,9 +3,9 @@ package memcached import ( "bytes" "context" + "errors" + "fmt" "time" - - "github.com/pkg/errors" ) type basicTextProtocolCommander interface { @@ -144,16 +144,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 +191,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 +214,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 +237,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 +260,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 +284,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 +313,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 +331,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 +350,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 +371,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 +387,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 +408,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 @@ -451,7 +451,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 +486,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 +513,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 +540,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 +579,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 +598,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,7 +621,7 @@ 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) @@ -632,7 +632,7 @@ func (c *client) Raw(ctx context.Context, cmd string) ([]string, 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) } lines := make([]string, 0, len(resp.rawLines)) 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/conn.go b/conn.go index f5fc7d9..7b5c8e6 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{ 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/go.mod b/go.mod index d097580..e48cfb7 100644 --- a/go.mod +++ b/go.mod @@ -1,210 +1,24 @@ module github.com/yeqown/memcached -go 1.26.0 +go 1.26.5 require ( - github.com/hashicorp/go-multierror v1.1.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/pkg/errors v0.9.1 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/protocol.go b/protocol.go index 7c80a1f..ba7954e 100644 --- a/protocol.go +++ b/protocol.go @@ -3,11 +3,10 @@ package memcached import ( "bytes" "encoding/json" + "fmt" "log" "strconv" "time" - - "github.com/pkg/errors" ) // Codec transforms value and flags at the protocol boundary. @@ -27,7 +26,7 @@ type Codec interface { func checkCodecSupportsOperation(codec Codec, operation string) error { if err := codec.SupportsOperation(operation); err != nil { - return errors.Wrap(ErrNotSupported, err.Error()) + return fmt.Errorf("%s: %w", err.Error(), ErrNotSupported) } return nil } @@ -147,9 +146,8 @@ func buildStorageCommand( noReply bool, codec Codec, ) (*request, *response, error) { - if err := checkCodecSupportsOperation(codec, command); err != nil { - return nil, nil, errors.Wrap(err, "codec does not support operation") + return nil, nil, fmt.Errorf("codec does not support operation: %w", err) } evalue, eflags, err := codec.Encode([]byte(key), value, flags) @@ -238,7 +236,7 @@ func buildCasCommand( key string, value []byte, flag uint32, expTime time.Duration, casUnique uint64, noReply bool, codec Codec, ) (*request, *response, error) { if err := checkCodecSupportsOperation(codec, "cas"); err != nil { - return nil, nil, errors.Wrap(err, "codec does not support operation") + return nil, nil, fmt.Errorf("codec does not support operation: %w", err) } evalue, eflag, err := codec.Encode([]byte(key), value, flag) @@ -328,11 +326,11 @@ func parseValueItems(lines [][]byte, withoutEndLine, withCAS bool, codec Codec) n := len(lines) if withoutEndLine && n%2 != 0 { // n must be even - return nil, errors.Wrap(ErrMalformedResponse, "want times of 2 lines, got "+strconv.Itoa(n)) + return nil, fmt.Errorf("want times of 2 lines, got %d: %w", n, ErrMalformedResponse) } if !withoutEndLine && n%2 == 0 { // n must be odd - return nil, errors.Wrap(ErrMalformedResponse, "want odd lines, got "+strconv.Itoa(n)) + return nil, fmt.Errorf("want odd lines, got %d: %w", n, ErrMalformedResponse) } var ( @@ -365,11 +363,11 @@ func parseValueItems(lines [][]byte, withoutEndLine, withCAS bool, codec Codec) // Read the data block if i+1 >= n { - return nil, errors.Wrap(ErrMalformedResponse, "missing data block") + return nil, fmt.Errorf("missing data block: %w", ErrMalformedResponse) } item.Value = trimCRLF(lines[i+1]) if len(item.Value) != int(dataLen) { - return nil, errors.Wrap(ErrMalformedResponse, "data block length mismatch") + return nil, fmt.Errorf("data block length mismatch: %w", ErrMalformedResponse) } decodedValue, decodedFlags, err := codec.Decode([]byte(item.Key), item.Value, uint32(item.Flags)) @@ -404,7 +402,7 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err for i := start; i < n; i++ { if nField > 5 || (!withCas && nField > 4) { - return 0, errors.Wrap(ErrMalformedResponse, "invalid VALUE line") + return 0, fmt.Errorf("invalid VALUE line: %w", ErrMalformedResponse) } if line[i] != ' ' && i != n-1 { @@ -419,7 +417,7 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err case flagsIndex: flags, err := parseUintFromBytes(line[fieldStart:i]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, "invalid flags") + return 0, fmt.Errorf("invalid flags: %w", ErrMalformedResponse) } item.Flags = uint32(flags) case dataLenIndex: @@ -429,7 +427,7 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err } dataLen, err = parseUintFromBytes(line[fieldStart:si]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, "invalid data length") + return 0, fmt.Errorf("invalid data length: %w", ErrMalformedResponse) } case casIndex: si := i @@ -438,7 +436,7 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err } item.CAS, err = parseUintFromBytes(line[fieldStart:si]) if err != nil { - return 0, errors.Wrap(ErrMalformedResponse, "invalid CAS") + return 0, fmt.Errorf("invalid CAS: %w", ErrMalformedResponse) } } @@ -465,7 +463,7 @@ func parseUintFromBytes(bs []byte) (uint64, error) { r := uint64(0) for _, b := range bs { if b < '0' || b > '9' { - return 0, errors.Wrap(ErrMalformedResponse, "invalid uint number") + return 0, fmt.Errorf("invalid uint number: %w", ErrMalformedResponse) } r = r*10 + uint64(b-'0') @@ -617,7 +615,7 @@ type Statistic struct { func parseStats(lines [][]byte) (*Statistic, error) { if len(lines) <= 0 { - return nil, errors.Wrap(ErrMalformedResponse, "empty response") + return nil, fmt.Errorf("empty response: %w", ErrMalformedResponse) } transitionMap := make(map[string]any, len(lines)) @@ -662,12 +660,12 @@ func parseStats(lines [][]byte) (*Statistic, error) { raw, err := json.Marshal(transitionMap) if err != nil { - return nil, errors.Wrap(err, "parseStats marshal transition map failed") + return nil, fmt.Errorf("parseStats marshal transition map failed: %w", err) } stat := &Statistic{} if err = json.Unmarshal(raw, stat); err != nil { - return nil, errors.Wrap(err, "parseStats unmarshal failed") + return nil, fmt.Errorf("parseStats unmarshal failed: %w", err) } return stat, nil diff --git a/protocol_bin.go b/protocol_bin.go index 6083633..824cd39 100644 --- a/protocol_bin.go +++ b/protocol_bin.go @@ -2,9 +2,8 @@ package memcached import ( "encoding/binary" + "fmt" "io" - - "github.com/pkg/errors" ) // https://docs.memcached.org/protocols/binary/ @@ -199,25 +198,25 @@ func (br *binaryResponse) expect(status uint16) error { case _binaryStatusNotSupported: return ErrNotSupported case _binaryStatusInternalError: - return errors.Wrap(ErrServerError, "internal error") + return fmt.Errorf("internal error: %w", ErrServerError) case _binaryStatusInvalidArgs: return ErrInvalidArgument case _binaryStatusOutOfMemory: - return errors.Wrap(ErrServerError, "out of memory") + return fmt.Errorf("out of memory: %w", ErrServerError) } // return: status: 0x1234 format - return errors.Wrapf(ErrServerError, "status: %x", br.status) + return fmt.Errorf("status: %x: %w", br.status, ErrServerError) } func (br *binaryResponse) read(rr io.Reader) error { buf := make([]byte, 24) if _, err := io.ReadFull(rr, buf); err != nil { - return errors.Wrap(err, "read header") + return fmt.Errorf("read header: %w", err) } if magic := buf[0]; magic != _binaryMagicRes { - return errors.Wrapf(ErrInvalidBinaryProtocol, "invalid magic: %d", magic) + return fmt.Errorf("invalid magic: %d: %w", magic, ErrInvalidBinaryProtocol) } br.opcode = buf[1] @@ -236,7 +235,7 @@ func (br *binaryResponse) read(rr io.Reader) error { // read the whole body and split them into extras, key, value body := make([]byte, br.totalBodyLength) if _, err := io.ReadFull(rr, body); err != nil { - return errors.Wrap(err, "read body") + return fmt.Errorf("read body: %w", err) } s := uint32(0) diff --git a/protocol_bin_test.go b/protocol_bin_test.go index 69ccccc..aabb253 100644 --- a/protocol_bin_test.go +++ b/protocol_bin_test.go @@ -2,10 +2,10 @@ package memcached import ( "bytes" + "errors" "io" "testing" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) diff --git a/protocol_builder.go b/protocol_builder.go index 4d7da57..949387e 100644 --- a/protocol_builder.go +++ b/protocol_builder.go @@ -4,13 +4,13 @@ import ( "bytes" "context" "encoding/base64" + "errors" + "fmt" "math" "strconv" "sync" "time" "unsafe" - - "github.com/pkg/errors" ) const ( @@ -53,10 +53,10 @@ func forecastCommonFaultLine(line []byte) error { return ErrNonexistentCommand case bytes.HasPrefix(line, []byte("CLIENT_ERROR")): message := string(line[12 : len(line)-2]) - return errors.Wrap(ErrClientError, message) + return fmt.Errorf("%s: %w", message, ErrClientError) case bytes.HasPrefix(line, []byte("SERVER_ERROR")): message := string(line[12 : len(line)-2]) - return errors.Wrap(ErrServerError, message) + return fmt.Errorf("%s: %w", message, ErrServerError) case bytes.Equal(line, []byte("NOT_FOUND\r\n")): return ErrNotFound case bytes.Equal(line, []byte("EXISTS\r\n")): @@ -468,7 +468,7 @@ func (resp *response) read1(rr memcachedConn) error { for read < int(resp.limitedLines) { line, err := rr.readLine('\n') if err != nil { - return errors.Wrap(err, "dispatchRequest read") + return fmt.Errorf("dispatchRequest read: %w", err) } if read == 0 { @@ -495,7 +495,7 @@ func (resp *response) read2(rr memcachedConn) error { // FIXME(@yeqown): read line would cost too much capacity. line, err := rr.readLine('\n') if err != nil { - return errors.Wrap(err, "dispatchRequest read") + return fmt.Errorf("dispatchRequest read: %w", err) } if read == 0 && resp.udpEnabled { @@ -528,7 +528,7 @@ func (resp *response) expect(line []byte) error { return nil } if n := len(resp.rawLines); n != 1 { - return errors.Wrapf(ErrMalformedResponse, "expect only 1 line, but got %d", n) + return fmt.Errorf("expect only 1 line, but got %d: %w", n, ErrMalformedResponse) } if bytes.Equal(resp.rawLines[0], line) { diff --git a/protocol_meta.go b/protocol_meta.go index 660814e..85b53a8 100644 --- a/protocol_meta.go +++ b/protocol_meta.go @@ -4,8 +4,6 @@ import ( "bytes" "fmt" "strconv" - - "github.com/pkg/errors" ) type metaSetFlags struct { @@ -118,13 +116,13 @@ func buildMetaSetCommand(key, value []byte, flags *metaSetFlags, codec Codec) (* operation = string(MetaSetModeSet) } if err := checkCodecSupportsOperation(codec, operation); err != nil { - return nil, nil, errors.Wrap(err, "codec does not support operation") + return nil, nil, fmt.Errorf("codec does not support operation: %w", err) } // codec hook evalue, eflags, err := codec.Encode(key, value, flags.F) if err != nil { - return nil, nil, errors.Wrap(err, "encode value and flags") + return nil, nil, fmt.Errorf("encode value and flags: %w", err) } flags.F = eflags @@ -358,13 +356,13 @@ func parseMetaItem(lines [][]byte, item *MetaItem, noReply bool, codec Codec) er // figure out the if len(lines) == 0 { - return errors.Wrap(ErrMalformedResponse, "missing response") + return fmt.Errorf("missing response: %w", ErrMalformedResponse) } // Normal CD handling parts := bytes.Split(trimCRLF(lines[0]), _SpaceBytes) if len(parts) < 1 { - return errors.Wrap(ErrMalformedResponse, "invalid response") + return fmt.Errorf("invalid response: %w", ErrMalformedResponse) } const ( CDIndex = 0 @@ -393,12 +391,12 @@ func parseMetaItem(lines [][]byte, item *MetaItem, noReply bool, codec Codec) er parseFlags(parts, 2, item) if len(lines) < 2 { - return errors.Wrap(ErrMalformedResponse, "missing value") + return fmt.Errorf("missing value: %w", ErrMalformedResponse) } var err error if item.Value, item.Flags, err = codec.Decode(item.Key, trimCRLF(lines[1]), item.Flags); err != nil { - return errors.Wrap(err, "codec decode") + return fmt.Errorf("codec decode: %w", err) } return nil @@ -711,12 +709,12 @@ func buildMetaDebugCommand(key []byte, flags *metaDebugFlags) (*request, *respon // failed: EN\r\n func parseMetaItemDebug(lines [][]byte, item *MetaItemDebug) error { if len(lines) != 1 { - return errors.Wrap(ErrMalformedResponse, "invalid response") + return fmt.Errorf("invalid response: %w", ErrMalformedResponse) } parts := bytes.Split(trimCRLF(lines[0]), _SpaceBytes) if len(parts) < 1 { - return errors.Wrap(ErrMalformedResponse, "invalid response") + return fmt.Errorf("invalid response: %w", ErrMalformedResponse) } const ( @@ -731,7 +729,7 @@ func parseMetaItemDebug(lines [][]byte, item *MetaItemDebug) error { case "ME": // success default: - return errors.Wrap(ErrMalformedResponse, "unexpected cd<="+string(cd)+">") + return fmt.Errorf("unexpected cd<=%s>: %w", string(cd), ErrMalformedResponse) } // parse key diff --git a/protocol_test.go b/protocol_test.go index 18781d4..e04d788 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -2,11 +2,11 @@ package memcached import ( "bytes" + "errors" "strconv" "testing" "time" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" memcodec "github.com/yeqown/memcached/codec" ) From e7807b490f14126e254f9cca34fde127c1a0d483 Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:11:25 +0200 Subject: [PATCH 02/10] fix: harden connection pool shutdown and cleanup Keep the idle channel open across close, wake waiters via closedCh, and reject connections created during shutdown. Add race-focused pool tests. Co-authored-by: Cursor --- conn.go | 178 +++++++++++++++++++++++++++++++-------------------- conn_test.go | 159 ++++++++++++++++++++++++++++++++++++++++++--- go.mod | 1 - 3 files changed, 259 insertions(+), 79 deletions(-) diff --git a/conn.go b/conn.go index 7b5c8e6..15e8cbc 100644 --- a/conn.go +++ b/conn.go @@ -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/go.mod b/go.mod index e48cfb7..65fd5fb 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/golang/snappy v1.0.0 github.com/klauspost/compress v1.19.2 github.com/pierrec/lz4/v4 v4.1.28 - github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.45.0 go.opentelemetry.io/otel/metric v1.45.0 From 2398cc06c17f7ed29bd12812b9762975486d444a Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:12:10 +0200 Subject: [PATCH 03/10] fix: harden protocol parsing and buffer lifecycle Add overflow and bounds checks for text/meta/binary frames, replace unsafe string conversions, and cap protocol-builder buffer reuse. Co-authored-by: Cursor --- client_cluster.go | 8 ++- client_commands.go | 4 ++ protocol.go | 15 +++-- protocol_bin.go | 58 +++++++++++--------- protocol_builder.go | 46 +++++++--------- protocol_builder_test.go | 116 +++++++++++++++++++++++++++++++++++++++ protocol_meta.go | 82 ++++++++++++++++++++------- protocol_meta_test.go | 2 +- protocol_test.go | 24 ++++++++ 9 files changed, 277 insertions(+), 78 deletions(-) diff --git a/client_cluster.go b/client_cluster.go index 11845f8..5a351e2 100644 --- a/client_cluster.go +++ b/client_cluster.go @@ -3,6 +3,7 @@ package memcached import ( "fmt" "hash/crc32" + "math" "net" "strings" @@ -127,7 +128,12 @@ func (p *crc32HashPicker) Pick(addrs []*Addr, _, key []byte) (*Addr, error) { } 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{} diff --git a/client_commands.go b/client_commands.go index 1544a36..cbcb73f 100644 --- a/client_commands.go +++ b/client_commands.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "math" "time" ) @@ -443,6 +444,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) diff --git a/protocol.go b/protocol.go index ba7954e..161d21f 100644 --- a/protocol.go +++ b/protocol.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "math" "strconv" "time" ) @@ -366,7 +367,7 @@ func parseValueItems(lines [][]byte, withoutEndLine, withCAS bool, codec Codec) return nil, fmt.Errorf("missing data block: %w", ErrMalformedResponse) } item.Value = trimCRLF(lines[i+1]) - if len(item.Value) != int(dataLen) { + if uint64(len(item.Value)) != dataLen { return nil, fmt.Errorf("data block length mismatch: %w", ErrMalformedResponse) } @@ -413,12 +414,15 @@ func parseValueLine(line []byte, item *Item, withCas bool) (dataLen uint64, err // the 'i' is the index of space or the last byte. switch nField { case keyIndex: - item.Key = unsafeByteSliceToString(line[fieldStart:i]) + item.Key = byteSliceToString(line[fieldStart:i]) case flagsIndex: flags, err := parseUintFromBytes(line[fieldStart:i]) if err != nil { return 0, fmt.Errorf("invalid flags: %w", ErrMalformedResponse) } + if flags > math.MaxUint32 { + return 0, fmt.Errorf("flags exceed uint32: %w", ErrMalformedResponse) + } item.Flags = uint32(flags) case dataLenIndex: si := i @@ -466,7 +470,11 @@ func parseUintFromBytes(bs []byte) (uint64, error) { return 0, fmt.Errorf("invalid uint number: %w", ErrMalformedResponse) } - r = r*10 + uint64(b-'0') + digit := uint64(b - '0') + if r > (math.MaxUint64-digit)/10 { + return 0, fmt.Errorf("uint number overflows uint64: %w", ErrMalformedResponse) + } + r = r*10 + digit } return r, nil @@ -690,7 +698,6 @@ func buildStatsCommand(subCommand string) (*request, *response) { return req, resp } -//nolint:unused func buildRawCommand(rawCommand string, indicator responseEndIndicator, lines int) (*request, *response) { _, _, _ = rawCommand, indicator, lines panic("IMPLEMENT ME!!!") diff --git a/protocol_bin.go b/protocol_bin.go index 824cd39..d434a24 100644 --- a/protocol_bin.go +++ b/protocol_bin.go @@ -125,36 +125,44 @@ type binaryRequest struct { } func (br *binaryRequest) send(w io.Writer) error { - nKey := uint16(len(br.key)) - nValue := uint32(len(br.value)) - nExtras := uint8(len(br.extras)) + keyLen := uint64(len(br.key)) + valueLen := uint64(len(br.value)) + extrasLen := uint64(len(br.extras)) + bodyLen := keyLen + valueLen + extrasLen - buf := make([]byte, 24+uint32(nExtras)+uint32(nKey)+nValue) - - buf[0] = _binaryMagicReq - buf[1] = br.opcode - binary.BigEndian.PutUint16(buf[2:4], nKey) // key length - buf[4] = nExtras // extras length - buf[5] = _binaryDataTypeRawBytes // data type - binary.BigEndian.PutUint16(buf[6:8], uint16(0)) // vbucket id - binary.BigEndian.PutUint32(buf[8:12], uint32(nKey)+nValue+uint32(nExtras)) // total body length - binary.BigEndian.PutUint32(buf[12:16], br.opaque) // opaque - binary.BigEndian.PutUint64(buf[16:24], br.cas) // cas - - s := uint32(24) - if nExtras > 0 { - copy(buf[s:s+uint32(nExtras)], br.extras) - s += uint32(nExtras) + if keyLen > maxKeySize { + return ErrInvalidKey } - - if nKey > 0 { - copy(buf[s:s+uint32(nKey)], br.key) - s += uint32(nKey) + if valueLen > maxValueSize { + return ErrInvalidValue } - if nValue > 0 { - copy(buf[s:], br.value) + if extrasLen > 255 { + return fmt.Errorf("binary extras exceed uint8: %w", ErrInvalidBinaryProtocol) + } + if bodyLen > maxValueSize { + return fmt.Errorf("binary body exceeds uint32: %w", ErrInvalidBinaryProtocol) } + nKey := uint16(keyLen) + nExtras := uint8(extrasLen) + nBody := uint32(bodyLen) + + buf := make([]byte, 24) + + buf[0] = _binaryMagicReq + buf[1] = br.opcode + binary.BigEndian.PutUint16(buf[2:4], nKey) // key length + buf[4] = nExtras // extras length + buf[5] = _binaryDataTypeRawBytes // data type + binary.BigEndian.PutUint16(buf[6:8], uint16(0)) // vbucket id + binary.BigEndian.PutUint32(buf[8:12], nBody) // total body length + binary.BigEndian.PutUint32(buf[12:16], br.opaque) // opaque + binary.BigEndian.PutUint64(buf[16:24], br.cas) // cas + + buf = append(buf, br.extras...) + buf = append(buf, br.key...) + buf = append(buf, br.value...) + _, err := w.Write(buf) return err } diff --git a/protocol_builder.go b/protocol_builder.go index 949387e..106cb3b 100644 --- a/protocol_builder.go +++ b/protocol_builder.go @@ -10,7 +10,6 @@ import ( "strconv" "sync" "time" - "unsafe" ) const ( @@ -79,9 +78,16 @@ func forecastCommonFaultLine(line []byte) error { } const ( - // defaultBufferSize is the default size of the buffer. - // TODO: It is used to avoid the buffer growth, but is 64B the most common case? + // defaultBufferSize covers common short text/meta command headers and + // small payloads without growth. Measured lengths with the builder's + // spacing rules: get/delete ≈14-17B, small set ≈32B, meta set with + // several flags ≈42B. Larger storage values grow once; oversized + // buffers are discarded on release via maxPooledBufferCap. defaultBufferSize = 64 + + // maxPooledBufferCap prevents large storage values from permanently + // retaining oversized buffers in the pool. + maxPooledBufferCap = 4 << 10 // 4 KiB ) var ( @@ -127,8 +133,10 @@ func newProtocolBuilder() *protocolBuilder { func (b *protocolBuilder) release() { if b.buf != nil { - b.buf.Reset() - bufferPool.Put(b.buf) + if b.buf.Cap() <= maxPooledBufferCap { + b.buf.Reset() + bufferPool.Put(b.buf) + } b.buf = nil } @@ -188,7 +196,8 @@ func (b *protocolBuilder) AddFlagUint(flag string, tok uint64) *protocolBuilder return b } - b.buf.WriteString(flag + strconv.FormatUint(tok, 10)) + b.buf.WriteString(flag) + b.buf.WriteString(strconv.FormatUint(tok, 10)) b.buf.WriteByte(_SpaceByte) return b } @@ -198,7 +207,8 @@ func (b *protocolBuilder) AddFlagString(flag, tok string) *protocolBuilder { return b } - b.buf.WriteString(flag + tok) + b.buf.WriteString(flag) + b.buf.WriteString(tok) b.buf.WriteByte(_SpaceByte) return b } @@ -225,10 +235,6 @@ func trimCRLF(line []byte) []byte { return bytes.TrimSuffix(line, _CRLFBytes) } -func withCRLF(bs []byte) []byte { - return append(bs, _CRLFBytes...) -} - var requestPool = sync.Pool{ New: func() any { return &request{ @@ -548,16 +554,6 @@ func base64Encode(src []byte) []byte { return dst } -func base64Decode(src []byte) ([]byte, error) { - dst := make([]byte, base64.StdEncoding.DecodedLen(len(src))) - n, err := base64.StdEncoding.Decode(dst, src) - if err != nil { - return nil, err - } - - return dst[:n], nil -} - func releaseReqAndResp(req *request, resp *response) { if req != nil { req.release() @@ -568,10 +564,6 @@ func releaseReqAndResp(req *request, resp *response) { } } -func unsafeStringToByteSlice(s string) []byte { - return unsafe.Slice(unsafe.StringData(s), len(s)) -} - -func unsafeByteSliceToString(bs []byte) string { - return unsafe.String(unsafe.SliceData(bs), len(bs)) +func byteSliceToString(bs []byte) string { + return string(bs) } diff --git a/protocol_builder_test.go b/protocol_builder_test.go index 5b158a5..6b43faf 100644 --- a/protocol_builder_test.go +++ b/protocol_builder_test.go @@ -1,6 +1,7 @@ package memcached import ( + "bytes" "context" "testing" "time" @@ -16,6 +17,121 @@ func Test_protocolBuilder(t *testing.T) { AddString("value") expected := "set key 0 0 5\r\nvalue\r\n" assert.Equal(t, expected, string(builder.build())) + builder.release() +} + +func Test_protocolBuilder_releaseDiscardsOversizedBuffer(t *testing.T) { + large := bytes.NewBuffer(make([]byte, 0, maxPooledBufferCap+1024)) + _, _ = large.Write(make([]byte, maxPooledBufferCap+1)) + b := &protocolBuilder{buf: large} + b.release() + assert.Nil(t, b.buf) + + // Oversized buffers must not be reused via the pool. + for i := 0; i < 32; i++ { + pb := newProtocolBuilder() + capSize := pb.buf.Cap() + pb.release() + assert.LessOrEqual(t, capSize, maxPooledBufferCap) + } +} + +func Test_protocolBuilder_shortCommandsFitDefaultCap(t *testing.T) { + cases := []struct { + name string + build func() *protocolBuilder + }{ + { + name: "get", + build: func() *protocolBuilder { + return newProtocolBuilder().AddString("get").AddString("test_key").AddCRLF() + }, + }, + { + name: "delete", + build: func() *protocolBuilder { + return newProtocolBuilder().AddString("delete").AddString("test_key").AddCRLF() + }, + }, + { + name: "set small", + build: func() *protocolBuilder { + value := []byte("test_value") + return newProtocolBuilder(). + AddString("set").AddString("test_key").AddInt(0).AddInt(0).AddInt(len(value)). + AddCRLF(). + AddBytes(value) + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + pb := tc.build() + defer pb.release() + assert.LessOrEqual(t, pb.buf.Len(), defaultBufferSize) + assert.LessOrEqual(t, pb.buf.Cap(), maxPooledBufferCap) + }) + } +} + +func BenchmarkProtocolBuilder_GetShortKey(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + pb := newProtocolBuilder().AddString("get").AddString("test_key").AddCRLF() + _ = pb.build() + pb.release() + } +} + +func BenchmarkProtocolBuilder_DeleteShortKey(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + pb := newProtocolBuilder().AddString("delete").AddString("test_key").AddCRLF() + _ = pb.build() + pb.release() + } +} + +func BenchmarkProtocolBuilder_SetSmallValue(b *testing.B) { + b.ReportAllocs() + value := []byte("test_value") + for i := 0; i < b.N; i++ { + pb := newProtocolBuilder(). + AddString("set").AddString("test_key").AddInt(0).AddInt(0).AddInt(len(value)). + AddCRLF(). + AddBytes(value) + _ = pb.build() + pb.release() + } +} + +func BenchmarkProtocolBuilder_MetaSetManyFlags(b *testing.B) { + b.ReportAllocs() + value := []byte("test_value") + for i := 0; i < b.N; i++ { + pb := newProtocolBuilder(). + AddString("ms").AddBytes([]byte("test_key")). + AddString("c").AddString("C").AddString("F").AddUint(1). + AddString("T").AddUint(60).AddString("S").AddInt(len(value)). + AddCRLF(). + AddBytes(value) + _ = pb.build() + pb.release() + } +} + +func BenchmarkProtocolBuilder_SetLargeValue(b *testing.B) { + b.ReportAllocs() + value := make([]byte, 4096) + for i := 0; i < b.N; i++ { + pb := newProtocolBuilder(). + AddString("set").AddString("test_key").AddInt(0).AddInt(0).AddInt(len(value)). + AddCRLF(). + AddBytes(value) + _ = pb.build() + pb.release() + } } func Test_selectProximateDeadline(t *testing.T) { diff --git a/protocol_meta.go b/protocol_meta.go index 85b53a8..aae49b9 100644 --- a/protocol_meta.go +++ b/protocol_meta.go @@ -3,6 +3,7 @@ package memcached import ( "bytes" "fmt" + "math" "strconv" ) @@ -382,19 +383,26 @@ func parseMetaItem(lines [][]byte, item *MetaItem, noReply bool, codec Codec) er } if !bytes.Equal(cd, []byte("VA")) { - parseFlags(parts, 1, item) - return nil + return parseFlags(parts, 1, item) } // VA handling - item.Size, _ = strconv.ParseUint(string(parts[dataLenIndex]), 10, 32) - parseFlags(parts, 2, item) + if len(parts) <= dataLenIndex { + return fmt.Errorf("missing value size: %w", ErrMalformedResponse) + } + size, err := strconv.ParseUint(string(parts[dataLenIndex]), 10, 64) + if err != nil { + return fmt.Errorf("invalid value size: %w", ErrMalformedResponse) + } + item.Size = size + if err := parseFlags(parts, 2, item); err != nil { + return err + } if len(lines) < 2 { return fmt.Errorf("missing value: %w", ErrMalformedResponse) } - var err error if item.Value, item.Flags, err = codec.Decode(item.Key, trimCRLF(lines[1]), item.Flags); err != nil { return fmt.Errorf("codec decode: %w", err) } @@ -406,38 +414,72 @@ func parseMetaItem(lines [][]byte, item *MetaItem, noReply bool, codec Codec) er // .e.g: // // HD c26 kZm9v b O456 s3\r\n -func parseFlags(parts [][]byte, startPos int, item *MetaItem) { - parseUint := func(b []byte) uint64 { - v, _ := strconv.ParseUint(string(b), 10, 64) - return v +func parseFlags(parts [][]byte, startPos int, item *MetaItem) error { + parseUint := func(flag byte, b []byte) (uint64, error) { + v, err := strconv.ParseUint(string(b), 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid %c flag: %w", flag, ErrMalformedResponse) + } + return v, nil } - parseInt := func(b []byte) int64 { - v, _ := strconv.ParseInt(string(b), 10, 64) - return v + parseInt := func(flag byte, b []byte) (int64, error) { + v, err := strconv.ParseInt(string(b), 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid %c flag: %w", flag, ErrMalformedResponse) + } + return v, nil } for i := startPos; i < len(parts); i++ { - switch parts[i][0] { + if len(parts[i]) == 0 { + return fmt.Errorf("empty meta flag: %w", ErrMalformedResponse) + } + + flag := parts[i][0] + value := parts[i][1:] + var err error + switch flag { case 'c': - item.CAS = parseUint(parts[i][1:]) + item.CAS, err = parseUint(flag, value) case 'f': - item.Flags = uint32(parseUint(parts[i][1:])) + var flags uint64 + flags, err = parseUint(flag, value) + if err == nil { + if flags > math.MaxUint32 { + return fmt.Errorf("f flag exceeds uint32: %w", ErrMalformedResponse) + } + item.Flags = uint32(flags) + } case 't': - item.TTL = parseInt(parts[i][1:]) + item.TTL, err = parseInt(flag, value) case 'l': - item.LastAccessedTime = int64(parseUint(parts[i][1:])) + var lastAccessed uint64 + lastAccessed, err = parseUint(flag, value) + if err == nil { + if lastAccessed > math.MaxInt64 { + return fmt.Errorf("l flag exceeds int64: %w", ErrMalformedResponse) + } + item.LastAccessedTime = int64(lastAccessed) + } case 's': - item.Size = parseUint(parts[i][1:]) + item.Size, err = parseUint(flag, value) case 'O': - item.Opaque = parseUint(parts[i][1:]) + item.Opaque, err = parseUint(flag, value) case 'h': - item.HitBefore = parseUint(parts[i][1:]) == 1 + var hitBefore uint64 + hitBefore, err = parseUint(flag, value) + item.HitBefore = hitBefore == 1 // NO need to parse key again in client. // case 'k': // item.Key = string(parts[i][1:]) } + if err != nil { + return err + } } + + return nil } // MetaDeleteOption is used to set options for MetaDelete command. diff --git a/protocol_meta_test.go b/protocol_meta_test.go index a8ad869..c3ed289 100644 --- a/protocol_meta_test.go +++ b/protocol_meta_test.go @@ -62,7 +62,7 @@ func Test_parseFlags(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - parseFlags(tt.args.parts, tt.args.startPos, tt.args.item) + require.NoError(t, parseFlags(tt.args.parts, tt.args.startPos, tt.args.item)) assert.Equal(t, tt.want, tt.args.item) }) } diff --git a/protocol_test.go b/protocol_test.go index e04d788..5282650 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -223,6 +223,30 @@ func Test_parseUintFromBytes(t *testing.T) { want: 1234567890, wantErr: false, }, + { + name: "max-uint64", + args: args{ + raw: []byte("18446744073709551615"), + }, + want: ^uint64(0), + wantErr: false, + }, + { + name: "overflow-uint64", + args: args{ + raw: []byte("18446744073709551616"), + }, + want: 0, + wantErr: true, + }, + { + name: "overflow-long-number", + args: args{ + raw: []byte("9999999999999999999999999999999999999999"), + }, + want: 0, + wantErr: true, + }, { name: "malformed-contains-letters", args: args{ From 2d48d4aab9f701b555bbb7778fa6e61f3155be9a Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:12:32 +0200 Subject: [PATCH 04/10] refactor: remove raw command passthrough Delete the undocumented Raw text-command API and document that callers must use typed client methods. Co-authored-by: Cursor --- README.md | 3 +++ client.go | 2 -- client_commands.go | 23 ----------------------- protocol.go | 5 ----- 4 files changed, 3 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ba30e9c..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 | diff --git a/client.go b/client.go index 438aa3e..3569357 100644 --- a/client.go +++ b/client.go @@ -21,8 +21,6 @@ type Client interface { basicTextProtocolCommander metaTextProtocolCommander statisticsTextProtocolCommander - // TODO: support rawTextProtocolCommander - // rawTextProtocolCommander } var _ Client = (*client)(nil) diff --git a/client_commands.go b/client_commands.go index cbcb73f..58eaf89 100644 --- a/client_commands.go +++ b/client_commands.go @@ -127,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 */ @@ -630,19 +623,3 @@ func (c *client) Stats(ctx context.Context) (*Statistic, error) { 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, fmt.Errorf("request failed: %w", err) - } - - 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/protocol.go b/protocol.go index 161d21f..0c0a858 100644 --- a/protocol.go +++ b/protocol.go @@ -697,8 +697,3 @@ func buildStatsCommand(subCommand string) (*request, *response) { return req, resp } - -func buildRawCommand(rawCommand string, indicator responseEndIndicator, lines int) (*request, *response) { - _, _, _ = rawCommand, indicator, lines - panic("IMPLEMENT ME!!!") -} From 5c3ed4575fa022da18b362eccfcc0b12d0436ea2 Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:14:45 +0200 Subject: [PATCH 05/10] fix(cli): harden context and history persistence Tighten config/history file permissions with OpenRoot, improve error propagation, and add persistence regression tests while keeping go-prompt. Co-authored-by: Cursor --- cmd/memcached-cli/commands.go | 102 +++++++++++++--------- cmd/memcached-cli/context_manager.go | 70 +++++++++++---- cmd/memcached-cli/context_manager_test.go | 42 +++++++++ cmd/memcached-cli/errors.go | 14 +++ cmd/memcached-cli/go.mod | 44 ++++++---- cmd/memcached-cli/go.sum | 74 ++++++++++------ cmd/memcached-cli/history.go | 37 +++++--- cmd/memcached-cli/history_test.go | 59 +++++++++++++ cmd/memcached-cli/main.go | 14 +-- cmd/memcached-cli/repl.go | 74 ++++++++++------ 10 files changed, 386 insertions(+), 144 deletions(-) create mode 100644 cmd/memcached-cli/context_manager_test.go create mode 100644 cmd/memcached-cli/errors.go create mode 100644 cmd/memcached-cli/history_test.go 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..669f1d6 100644 --- a/cmd/memcached-cli/go.mod +++ b/cmd/memcached-cli/go.mod @@ -1,29 +1,41 @@ module github.com/yeqown/memcached/cmd/memcached-cli -go 1.26.0 +go 1.26.5 require ( 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 + github.com/cespare/xxhash/v2 v2.3.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/klauspost/compress v1.19.2 // indirect + github.com/mattn/go-colorable v0.1.7 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-tty v0.0.3 // indirect + github.com/pierrec/lz4/v4 v4.1.28 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pkg/term v1.2.0-beta.2 // 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/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.9 // 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/sys v0.0.0-20200918174421-af09f7315aff // indirect + golang.org/x/text v0.22.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..00ff95b 100644 --- a/cmd/memcached-cli/go.sum +++ b/cmd/memcached-cli/go.sum @@ -2,55 +2,73 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/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/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= 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 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 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 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 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/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 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/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= +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/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= @@ -58,13 +76,13 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w 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 h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= 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= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 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..22b80e6 100644 --- a/cmd/memcached-cli/main.go +++ b/cmd/memcached-cli/main.go @@ -9,7 +9,6 @@ import ( "github.com/MakeNowJust/heredoc" prompt "github.com/c-bata/go-prompt" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/yeqown/log" ) @@ -85,13 +84,15 @@ 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) } // 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() @@ -105,7 +106,7 @@ 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") + return fmt.Errorf("failed to get current context: %w", err) } if len(contexts) > 0 && current == nil { @@ -256,7 +257,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..b675f75 100644 --- a/cmd/memcached-cli/repl.go +++ b/cmd/memcached-cli/repl.go @@ -9,7 +9,6 @@ import ( "time" "github.com/c-bata/go-prompt" - "github.com/pkg/errors" "github.com/yeqown/memcached" ) @@ -110,13 +109,8 @@ func (r *replCommander) commandExecutor(line string) { } } -func (r *replCommander) getMemcachedClient() memcached.Client { - client, err := r.cm.getCurrentClient() - if err != nil { - panic(err) - } - - return client +func (r *replCommander) getMemcachedClient() (memcached.Client, error) { + return r.cm.getCurrentClient() } /** @@ -161,7 +155,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 +172,9 @@ func (r *replCommander) handleGet(ctx context.Context, args []string) error { memcached.MetaGetFlagReturnHitBefore(), ) if err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(os.Stdout, err) } - printMetaItem(item) - return nil + return printMetaItem(item) } func (r *replCommander) handleSet(ctx context.Context, args []string) error { @@ -192,8 +189,12 @@ 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 + } + if err := client.Set(ctx, args[1], []byte(args[2]), magicFlags, expiration); err != nil { + return ignoreMemcachedError(os.Stdout, err) } fmt.Println("OK") return nil @@ -204,8 +205,12 @@ 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 + } + if err := client.Delete(ctx, args[1]); err != nil { + return ignoreMemcachedError(os.Stdout, err) } fmt.Println("OK") return nil @@ -222,9 +227,13 @@ 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 err + } + newValue, err := client.Incr(ctx, args[1], delta) if err != nil { - return ignoreMemcachedError(err) + return ignoreMemcachedError(os.Stdout, err) } fmt.Printf("%d\n", newValue) return nil @@ -241,9 +250,13 @@ 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 ignoreMemcachedError(err) + return err + } + newValue, err := client.Decr(ctx, args[1], delta) + if err != nil { + return ignoreMemcachedError(os.Stdout, err) } fmt.Printf("%d\n", newValue) return nil @@ -258,8 +271,12 @@ 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 + } + if err := client.Touch(ctx, args[1], time.Duration(expiration)*time.Second); err != nil { + return ignoreMemcachedError(os.Stdout, err) } fmt.Println("OK") return nil @@ -273,9 +290,14 @@ func (r *replCommander) handleMGet(ctx context.Context, args []string) error { keys := make([]string, len(args)-1) copy(keys, args[1:]) + client, err := r.getMemcachedClient() + if err != nil { + return err + } + items := make([]*memcached.MetaItem, 0, len(keys)) for _, key := range keys { - item, err := r.getMemcachedClient().MetaGet( + item, err := client.MetaGet( ctx, []byte(key), memcached.MetaGetFlagReturnTTL(), @@ -288,16 +310,14 @@ 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)) + fmt.Printf("Encounter error while getting key '%s': %v\n", key, rootErr(err)) continue } items = append(items, item) } - printMetaItems(items) - - return nil + return printMetaItems(items) } /** From c5b43900e67991af44c0c4ba973763174007af71 Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:15:03 +0200 Subject: [PATCH 06/10] feat(cli): replace go-prompt REPL with Bubble Tea Migrate the interactive CLI to a Bubble Tea TUI with captured output, history navigation, and clean shutdown without os.Exit. Co-authored-by: Cursor --- cmd/memcached-cli/go.mod | 30 +++-- cmd/memcached-cli/go.sum | 76 +++++++----- cmd/memcached-cli/main.go | 33 +++-- cmd/memcached-cli/repl.go | 195 ++++++++++++++++------------- cmd/memcached-cli/repl_test.go | 220 +++++++++++++++++++++++++++++++++ cmd/memcached-cli/repl_tui.go | 175 ++++++++++++++++++++++++++ 6 files changed, 586 insertions(+), 143 deletions(-) create mode 100644 cmd/memcached-cli/repl_test.go create mode 100644 cmd/memcached-cli/repl_tui.go diff --git a/cmd/memcached-cli/go.mod b/cmd/memcached-cli/go.mod index 669f1d6..1f7fa9a 100644 --- a/cmd/memcached-cli/go.mod +++ b/cmd/memcached-cli/go.mod @@ -3,8 +3,9 @@ module github.com/yeqown/memcached/cmd/memcached-cli 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/samber/lo v1.53.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 @@ -13,28 +14,39 @@ require ( ) require ( + 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/klauspost/compress v1.19.2 // indirect - github.com/mattn/go-colorable v0.1.7 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/mattn/go-tty v0.0.3 // 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/pkg/term v1.2.0-beta.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/rivo/uniseg v0.4.7 // 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/sys v0.0.0-20200918174421-af09f7315aff // indirect - golang.org/x/text v0.22.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 ) diff --git a/cmd/memcached-cli/go.sum b/cmd/memcached-cli/go.sum index 00ff95b..aec5b83 100644 --- a/cmd/memcached-cli/go.sum +++ b/cmd/memcached-cli/go.sum @@ -1,9 +1,35 @@ +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= @@ -25,26 +51,20 @@ 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/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -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 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -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/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.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= @@ -52,12 +72,15 @@ 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 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 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/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= @@ -69,17 +92,14 @@ go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq 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/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 h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +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= diff --git a/cmd/memcached-cli/main.go b/cmd/memcached-cli/main.go index 22b80e6..93c1ecd 100644 --- a/cmd/memcached-cli/main.go +++ b/cmd/memcached-cli/main.go @@ -2,13 +2,13 @@ package main import ( "context" + "errors" "fmt" "os" "strings" "time" "github.com/MakeNowJust/heredoc" - prompt "github.com/c-bata/go-prompt" "github.com/spf13/cobra" "github.com/yeqown/log" ) @@ -17,9 +17,7 @@ const ( version = "v1.3.1" ) -var ( - logger = newLogger() -) +var logger = newLogger() func main() { var ( @@ -72,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 @@ -86,6 +84,13 @@ func runAsREPL(timeout time.Duration, servers, hashStrategy string) error { if err != nil { 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 != "" { @@ -104,9 +109,9 @@ func runAsREPL(timeout time.Duration, servers, hashStrategy string) error { `)) } - current, err := manager.getCurrentContext() - if err != nil { - return fmt.Errorf("failed to get current context: %w", err) + var current *Context + if len(contexts) > 0 { + current, _ = manager.getCurrentContext() } if len(contexts) > 0 && current == nil { @@ -128,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 { @@ -181,7 +177,6 @@ func newContextCommand() *cobra.Command { } func newKVCommand() *cobra.Command { - var contextName string cmd := &cobra.Command{ diff --git a/cmd/memcached-cli/repl.go b/cmd/memcached-cli/repl.go index b675f75..ac2c98d 100644 --- a/cmd/memcached-cli/repl.go +++ b/cmd/memcached-cli/repl.go @@ -1,20 +1,21 @@ package main import ( + "bytes" "context" "fmt" - "os" + "io" "strconv" "strings" "time" - "github.com/c-bata/go-prompt" "github.com/yeqown/memcached" ) type replCommander struct { cm *contextManager timeout time.Duration + out io.Writer } func newREPLCommander(manager *contextManager, timeout time.Duration) (*replCommander, error) { @@ -25,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] @@ -71,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) @@ -78,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": @@ -93,19 +102,29 @@ 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, + } + } + } + + return commandResult{ + output: buf.String(), + quit: quit, } } @@ -117,7 +136,7 @@ func (r *replCommander) getMemcachedClient() (memcached.Client, error) { * 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 ") } @@ -128,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 @@ -141,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) } /** @@ -172,9 +193,9 @@ func (r *replCommander) handleGet(ctx context.Context, args []string) error { memcached.MetaGetFlagReturnHitBefore(), ) if err != nil { - return ignoreMemcachedError(os.Stdout, err) + return ignoreMemcachedError(r.out, err) } - return printMetaItem(item) + return writeMetaItem(r.out, item) } func (r *replCommander) handleSet(ctx context.Context, args []string) error { @@ -194,10 +215,10 @@ func (r *replCommander) handleSet(ctx context.Context, args []string) error { return err } if err := client.Set(ctx, args[1], []byte(args[2]), magicFlags, expiration); err != nil { - return ignoreMemcachedError(os.Stdout, err) + return ignoreMemcachedError(r.out, err) } - fmt.Println("OK") - return nil + _, err = fmt.Fprintln(r.out, "OK") + return err } func (r *replCommander) handleDelete(ctx context.Context, args []string) error { @@ -210,10 +231,10 @@ func (r *replCommander) handleDelete(ctx context.Context, args []string) error { return err } if err := client.Delete(ctx, args[1]); err != nil { - return ignoreMemcachedError(os.Stdout, err) + return ignoreMemcachedError(r.out, err) } - fmt.Println("OK") - return nil + _, err = fmt.Fprintln(r.out, "OK") + return err } func (r *replCommander) handleIncr(ctx context.Context, args []string) error { @@ -233,10 +254,9 @@ func (r *replCommander) handleIncr(ctx context.Context, args []string) error { } newValue, err := client.Incr(ctx, args[1], delta) if err != nil { - return ignoreMemcachedError(os.Stdout, err) + return ignoreMemcachedError(r.out, err) } - fmt.Printf("%d\n", newValue) - return nil + return writeOutput(r.out, "%d\n", newValue) } func (r *replCommander) handleDecr(ctx context.Context, args []string) error { @@ -256,10 +276,9 @@ func (r *replCommander) handleDecr(ctx context.Context, args []string) error { } newValue, err := client.Decr(ctx, args[1], delta) if err != nil { - return ignoreMemcachedError(os.Stdout, 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 { @@ -276,26 +295,24 @@ func (r *replCommander) handleTouch(ctx context.Context, args []string) error { return err } if err := client.Touch(ctx, args[1], time.Duration(expiration)*time.Second); err != nil { - return ignoreMemcachedError(os.Stdout, err) + return ignoreMemcachedError(r.out, err) } - fmt.Println("OK") - return nil + 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 } - - items := make([]*memcached.MetaItem, 0, len(keys)) for _, key := range keys { item, err := client.MetaGet( ctx, @@ -310,14 +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, rootErr(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) } - return printMetaItems(items) + return writeMetaItems(r.out, items) } /** @@ -325,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)) + } +} From 8a17df6b15bcfe572d400e3abe2775cefeba3423 Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:15:22 +0200 Subject: [PATCH 07/10] chore: refresh examples and benchmarks Move examples into dedicated main packages, fix the concurrent example, and update benchmark module paths, dependencies, and docs. Co-authored-by: Cursor --- benchmark/README.md | 9 ++-- benchmark/client_test.go | 31 ++++-------- benchmark/concurrent_benchmark_test.go | 6 ++- benchmark/go.mod | 25 ++++++---- benchmark/go.sum | 37 ++++++++++----- benchmark/mem_benchmark_test.go | 31 ++---------- example/{cas.go => cas/main.go} | 0 example/{cocurrent.go => concurrent/main.go} | 5 +- example/{flushall.go => flushall/main.go} | 0 example/go.mod | 28 +++++------ example/go.sum | 50 ++++++++++---------- example/{noreply.go => noreply/main.go} | 2 +- example/{options.go => options/main.go} | 7 +-- example/otel/main.go | 18 +++++-- example/{simple.go => simple/main.go} | 9 ++-- example/{udp.go => udp/main.go} | 11 +++-- 16 files changed, 136 insertions(+), 133 deletions(-) rename example/{cas.go => cas/main.go} (100%) rename example/{cocurrent.go => concurrent/main.go} (84%) rename example/{flushall.go => flushall/main.go} (100%) rename example/{noreply.go => noreply/main.go} (94%) rename example/{options.go => options/main.go} (83%) rename example/{simple.go => simple/main.go} (80%) rename example/{udp.go => udp/main.go} (81%) 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/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)) } From d599ab499e8110a8167cb7ba1fe983e78bc20aef Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:15:44 +0200 Subject: [PATCH 08/10] fix(gui): harden connection config and lifecycle Use OpenRoot with restrictive permissions for context files and propagate connection close errors during connect and disconnect. Co-authored-by: Cursor --- gui/service/connection.go | 71 ++++++++++++++++++++-------------- gui/service/operations_test.go | 16 -------- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/gui/service/connection.go b/gui/service/connection.go index 0f586bc..8e84e5e 100644 --- a/gui/service/connection.go +++ b/gui/service/connection.go @@ -3,6 +3,7 @@ package service import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -13,6 +14,8 @@ import ( memcached "github.com/yeqown/memcached" ) +const configFileName = "contexts.json" + // ConnectionService manages memcached contexts and connections. type ConnectionService struct { mu sync.Mutex @@ -30,7 +33,7 @@ func NewConnectionService() (*ConnectionService, error) { } configDir = filepath.Join(configDir, "memcached-gui") - if err := os.MkdirAll(configDir, 0755); err != nil { + if err := os.MkdirAll(configDir, 0o700); err != nil { return nil, fmt.Errorf("failed to create config dir: %w", err) } @@ -39,28 +42,15 @@ func NewConnectionService() (*ConnectionService, error) { }, nil } -func (s *ConnectionService) configPath() string { - return filepath.Join(s.configDir, "contexts.json") -} - // LoadContexts loads saved contexts from the config file. func (s *ConnectionService) LoadContexts() ([]Context, error) { s.mu.Lock() defer s.mu.Unlock() - path := s.configPath() - data, err := os.ReadFile(path) + contexts, err := s.loadContextsLocked() if err != nil { - if os.IsNotExist(err) { - return []Context{}, nil - } return nil, fmt.Errorf("failed to read config: %w", err) } - - var contexts []Context - if err := json.Unmarshal(data, &contexts); err != nil { - return nil, fmt.Errorf("failed to parse config: %w", err) - } return contexts, nil } @@ -120,10 +110,8 @@ func (s *ConnectionService) Connect(ctxID string) error { defer s.mu.Unlock() // Disconnect existing connection - if s.client != nil { - s.client.Close() - s.client = nil - s.connected = false + if err := s.disconnectLocked(); err != nil { + return fmt.Errorf("failed to close existing connection: %w", err) } contexts, err := s.loadContextsLocked() @@ -162,8 +150,10 @@ func (s *ConnectionService) Connect(ctxID string) error { defer cancel() if _, err := client.Version(verifyCtx); err != nil { - client.Close() - return fmt.Errorf("failed to connect to %s: %w", addr, err) + return errors.Join( + fmt.Errorf("failed to connect to %s: %w", addr, err), + client.Close(), + ) } s.client = client @@ -177,13 +167,18 @@ func (s *ConnectionService) Disconnect() error { s.mu.Lock() defer s.mu.Unlock() + return s.disconnectLocked() +} + +func (s *ConnectionService) disconnectLocked() error { + var err error if s.client != nil { - s.client.Close() + err = s.client.Close() s.client = nil } s.connected = false s.activeCtx = nil - return nil + return err } // IsConnected returns whether there's an active connection. @@ -204,26 +199,44 @@ func (s *ConnectionService) GetClient() (memcached.Client, error) { return s.client, nil } -func (s *ConnectionService) loadContextsLocked() ([]Context, error) { - path := s.configPath() - data, err := os.ReadFile(path) +func (s *ConnectionService) loadContextsLocked() (contexts []Context, retErr error) { + root, err := os.OpenRoot(s.configDir) + if err != nil { + return nil, err + } + defer func() { + retErr = errors.Join(retErr, root.Close()) + }() + + data, err := root.ReadFile(configFileName) if err != nil { if os.IsNotExist(err) { return []Context{}, nil } return nil, err } - var contexts []Context if err := json.Unmarshal(data, &contexts); err != nil { return nil, err } return contexts, nil } -func (s *ConnectionService) saveContextsLocked(contexts []Context) error { +func (s *ConnectionService) saveContextsLocked(contexts []Context) (retErr error) { data, err := json.MarshalIndent(contexts, "", " ") if err != nil { return err } - return os.WriteFile(s.configPath(), data, 0644) + + root, err := os.OpenRoot(s.configDir) + if err != nil { + return err + } + defer func() { + retErr = errors.Join(retErr, root.Close()) + }() + + if err := root.WriteFile(configFileName, data, 0o600); err != nil { + return err + } + return root.Chmod(configFileName, 0o600) } diff --git a/gui/service/operations_test.go b/gui/service/operations_test.go index 61ade5d..b082459 100644 --- a/gui/service/operations_test.go +++ b/gui/service/operations_test.go @@ -9,22 +9,6 @@ import ( memcached "github.com/yeqown/memcached" ) -type structuredOperationResult struct { - Success bool `json:"success"` - Data string `json:"data"` - Error string `json:"error"` - Key string `json:"key"` - Value string `json:"value"` - TTL int64 `json:"ttl"` - LastAccessedTime int64 `json:"lastAccessedTime"` - CAS uint64 `json:"cas"` - Flags uint32 `json:"flags"` - Size uint64 `json:"size"` - HitBefore bool `json:"hitBefore"` - Opaque uint64 `json:"opaque"` - ValueKind string `json:"valueKind"` -} - type fakeMemcachedClient struct { getCalled bool metaGetCalled bool From e819b51d30b967ddfa56dc28b73e7a45ea2889b3 Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:15:47 +0200 Subject: [PATCH 09/10] chore(gui): align module path and upgrade Wails Rename the GUI module to github.com/yeqown/memcached/gui and upgrade Wails to v2.14.0 with matching dependency updates. Co-authored-by: Cursor --- gui/go.mod | 35 +++++++++++++++++-------------- gui/go.sum | 59 +++++++++++++++++++++++++++++++---------------------- gui/main.go | 3 +-- 3 files changed, 56 insertions(+), 41 deletions(-) diff --git a/gui/go.mod b/gui/go.mod index 8ad7081..2cd537e 100644 --- a/gui/go.mod +++ b/gui/go.mod @@ -1,25 +1,27 @@ -module memcached-gui +module github.com/yeqown/memcached/gui -go 1.26.0 +go 1.26.5 require ( github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 - github.com/wailsapp/wails/v2 v2.12.0 - github.com/yeqown/memcached v1.3.4 + github.com/wailsapp/wails/v2 v2.14.0 + github.com/yeqown/memcached v1.4.0 ) require ( git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect github.com/bep/debounce v1.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.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/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/klauspost/compress v1.19.2 // indirect github.com/labstack/echo/v4 v4.13.3 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect @@ -28,23 +30,26 @@ require ( github.com/leaanthony/u v1.1.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pierrec/lz4/v4 v4.1.28 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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/rogpeppe/go-internal v1.14.1 // indirect - github.com/samber/lo v1.49.1 // indirect + github.com/samber/lo v1.53.0 // indirect github.com/tkrajina/go-reflector v0.5.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.23.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // 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/crypto v0.53.0 // indirect + golang.org/x/net v0.56.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/wailsapp/wails/v2 v2.12.0 => /Users/apple/.go/pkg/mod +replace github.com/yeqown/memcached => ../ diff --git a/gui/go.sum b/gui/go.sum index 1c5d177..7a1758f 100644 --- a/gui/go.sum +++ b/gui/go.sum @@ -2,28 +2,33 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/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/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -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/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= @@ -47,9 +52,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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/pierrec/lz4/v4 v4.1.28 h1:pPEPwRJ4kybBTfGt28q7lQsRJQHhC08axprdLD5Ppio= +github.com/pierrec/lz4/v4 v4.1.28/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -57,11 +63,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= -github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= 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/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= @@ -74,26 +79,32 @@ github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6N github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c= -github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg= -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/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +github.com/wailsapp/wails/v2 v2.14.0 h1:+GlF6BM8Mg7Saa1lxUAzfZ6jsEMg2zQhQvUHtqB7xpc= +github.com/wailsapp/wails/v2 v2.14.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M= +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= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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= diff --git a/gui/main.go b/gui/main.go index 28777ee..ad39cd8 100644 --- a/gui/main.go +++ b/gui/main.go @@ -7,7 +7,7 @@ import ( "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" - "memcached-gui/service" + "github.com/yeqown/memcached/gui/service" ) //go:embed all:frontend/dist @@ -39,7 +39,6 @@ func main() { opSvc, }, }) - if err != nil { println("Error:", err.Error()) } From 294f7fe8a1e579c0fb9cddf739a4a2c6b5711cb4 Mon Sep 17 00:00:00 2001 From: EyJunge1 Date: Tue, 11 Aug 2026 07:16:08 +0200 Subject: [PATCH 10/10] chore(gui/frontend): migrate to Svelte 5 Upgrade the GUI frontend to Svelte 5, Vite 8, and TypeScript 6 with reactive and accessibility fixes required by the new toolchain. Co-authored-by: Cursor --- gui/.gitignore | 1 + gui/frontend/package-lock.json | 1757 +++++++---------- gui/frontend/package.json | 20 +- gui/frontend/package.json.md5 | 2 +- .../src/components/CommandSelector.svelte | 2 +- gui/frontend/src/components/InputArea.svelte | 17 +- gui/frontend/src/components/JsonTree.svelte | 8 +- gui/frontend/src/components/KeyInput.svelte | 10 +- .../src/components/OperationPanel.svelte | 10 +- gui/frontend/src/components/Sidebar.svelte | 16 +- gui/frontend/src/components/StatsCards.svelte | 6 - .../src/components/ThemeToggle.svelte | 16 +- .../src/components/ValueDisplay.svelte | 14 +- gui/frontend/src/main.ts | 10 +- gui/frontend/svelte.config.js | 6 +- gui/frontend/tsconfig.json | 1 - 16 files changed, 755 insertions(+), 1141 deletions(-) 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 @@