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

import (
	"context"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/events"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/fsx"
)

type Snapshot struct {
	Started bool

	Cursor int64

	LastEventAt time.Time

	Lag int64

	Err error
}

type TrackerConfig struct {
	CountGaps bool

	Seed int64
}

type Tracker struct {
	gaps bool

	mu sync.Mutex

	s Snapshot
}

func NewTracker(cfg TrackerConfig) *Tracker {
	t := &Tracker{gaps: cfg.CountGaps}
	if cfg.Seed > 0 {
		t.s.Cursor = cfg.Seed
	}
	return t
}

func (t *Tracker) Observe(e events.Event, gapless bool) bool {
	at := e.Time
	if at.IsZero() {
		at = time.Now().UTC()
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	transition := !t.s.Started || t.s.Err != nil
	t.s.Lag = 0
	if t.gaps && !gapless && t.s.Started && e.Seq > t.s.Cursor+1 {
		t.s.Lag = e.Seq - t.s.Cursor - 1
	}
	t.s.Started, t.s.Cursor, t.s.LastEventAt, t.s.Err = true, e.Seq, at, nil
	return transition
}

func (t *Tracker) Fail(err error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	t.s.Err = err
}

func (t *Tracker) Recover() {
	t.mu.Lock()
	defer t.mu.Unlock()
	t.s.Err = nil
}

func (t *Tracker) Snapshot() Snapshot {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.s
}

type Tracked struct {
	stream events.Stream

	tracker *Tracker

	changed func()

	progress func()

	gapless bool
}

var _ events.Stream = (*Tracked)(nil)

func NewTracked(stream events.Stream, tracker *Tracker, changed, progress func()) *Tracked {
	if progress == nil {
		progress = changed
	}
	return &Tracked{stream: stream, tracker: tracker, changed: changed, progress: progress}
}

func (t *Tracked) Next(ctx context.Context) (events.Event, error) {
	event, err := t.stream.Next(ctx)
	if err != nil {
		if ctx.Err() == nil {
			t.tracker.Fail(err)
			t.changed()
		}
		return event, err
	}
	if event.ServerFilter != nil {
		t.gapless = event.ServerFilter.Gapless()
		return event, nil
	}
	if t.tracker.Observe(event, t.gapless) {
		t.changed()
	} else {
		t.progress()
	}
	return event, nil
}

func (t *Tracked) Close() error {
	return t.stream.Close()
}

var _ events.CursorStore = CursorStore{}

type CursorStore struct {
	dir string
}

type cursorFile struct {
	UID string `json:"uid"`

	Cursor int64 `json:"cursor"`

	SavedAt time.Time `json:"savedAt,omitzero"`
}

func NewCursorStore(dir string) CursorStore {
	return CursorStore{dir: strings.TrimSpace(dir)}
}

func (s CursorStore) path(workspace, name string) string {
	clean := func(v string) string {
		return strings.Map(func(r rune) rune {
			switch {
			case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '.', r == '_':
				return r
			default:
				return '_'
			}
		}, strings.TrimSpace(v))
	}
	return filepath.Join(s.dir, clean(workspace), clean(name)+".json")
}

func (s CursorStore) Load(workspace, name, uid string) (events.SavedCursor, bool) {
	if s.dir == "" {
		return events.SavedCursor{}, false
	}
	var f cursorFile
	if err := fsx.ReadJSON(s.path(workspace, name), &f); err != nil || f.UID != uid {
		return events.SavedCursor{}, false
	}
	return events.SavedCursor{Cursor: f.Cursor, SavedAt: f.SavedAt}, true
}

func (s CursorStore) Save(workspace, name, uid string, cursor int64) error {
	if s.dir == "" {
		return nil
	}
	return fsx.WriteJSON(s.path(workspace, name), cursorFile{UID: uid, Cursor: cursor, SavedAt: time.Now().UTC()}, 0o600)
}