~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
199 lines · 3.4 KB Raw History
package events

import (
	"context"
	"errors"
	"slices"
	"strings"
	"time"
)

const (
	CapabilitySubscribe = "events.subscribe"

	CapabilityDeliver = "events.deliver"

	HandlerKind = "EventHandler"

	RecordWatchKind = "AtprotoRecordWatch"

	ScopeAll = "*"
)

var ErrFutureCursor = errors.New("events: the cursor is past the source's newest event")

type Op struct {
	Action string

	Path string

	CID string

	Prev string
}

type Event struct {
	Source string

	Seq int64

	Repo string

	Commit string

	Rev string

	Time time.Time

	Ops []Op

	ServerFilter *ServerFilter
}

type ServerFilterSupport string

const (
	ServerFilterSupported ServerFilterSupport = "true"

	ServerFilterUnsupported ServerFilterSupport = "false"

	ServerFilterUnknown ServerFilterSupport = "unknown"

	MaxServerFilterCollections = 100

	MaxServerFilterDIDs = 10000

	WantedCollectionsParam = "wantedCollections"

	WantedDIDsParam = "wantedDids"

	ServerFilterInfoName = "ServerFilter"

	collectionWildcard = ".*"

	logDIDs = 10
)

func MatchCollection(patterns []string, collection string) bool {
	for _, p := range patterns {
		p = strings.TrimSpace(p)
		if prefix, ok := strings.CutSuffix(p, collectionWildcard); ok {
			if strings.HasPrefix(collection, prefix+".") {
				return true
			}
			continue
		}
		if p == collection {
			return true
		}
	}
	return false
}

type ServerFilter struct {
	Collections []string

	DIDs []string

	Supported ServerFilterSupport
}

func (f *ServerFilter) Gapless() bool {
	return f != nil && f.Supported == ServerFilterSupported
}

type Filter struct {
	Collections []string

	DIDs []string

	Cursor *int64
}

type Stream interface {
	Next(ctx context.Context) (Event, error)

	Close() error
}

type SavedCursor struct {
	Cursor int64

	SavedAt time.Time
}

type CursorStore interface {
	Load(workspace, name, uid string) (SavedCursor, bool)

	Save(workspace, name, uid string, cursor int64) error
}

type Source interface {
	Subscribe(ctx context.Context, filter Filter) (Stream, error)
}

type Sink interface {
	Deliver(ctx context.Context, event Event) error
}

func (f Filter) Scope() []string {
	var out []string
	for _, v := range append(append([]string(nil), f.Collections...), f.DIDs...) {
		if v = strings.TrimSpace(v); v != "" && !slices.Contains(out, v) {
			out = append(out, v)
		}
	}
	if len(out) == 0 {
		return []string{ScopeAll}
	}
	return out
}

func (f Filter) ServerParams() (collections, dids []string) {
	collections, dids = trimmedUnique(f.Collections), trimmedUnique(f.DIDs)
	if len(collections) > MaxServerFilterCollections {
		collections = nil
	}
	if len(dids) > MaxServerFilterDIDs {
		dids = nil
	}
	return collections, dids
}

func LogDIDs(dids []string) []string {
	if len(dids) > logDIDs {
		return dids[:logDIDs]
	}
	return dids
}

func trimmedUnique(in []string) []string {
	var out []string
	seen := map[string]bool{}
	for _, v := range in {
		if v = strings.TrimSpace(v); v != "" && !seen[v] {
			seen[v] = true
			out = append(out, v)
		}
	}
	return out
}

func (f Filter) Narrow(e Event) Event {
	if len(f.DIDs) > 0 && !slices.ContainsFunc(f.DIDs, func(d string) bool { return strings.TrimSpace(d) == e.Repo }) {
		e.Ops = nil
		return e
	}
	if len(f.Collections) == 0 {
		return e
	}
	kept := make([]Op, 0, len(e.Ops))
	for _, op := range e.Ops {
		collection, _, _ := strings.Cut(op.Path, "/")
		if MatchCollection(f.Collections, collection) {
			kept = append(kept, op)
		}
	}
	e.Ops = kept
	return e
}