fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+436
@@ -0,0 +1,436 @@
|
||||
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file is for SSE events.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events.
|
||||
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// If true, MemoryEventStore will do frequent validation to check invariants, slowing it down.
|
||||
// Enable for debugging.
|
||||
const validateMemoryEventStore = false
|
||||
|
||||
// An Event is a server-sent event.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields.
|
||||
type Event struct {
|
||||
Name string // the "event" field
|
||||
ID string // the "id" field
|
||||
Data []byte // the "data" field
|
||||
Retry string // the "retry" field
|
||||
}
|
||||
|
||||
// Empty reports whether the Event is empty.
|
||||
func (e Event) Empty() bool {
|
||||
return e.Name == "" && e.ID == "" && len(e.Data) == 0 && e.Retry == ""
|
||||
}
|
||||
|
||||
// writeEvent writes the event to w, and flushes.
|
||||
func writeEvent(w io.Writer, evt Event) (int, error) {
|
||||
var b bytes.Buffer
|
||||
if evt.Name != "" {
|
||||
fmt.Fprintf(&b, "event: %s\n", evt.Name)
|
||||
}
|
||||
if evt.ID != "" {
|
||||
fmt.Fprintf(&b, "id: %s\n", evt.ID)
|
||||
}
|
||||
if evt.Retry != "" {
|
||||
fmt.Fprintf(&b, "retry: %s\n", evt.Retry)
|
||||
}
|
||||
fmt.Fprintf(&b, "data: %s\n\n", string(evt.Data))
|
||||
n, err := w.Write(b.Bytes())
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// scanEvents iterates SSE events in the given scanner. The iterated error is
|
||||
// terminal: if encountered, the stream is corrupt or broken and should no
|
||||
// longer be used.
|
||||
//
|
||||
// TODO(rfindley): consider a different API here that makes failure modes more
|
||||
// apparent.
|
||||
func scanEvents(r io.Reader) iter.Seq2[Event, error] {
|
||||
reader := bufio.NewReader(r)
|
||||
|
||||
// TODO: investigate proper behavior when events are out of order, or have
|
||||
// non-standard names.
|
||||
var (
|
||||
eventKey = []byte("event")
|
||||
idKey = []byte("id")
|
||||
dataKey = []byte("data")
|
||||
retryKey = []byte("retry")
|
||||
)
|
||||
|
||||
return func(yield func(Event, error) bool) {
|
||||
// iterate event from the wire.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#examples
|
||||
//
|
||||
// - `key: value` line records.
|
||||
// - Consecutive `data: ...` fields are joined with newlines.
|
||||
// - Unrecognized fields are ignored. Since we only care about 'event', 'id', and
|
||||
// 'data', these are the only three we consider.
|
||||
// - Lines starting with ":" are ignored.
|
||||
// - Records are terminated with two consecutive newlines.
|
||||
var (
|
||||
evt Event
|
||||
dataBuf *bytes.Buffer // if non-nil, preceding field was also data
|
||||
)
|
||||
yieldEvent := func() bool {
|
||||
if dataBuf != nil {
|
||||
evt.Data = dataBuf.Bytes()
|
||||
dataBuf = nil
|
||||
}
|
||||
if evt.Empty() {
|
||||
return true
|
||||
}
|
||||
if !yield(evt, nil) {
|
||||
return false
|
||||
}
|
||||
evt = Event{}
|
||||
return true
|
||||
}
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
yield(Event{}, fmt.Errorf("error reading event: %v", err))
|
||||
return
|
||||
}
|
||||
line = bytes.TrimRight(line, "\r\n")
|
||||
isEOF := errors.Is(err, io.EOF)
|
||||
|
||||
if len(line) == 0 {
|
||||
if !yieldEvent() {
|
||||
return
|
||||
}
|
||||
if isEOF {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
before, after, found := bytes.Cut(line, []byte{':'})
|
||||
if !found {
|
||||
yield(Event{}, fmt.Errorf("%w: malformed line in SSE stream: %q", errMalformedEvent, string(line)))
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case bytes.Equal(before, eventKey):
|
||||
evt.Name = strings.TrimSpace(string(after))
|
||||
case bytes.Equal(before, idKey):
|
||||
evt.ID = strings.TrimSpace(string(after))
|
||||
case bytes.Equal(before, retryKey):
|
||||
evt.Retry = strings.TrimSpace(string(after))
|
||||
case bytes.Equal(before, dataKey):
|
||||
data := bytes.TrimSpace(after)
|
||||
if dataBuf == nil {
|
||||
dataBuf = new(bytes.Buffer)
|
||||
} else {
|
||||
dataBuf.WriteByte('\n')
|
||||
}
|
||||
dataBuf.Write(data)
|
||||
}
|
||||
|
||||
if isEOF {
|
||||
yieldEvent()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An EventStore tracks data for SSE streams.
|
||||
// A single EventStore suffices for all sessions, since session IDs are
|
||||
// globally unique. So one EventStore can be created per process, for
|
||||
// all Servers in the process.
|
||||
// Such a store is able to bound resource usage for the entire process.
|
||||
//
|
||||
// All of an EventStore's methods must be safe for use by multiple goroutines.
|
||||
type EventStore interface {
|
||||
// Open is called when a new stream is created. It may be used to ensure that
|
||||
// the underlying data structure for the stream is initialized, making it
|
||||
// ready to store and replay event streams.
|
||||
Open(_ context.Context, sessionID, streamID string) error
|
||||
|
||||
// Append appends data for an outgoing event to given stream, which is part of the
|
||||
// given session.
|
||||
Append(_ context.Context, sessionID, streamID string, data []byte) error
|
||||
|
||||
// After returns an iterator over the data for the given session and stream, beginning
|
||||
// just after the given index.
|
||||
//
|
||||
// Once the iterator yields a non-nil error, it will stop.
|
||||
// After's iterator must return an error immediately if any data after index was
|
||||
// dropped; it must not return partial results.
|
||||
// The stream must have been opened previously (see [EventStore.Open]).
|
||||
After(_ context.Context, sessionID, streamID string, index int) iter.Seq2[[]byte, error]
|
||||
|
||||
// SessionClosed informs the store that the given session is finished, along
|
||||
// with all of its streams.
|
||||
//
|
||||
// A store cannot rely on this method being called for cleanup. It should institute
|
||||
// additional mechanisms, such as timeouts, to reclaim storage.
|
||||
SessionClosed(_ context.Context, sessionID string) error
|
||||
|
||||
// There is no StreamClosed method. A server doesn't know when a stream is finished, because
|
||||
// the client can always send a GET with a Last-Event-ID referring to the stream.
|
||||
}
|
||||
|
||||
// A dataList is a list of []byte.
|
||||
// The zero dataList is ready to use.
|
||||
type dataList struct {
|
||||
size int // total size of data bytes
|
||||
first int // the stream index of the first element in data
|
||||
data [][]byte
|
||||
}
|
||||
|
||||
func (dl *dataList) appendData(d []byte) {
|
||||
// Empty data consumes memory but doesn't increment size. However, it should
|
||||
// be rare.
|
||||
dl.data = append(dl.data, d)
|
||||
dl.size += len(d)
|
||||
}
|
||||
|
||||
// removeFirst removes the first data item in dl, returning the size of the item.
|
||||
// It panics if dl is empty.
|
||||
func (dl *dataList) removeFirst() int {
|
||||
if len(dl.data) == 0 {
|
||||
panic("empty dataList")
|
||||
}
|
||||
r := len(dl.data[0])
|
||||
dl.size -= r
|
||||
dl.data[0] = nil // help GC
|
||||
dl.data = dl.data[1:]
|
||||
dl.first++
|
||||
return r
|
||||
}
|
||||
|
||||
// A MemoryEventStore is an [EventStore] backed by memory.
|
||||
type MemoryEventStore struct {
|
||||
mu sync.Mutex
|
||||
maxBytes int // max total size of all data
|
||||
nBytes int // current total size of all data
|
||||
store map[string]map[string]*dataList // session ID -> stream ID -> *dataList
|
||||
}
|
||||
|
||||
// MemoryEventStoreOptions are options for a [MemoryEventStore].
|
||||
type MemoryEventStoreOptions struct{}
|
||||
|
||||
// MaxBytes returns the maximum number of bytes that the store will retain before
|
||||
// purging data.
|
||||
func (s *MemoryEventStore) MaxBytes() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.maxBytes
|
||||
}
|
||||
|
||||
// SetMaxBytes sets the maximum number of bytes the store will retain before purging
|
||||
// data. The argument must not be negative. If it is zero, a suitable default will be used.
|
||||
// SetMaxBytes can be called at any time. The size of the store will be adjusted
|
||||
// immediately.
|
||||
func (s *MemoryEventStore) SetMaxBytes(n int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
switch {
|
||||
case n < 0:
|
||||
panic("negative argument")
|
||||
case n == 0:
|
||||
s.maxBytes = defaultMaxBytes
|
||||
default:
|
||||
s.maxBytes = n
|
||||
}
|
||||
s.purge()
|
||||
}
|
||||
|
||||
const defaultMaxBytes = 10 << 20 // 10 MiB
|
||||
|
||||
// NewMemoryEventStore creates a [MemoryEventStore] with the default value
|
||||
// for MaxBytes.
|
||||
func NewMemoryEventStore(opts *MemoryEventStoreOptions) *MemoryEventStore {
|
||||
return &MemoryEventStore{
|
||||
maxBytes: defaultMaxBytes,
|
||||
store: make(map[string]map[string]*dataList),
|
||||
}
|
||||
}
|
||||
|
||||
// Open implements [EventStore.Open]. It ensures that the underlying data
|
||||
// structures for the given session are initialized and ready for use.
|
||||
func (s *MemoryEventStore) Open(_ context.Context, sessionID, streamID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.init(sessionID, streamID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// init is an internal helper function that ensures the nested map structure for a
|
||||
// given sessionID and streamID exists, creating it if necessary. It returns the
|
||||
// dataList associated with the specified IDs.
|
||||
// Requires s.mu.
|
||||
func (s *MemoryEventStore) init(sessionID, streamID string) *dataList {
|
||||
streamMap, ok := s.store[sessionID]
|
||||
if !ok {
|
||||
streamMap = make(map[string]*dataList)
|
||||
s.store[sessionID] = streamMap
|
||||
}
|
||||
dl, ok := streamMap[streamID]
|
||||
if !ok {
|
||||
dl = &dataList{}
|
||||
streamMap[streamID] = dl
|
||||
}
|
||||
return dl
|
||||
}
|
||||
|
||||
// Append implements [EventStore.Append] by recording data in memory.
|
||||
func (s *MemoryEventStore) Append(_ context.Context, sessionID, streamID string, data []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
dl := s.init(sessionID, streamID)
|
||||
// Purge before adding, so at least the current data item will be present.
|
||||
// (That could result in nBytes > maxBytes, but we'll live with that.)
|
||||
s.purge()
|
||||
dl.appendData(data)
|
||||
s.nBytes += len(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrEventsPurged is the error that [EventStore.After] should return if the event just after the
|
||||
// index is no longer available.
|
||||
var ErrEventsPurged = errors.New("data purged")
|
||||
|
||||
// errMalformedEvent is returned when an SSE event cannot be parsed due to format violations.
|
||||
// This is a hard error indicating corrupted data or protocol violations, as opposed to
|
||||
// transient I/O errors which may be retryable.
|
||||
var errMalformedEvent = errors.New("malformed event")
|
||||
|
||||
// After implements [EventStore.After].
|
||||
func (s *MemoryEventStore) After(_ context.Context, sessionID, streamID string, index int) iter.Seq2[[]byte, error] {
|
||||
// Return the data items to yield.
|
||||
// We must copy, because dataList.removeFirst nils out slice elements.
|
||||
copyData := func() ([][]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
streamMap, ok := s.store[sessionID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("MemoryEventStore.After: unknown session ID %q", sessionID)
|
||||
}
|
||||
dl, ok := streamMap[streamID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("MemoryEventStore.After: unknown stream ID %v in session %q", streamID, sessionID)
|
||||
}
|
||||
start := index + 1
|
||||
if dl.first > start {
|
||||
return nil, fmt.Errorf("MemoryEventStore.After: index %d, stream ID %v, session %q: %w",
|
||||
index, streamID, sessionID, ErrEventsPurged)
|
||||
}
|
||||
return slices.Clone(dl.data[start-dl.first:]), nil
|
||||
}
|
||||
|
||||
return func(yield func([]byte, error) bool) {
|
||||
ds, err := copyData()
|
||||
if err != nil {
|
||||
yield(nil, err)
|
||||
return
|
||||
}
|
||||
for _, d := range ds {
|
||||
if !yield(d, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SessionClosed implements [EventStore.SessionClosed].
|
||||
func (s *MemoryEventStore) SessionClosed(_ context.Context, sessionID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, dl := range s.store[sessionID] {
|
||||
s.nBytes -= dl.size
|
||||
}
|
||||
delete(s.store, sessionID)
|
||||
s.validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// purge removes data until no more than s.maxBytes bytes are in use.
|
||||
// It must be called with s.mu held.
|
||||
func (s *MemoryEventStore) purge() {
|
||||
// Remove the first element of every dataList until below the max.
|
||||
for s.nBytes > s.maxBytes {
|
||||
changed := false
|
||||
for _, sm := range s.store {
|
||||
for _, dl := range sm {
|
||||
if dl.size > 0 {
|
||||
r := dl.removeFirst()
|
||||
if r > 0 {
|
||||
changed = true
|
||||
s.nBytes -= r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
panic("no progress during purge")
|
||||
}
|
||||
}
|
||||
s.validate()
|
||||
}
|
||||
|
||||
// validate checks that the store's data structures are valid.
|
||||
// It must be called with s.mu held.
|
||||
func (s *MemoryEventStore) validate() {
|
||||
if !validateMemoryEventStore {
|
||||
return
|
||||
}
|
||||
// Check that we're accounting for the size correctly.
|
||||
n := 0
|
||||
for _, sm := range s.store {
|
||||
for _, dl := range sm {
|
||||
for _, d := range dl.data {
|
||||
n += len(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
if n != s.nBytes {
|
||||
panic("sizes don't add up")
|
||||
}
|
||||
}
|
||||
|
||||
// debugString returns a string containing the state of s.
|
||||
// Used in tests.
|
||||
func (s *MemoryEventStore) debugString() string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var b strings.Builder
|
||||
for i, sess := range slices.Sorted(maps.Keys(s.store)) {
|
||||
if i > 0 {
|
||||
fmt.Fprintf(&b, "; ")
|
||||
}
|
||||
sm := s.store[sess]
|
||||
for i, sid := range slices.Sorted(maps.Keys(sm)) {
|
||||
if i > 0 {
|
||||
fmt.Fprintf(&b, "; ")
|
||||
}
|
||||
dl := sm[sid]
|
||||
fmt.Fprintf(&b, "%s %s first=%d", sess, sid, dl.first)
|
||||
for _, d := range dl.data {
|
||||
fmt.Fprintf(&b, " %s", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user