From 3e398da3fbd27aee3c06eb3acc994911fba8bdb5 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sat, 2 May 2026 03:27:32 -0400 Subject: [PATCH] feat(monitor/collectors/coinbase): per-product sequence-gap detector --- .../internal/collectors/coinbase/sequencer.go | 32 ++++++++++ .../collectors/coinbase/sequencer_test.go | 63 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/coinbase/sequencer.go create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/coinbase/sequencer_test.go diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/coinbase/sequencer.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/coinbase/sequencer.go new file mode 100644 index 00000000..340a19b9 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/coinbase/sequencer.go @@ -0,0 +1,32 @@ +// ©AngelaMos | 2026 +// sequencer.go + +package coinbase + +import "sync" + +type Sequencer struct { + mu sync.Mutex + last map[string]int64 +} + +func NewSequencer() *Sequencer { + return &Sequencer{last: make(map[string]int64)} +} + +func (s *Sequencer) Observe(productID string, seq int64) bool { + s.mu.Lock() + defer s.mu.Unlock() + prev, ok := s.last[productID] + s.last[productID] = seq + if !ok { + return false + } + return seq != prev+1 +} + +func (s *Sequencer) Reset() { + s.mu.Lock() + defer s.mu.Unlock() + s.last = make(map[string]int64) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/coinbase/sequencer_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/coinbase/sequencer_test.go new file mode 100644 index 00000000..a67ba6fc --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/coinbase/sequencer_test.go @@ -0,0 +1,63 @@ +// ©AngelaMos | 2026 +// sequencer_test.go + +package coinbase_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/coinbase" +) + +func TestSequencer_FirstTickIsAlwaysInOrder(t *testing.T) { + s := coinbase.NewSequencer() + gap := s.Observe("BTC-USD", 100) + require.False(t, gap, "first sequence number must not be a gap") +} + +func TestSequencer_ConsecutiveSequencesAreInOrder(t *testing.T) { + s := coinbase.NewSequencer() + for i := int64(50); i < 60; i++ { + gap := s.Observe("BTC-USD", i) + require.False(t, gap, "i=%d", i) + } +} + +func TestSequencer_GapTriggersReportSignal(t *testing.T) { + s := coinbase.NewSequencer() + require.False(t, s.Observe("BTC-USD", 50)) + require.False(t, s.Observe("BTC-USD", 51)) + require.True(t, s.Observe("BTC-USD", 60), "skipping 52-59 must report gap") +} + +func TestSequencer_GapsArePerProduct(t *testing.T) { + s := coinbase.NewSequencer() + require.False(t, s.Observe("BTC-USD", 100)) + require.False(t, s.Observe("ETH-USD", 200)) + require.False(t, s.Observe("BTC-USD", 101)) + require.True(t, s.Observe("ETH-USD", 250)) + require.False(t, s.Observe("BTC-USD", 102)) +} + +func TestSequencer_ResetClearsAllProducts(t *testing.T) { + s := coinbase.NewSequencer() + s.Observe("BTC-USD", 100) + s.Observe("ETH-USD", 200) + s.Reset() + require.False(t, s.Observe("BTC-USD", 9999)) + require.False(t, s.Observe("ETH-USD", 8888)) +} + +func TestSequencer_DuplicateSequenceTreatedAsGap(t *testing.T) { + s := coinbase.NewSequencer() + require.False(t, s.Observe("BTC-USD", 100)) + require.True(t, s.Observe("BTC-USD", 100), "replaying the same seq is a gap") +} + +func TestSequencer_BackwardSequenceTreatedAsGap(t *testing.T) { + s := coinbase.NewSequencer() + require.False(t, s.Observe("BTC-USD", 100)) + require.True(t, s.Observe("BTC-USD", 90)) +}