diff --git a/op-devstack/stack/component_id.go b/op-devstack/stack/component_id.go index 8fb76a4e01b..03769cee3dd 100644 --- a/op-devstack/stack/component_id.go +++ b/op-devstack/stack/component_id.go @@ -94,6 +94,12 @@ func (id ComponentID) Shape() IDShape { return id.shape } +// HasChainID returns true if this ID has a chain ID component. +// This is true for IDShapeKeyAndChain and IDShapeChainOnly shapes. +func (id ComponentID) HasChainID() bool { + return id.shape == IDShapeKeyAndChain || id.shape == IDShapeChainOnly +} + func (id ComponentID) Key() string { return id.key } diff --git a/op-devstack/stack/registry.go b/op-devstack/stack/registry.go new file mode 100644 index 00000000000..2f11edf773c --- /dev/null +++ b/op-devstack/stack/registry.go @@ -0,0 +1,364 @@ +package stack + +import ( + "sync" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +// Registrable is the interface that components must implement to be stored in the Registry. +// It provides a way to get the component's ID as a ComponentID. +type Registrable interface { + // RegistryID returns the ComponentID for this component. + // This is used as the key in the unified registry. + RegistryID() ComponentID +} + +// Registry is a unified storage for all components in the system. +// It replaces multiple type-specific maps with a single registry that supports: +// - Type-safe access via generic functions +// - Secondary indexes by Kind and ChainID +// - Thread-safe concurrent access +type Registry struct { + mu sync.RWMutex + + // Primary storage: ComponentID -> component value + components map[ComponentID]any + + // Secondary index: ComponentKind -> list of ComponentIDs + byKind map[ComponentKind][]ComponentID + + // Secondary index: ChainID -> list of ComponentIDs + byChainID map[eth.ChainID][]ComponentID +} + +type registryEntry struct { + id ComponentID + component any +} + +// NewRegistry creates a new empty Registry. +func NewRegistry() *Registry { + return &Registry{ + components: make(map[ComponentID]any), + byKind: make(map[ComponentKind][]ComponentID), + byChainID: make(map[eth.ChainID][]ComponentID), + } +} + +// Register adds a component to the registry. +// If a component with the same ID already exists, it is replaced. +func (r *Registry) Register(id ComponentID, component any) { + r.mu.Lock() + defer r.mu.Unlock() + + // Check if this ID already exists (for index cleanup) + _, exists := r.components[id] + if exists { + // Remove from indexes before re-adding + r.removeFromIndexesLocked(id) + } + + // Store in primary map + r.components[id] = component + + // Add to kind index + r.byKind[id.Kind()] = append(r.byKind[id.Kind()], id) + + // Add to chainID index (if applicable) + if id.HasChainID() { + chainID := id.ChainID() + if chainID != (eth.ChainID{}) { + r.byChainID[chainID] = append(r.byChainID[chainID], id) + } + } +} + +// RegisterComponent registers a Registrable component using its RegistryID. +func (r *Registry) RegisterComponent(component Registrable) { + r.Register(component.RegistryID(), component) +} + +// Unregister removes a component from the registry. +func (r *Registry) Unregister(id ComponentID) { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.components[id]; !exists { + return + } + + delete(r.components, id) + r.removeFromIndexesLocked(id) +} + +// removeFromIndexesLocked removes an ID from secondary indexes. +// Caller must hold the write lock. +func (r *Registry) removeFromIndexesLocked(id ComponentID) { + // Remove from kind index + kind := id.Kind() + ids := r.byKind[kind] + for i, existingID := range ids { + if existingID == id { + r.byKind[kind] = append(ids[:i], ids[i+1:]...) + break + } + } + + // Remove from chainID index + if id.HasChainID() { + chainID := id.ChainID() + if chainID != (eth.ChainID{}) { + ids := r.byChainID[chainID] + for i, existingID := range ids { + if existingID == id { + r.byChainID[chainID] = append(ids[:i], ids[i+1:]...) + break + } + } + } + } +} + +// Get retrieves a component by its ID. +// Returns nil and false if the component is not found. +func (r *Registry) Get(id ComponentID) (any, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + component, ok := r.components[id] + return component, ok +} + +// Has returns true if a component with the given ID exists. +func (r *Registry) Has(id ComponentID) bool { + r.mu.RLock() + defer r.mu.RUnlock() + + _, ok := r.components[id] + return ok +} + +// GetByKind returns all components of a specific kind. +func (r *Registry) GetByKind(kind ComponentKind) []any { + r.mu.RLock() + defer r.mu.RUnlock() + + ids := r.byKind[kind] + result := make([]any, 0, len(ids)) + for _, id := range ids { + if component, ok := r.components[id]; ok { + result = append(result, component) + } + } + return result +} + +// GetByChainID returns all components associated with a specific chain. +func (r *Registry) GetByChainID(chainID eth.ChainID) []any { + r.mu.RLock() + defer r.mu.RUnlock() + + ids := r.byChainID[chainID] + result := make([]any, 0, len(ids)) + for _, id := range ids { + if component, ok := r.components[id]; ok { + result = append(result, component) + } + } + return result +} + +// IDsByKind returns all component IDs of a specific kind. +func (r *Registry) IDsByKind(kind ComponentKind) []ComponentID { + r.mu.RLock() + defer r.mu.RUnlock() + + ids := r.byKind[kind] + result := make([]ComponentID, len(ids)) + copy(result, ids) + return result +} + +// IDsByChainID returns all component IDs associated with a specific chain. +func (r *Registry) IDsByChainID(chainID eth.ChainID) []ComponentID { + r.mu.RLock() + defer r.mu.RUnlock() + + ids := r.byChainID[chainID] + result := make([]ComponentID, len(ids)) + copy(result, ids) + return result +} + +// AllIDs returns all component IDs in the registry. +func (r *Registry) AllIDs() []ComponentID { + r.mu.RLock() + defer r.mu.RUnlock() + + result := make([]ComponentID, 0, len(r.components)) + for id := range r.components { + result = append(result, id) + } + return result +} + +// All returns all components in the registry. +func (r *Registry) All() []any { + r.mu.RLock() + defer r.mu.RUnlock() + + result := make([]any, 0, len(r.components)) + for _, component := range r.components { + result = append(result, component) + } + return result +} + +// Len returns the number of components in the registry. +func (r *Registry) Len() int { + r.mu.RLock() + defer r.mu.RUnlock() + + return len(r.components) +} + +// Range calls fn for each component in the registry. +// If fn returns false, iteration stops. +func (r *Registry) Range(fn func(id ComponentID, component any) bool) { + r.mu.RLock() + entries := make([]registryEntry, 0, len(r.components)) + for id, component := range r.components { + entries = append(entries, registryEntry{id: id, component: component}) + } + r.mu.RUnlock() + + for _, entry := range entries { + if !fn(entry.id, entry.component) { + break + } + } +} + +// RangeByKind calls fn for each component of a specific kind. +// If fn returns false, iteration stops. +func (r *Registry) RangeByKind(kind ComponentKind, fn func(id ComponentID, component any) bool) { + r.mu.RLock() + ids := r.byKind[kind] + entries := make([]registryEntry, 0, len(ids)) + for _, id := range ids { + if component, ok := r.components[id]; ok { + entries = append(entries, registryEntry{id: id, component: component}) + } + } + r.mu.RUnlock() + + for _, entry := range entries { + if !fn(entry.id, entry.component) { + break + } + } +} + +// RangeByChainID calls fn for each component associated with a specific chain. +// If fn returns false, iteration stops. +func (r *Registry) RangeByChainID(chainID eth.ChainID, fn func(id ComponentID, component any) bool) { + r.mu.RLock() + ids := r.byChainID[chainID] + entries := make([]registryEntry, 0, len(ids)) + for _, id := range ids { + if component, ok := r.components[id]; ok { + entries = append(entries, registryEntry{id: id, component: component}) + } + } + r.mu.RUnlock() + + for _, entry := range entries { + if !fn(entry.id, entry.component) { + break + } + } +} + +// Clear removes all components from the registry. +func (r *Registry) Clear() { + r.mu.Lock() + defer r.mu.Unlock() + + r.components = make(map[ComponentID]any) + r.byKind = make(map[ComponentKind][]ComponentID) + r.byChainID = make(map[eth.ChainID][]ComponentID) +} + +// Type-safe generic accessor functions. +// These provide compile-time type safety when working with the registry. + +// RegistryGet retrieves a component by its typed ID and returns it as the expected type. +// Returns the zero value and false if not found or if the type doesn't match. +func RegistryGet[T any, M KindMarker](r *Registry, id ID[M]) (T, bool) { + component, ok := r.Get(id.ComponentID) + if !ok { + var zero T + return zero, false + } + + typed, ok := component.(T) + if !ok { + var zero T + return zero, false + } + + return typed, true +} + +// RegistryGetByKind retrieves all components of a specific kind and casts them to the expected type. +// Components that don't match the expected type are skipped. +func RegistryGetByKind[T any](r *Registry, kind ComponentKind) []T { + components := r.GetByKind(kind) + result := make([]T, 0, len(components)) + for _, component := range components { + if typed, ok := component.(T); ok { + result = append(result, typed) + } + } + return result +} + +// RegistryGetByChainID retrieves all components for a chain and casts them to the expected type. +// Components that don't match the expected type are skipped. +func RegistryGetByChainID[T any](r *Registry, chainID eth.ChainID) []T { + components := r.GetByChainID(chainID) + result := make([]T, 0, len(components)) + for _, component := range components { + if typed, ok := component.(T); ok { + result = append(result, typed) + } + } + return result +} + +// RegistryRange calls fn for each component of the expected type. +// Components that don't match the expected type are skipped. +func RegistryRange[T any](r *Registry, fn func(id ComponentID, component T) bool) { + r.Range(func(id ComponentID, component any) bool { + if typed, ok := component.(T); ok { + return fn(id, typed) + } + return true // skip non-matching types + }) +} + +// RegistryRangeByKind calls fn for each component of a specific kind that matches the expected type. +func RegistryRangeByKind[T any](r *Registry, kind ComponentKind, fn func(id ComponentID, component T) bool) { + r.RangeByKind(kind, func(id ComponentID, component any) bool { + if typed, ok := component.(T); ok { + return fn(id, typed) + } + return true + }) +} + +// RegistryRegister is a type-safe way to register a component with a typed ID. +func RegistryRegister[T any, M KindMarker](r *Registry, id ID[M], component T) { + r.Register(id.ComponentID, component) +} diff --git a/op-devstack/stack/registry_test.go b/op-devstack/stack/registry_test.go new file mode 100644 index 00000000000..e4d1ebeb7a5 --- /dev/null +++ b/op-devstack/stack/registry_test.go @@ -0,0 +1,596 @@ +package stack + +import ( + "sync" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/stretchr/testify/require" +) + +// mockComponent is a test component that implements Registrable. +type mockComponent struct { + id ComponentID + name string +} + +func (m *mockComponent) RegistryID() ComponentID { + return m.id +} + +func requireCompletesWithoutDeadlock(t *testing.T, fn func()) { + t.Helper() + + done := make(chan struct{}) + go func() { + fn() + close(done) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("operation timed out (likely callback executed under lock)") + } +} + +func TestRegistry_RegisterAndGet(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id := NewComponentID(KindL2Batcher, "batcher1", chainID) + component := &mockComponent{id: id, name: "test-batcher"} + + // Register + r.Register(id, component) + + // Get + got, ok := r.Get(id) + require.True(t, ok) + require.Equal(t, component, got) + + // Check Has + require.True(t, r.Has(id)) + + // Check non-existent + otherId := NewComponentID(KindL2Batcher, "batcher2", chainID) + _, ok = r.Get(otherId) + require.False(t, ok) + require.False(t, r.Has(otherId)) +} + +func TestRegistry_RegisterComponent(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id := NewComponentID(KindL2Batcher, "batcher1", chainID) + component := &mockComponent{id: id, name: "test-batcher"} + + // Register using RegisterComponent + r.RegisterComponent(component) + + // Get + got, ok := r.Get(id) + require.True(t, ok) + require.Equal(t, component, got) +} + +func TestRegistry_Unregister(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id := NewComponentID(KindL2Batcher, "batcher1", chainID) + component := &mockComponent{id: id, name: "test-batcher"} + + r.Register(id, component) + require.True(t, r.Has(id)) + + r.Unregister(id) + require.False(t, r.Has(id)) + + // Unregistering again should be a no-op + r.Unregister(id) + require.False(t, r.Has(id)) +} + +func TestRegistry_Replace(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id := NewComponentID(KindL2Batcher, "batcher1", chainID) + component1 := &mockComponent{id: id, name: "original"} + component2 := &mockComponent{id: id, name: "replacement"} + + r.Register(id, component1) + r.Register(id, component2) // Replace + + got, ok := r.Get(id) + require.True(t, ok) + require.Equal(t, component2, got) + + // Should only have one entry + require.Equal(t, 1, r.Len()) + + // Should only be in indexes once + ids := r.IDsByKind(KindL2Batcher) + require.Len(t, ids, 1) +} + +func TestRegistry_GetByKind(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + // Register multiple batchers + batcher1 := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher1", chainID), + name: "batcher1", + } + batcher2 := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher2", chainID), + name: "batcher2", + } + // Register a proposer (different kind) + proposer := &mockComponent{ + id: NewComponentID(KindL2Proposer, "proposer1", chainID), + name: "proposer1", + } + + r.Register(batcher1.id, batcher1) + r.Register(batcher2.id, batcher2) + r.Register(proposer.id, proposer) + + // Get batchers + batchers := r.GetByKind(KindL2Batcher) + require.Len(t, batchers, 2) + + // Get proposers + proposers := r.GetByKind(KindL2Proposer) + require.Len(t, proposers, 1) + + // Get non-existent kind + challengers := r.GetByKind(KindL2Challenger) + require.Len(t, challengers, 0) +} + +func TestRegistry_GetByChainID(t *testing.T) { + r := NewRegistry() + + chainID1 := eth.ChainIDFromUInt64(420) + chainID2 := eth.ChainIDFromUInt64(421) + + // Components on chain 420 + batcher1 := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher1", chainID1), + name: "batcher1", + } + proposer1 := &mockComponent{ + id: NewComponentID(KindL2Proposer, "proposer1", chainID1), + name: "proposer1", + } + + // Component on chain 421 + batcher2 := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher2", chainID2), + name: "batcher2", + } + + r.Register(batcher1.id, batcher1) + r.Register(proposer1.id, proposer1) + r.Register(batcher2.id, batcher2) + + // Get all on chain 420 + chain420 := r.GetByChainID(chainID1) + require.Len(t, chain420, 2) + + // Get all on chain 421 + chain421 := r.GetByChainID(chainID2) + require.Len(t, chain421, 1) + + // Non-existent chain + chain999 := r.GetByChainID(eth.ChainIDFromUInt64(999)) + require.Len(t, chain999, 0) +} + +func TestRegistry_KeyOnlyComponents(t *testing.T) { + r := NewRegistry() + + // Key-only components (like Supervisor) don't have a ChainID + supervisor := &mockComponent{ + id: NewComponentIDKeyOnly(KindSupervisor, "supervisor1"), + name: "supervisor1", + } + + r.Register(supervisor.id, supervisor) + + // Should be findable by kind + supervisors := r.GetByKind(KindSupervisor) + require.Len(t, supervisors, 1) + + // Should not appear in any chain index + // (GetByChainID with zero ChainID should not return it) + byChain := r.GetByChainID(eth.ChainID{}) + require.Len(t, byChain, 0) +} + +func TestRegistry_ChainOnlyComponents(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(1) + + // Chain-only components (like L1Network) don't have a key + network := &mockComponent{ + id: NewComponentIDChainOnly(KindL1Network, chainID), + name: "mainnet", + } + + r.Register(network.id, network) + + // Should be findable by kind + networks := r.GetByKind(KindL1Network) + require.Len(t, networks, 1) + + // Should be findable by chain + byChain := r.GetByChainID(chainID) + require.Len(t, byChain, 1) +} + +func TestRegistry_IDsByKind(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id1 := NewComponentID(KindL2Batcher, "batcher1", chainID) + id2 := NewComponentID(KindL2Batcher, "batcher2", chainID) + + r.Register(id1, &mockComponent{id: id1}) + r.Register(id2, &mockComponent{id: id2}) + + ids := r.IDsByKind(KindL2Batcher) + require.Len(t, ids, 2) + require.Contains(t, ids, id1) + require.Contains(t, ids, id2) +} + +func TestRegistry_AllAndLen(t *testing.T) { + r := NewRegistry() + + require.Equal(t, 0, r.Len()) + require.Len(t, r.All(), 0) + require.Len(t, r.AllIDs(), 0) + + chainID := eth.ChainIDFromUInt64(420) + id1 := NewComponentID(KindL2Batcher, "batcher1", chainID) + id2 := NewComponentID(KindL2Proposer, "proposer1", chainID) + + r.Register(id1, &mockComponent{id: id1}) + r.Register(id2, &mockComponent{id: id2}) + + require.Equal(t, 2, r.Len()) + require.Len(t, r.All(), 2) + require.Len(t, r.AllIDs(), 2) +} + +func TestRegistry_Range(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id1 := NewComponentID(KindL2Batcher, "batcher1", chainID) + id2 := NewComponentID(KindL2Batcher, "batcher2", chainID) + + r.Register(id1, &mockComponent{id: id1, name: "b1"}) + r.Register(id2, &mockComponent{id: id2, name: "b2"}) + + // Collect all + var collected []ComponentID + r.Range(func(id ComponentID, component any) bool { + collected = append(collected, id) + return true + }) + require.Len(t, collected, 2) + + // Early termination + collected = nil + r.Range(func(id ComponentID, component any) bool { + collected = append(collected, id) + return false // stop after first + }) + require.Len(t, collected, 1) +} + +func TestRegistry_RangeByKind(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + batcher := NewComponentID(KindL2Batcher, "batcher1", chainID) + proposer := NewComponentID(KindL2Proposer, "proposer1", chainID) + + r.Register(batcher, &mockComponent{id: batcher}) + r.Register(proposer, &mockComponent{id: proposer}) + + var collected []ComponentID + r.RangeByKind(KindL2Batcher, func(id ComponentID, component any) bool { + collected = append(collected, id) + return true + }) + require.Len(t, collected, 1) + require.Equal(t, batcher, collected[0]) +} + +func TestRegistry_RangeByChainID(t *testing.T) { + r := NewRegistry() + + chainID1 := eth.ChainIDFromUInt64(420) + chainID2 := eth.ChainIDFromUInt64(421) + + batcher1 := NewComponentID(KindL2Batcher, "batcher1", chainID1) + batcher2 := NewComponentID(KindL2Batcher, "batcher2", chainID2) + + r.Register(batcher1, &mockComponent{id: batcher1}) + r.Register(batcher2, &mockComponent{id: batcher2}) + + var collected []ComponentID + r.RangeByChainID(chainID1, func(id ComponentID, component any) bool { + collected = append(collected, id) + return true + }) + require.Len(t, collected, 1) + require.Equal(t, batcher1, collected[0]) + + // Test early termination + collected = nil + r.RangeByChainID(chainID1, func(id ComponentID, component any) bool { + collected = append(collected, id) + return false // stop immediately + }) + require.Len(t, collected, 1) +} + +func TestRegistry_Range_CallbackCanMutateRegistry(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id := NewComponentID(KindL2Batcher, "batcher1", chainID) + r.Register(id, &mockComponent{id: id}) + + requireCompletesWithoutDeadlock(t, func() { + r.Range(func(id ComponentID, component any) bool { + r.Clear() + return false + }) + }) + + require.Equal(t, 0, r.Len()) +} + +func TestRegistry_RangeByKind_CallbackCanMutateRegistry(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + oldID := NewComponentID(KindL2Batcher, "batcher1", chainID) + newID := NewComponentID(KindL2Batcher, "batcher2", chainID) + r.Register(oldID, &mockComponent{id: oldID}) + + requireCompletesWithoutDeadlock(t, func() { + r.RangeByKind(KindL2Batcher, func(id ComponentID, component any) bool { + r.Unregister(oldID) + r.Register(newID, &mockComponent{id: newID}) + return false + }) + }) + + require.False(t, r.Has(oldID)) + require.True(t, r.Has(newID)) +} + +func TestRegistry_RangeByChainID_CallbackCanMutateRegistry(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + oldID := NewComponentID(KindL2Batcher, "batcher1", chainID) + newID := NewComponentID(KindL2Batcher, "batcher2", chainID) + r.Register(oldID, &mockComponent{id: oldID}) + + requireCompletesWithoutDeadlock(t, func() { + r.RangeByChainID(chainID, func(id ComponentID, component any) bool { + r.Unregister(oldID) + r.Register(newID, &mockComponent{id: newID}) + return false + }) + }) + + require.False(t, r.Has(oldID)) + require.True(t, r.Has(newID)) +} + +func TestRegistry_Clear(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id := NewComponentID(KindL2Batcher, "batcher1", chainID) + r.Register(id, &mockComponent{id: id}) + + require.Equal(t, 1, r.Len()) + + r.Clear() + + require.Equal(t, 0, r.Len()) + require.False(t, r.Has(id)) + require.Len(t, r.GetByKind(KindL2Batcher), 0) + require.Len(t, r.GetByChainID(chainID), 0) +} + +func TestRegistry_ConcurrentAccess(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + var wg sync.WaitGroup + numGoroutines := 100 + + // Concurrent writes + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + id := NewComponentID(KindL2Batcher, string(rune('a'+i%26)), chainID) + r.Register(id, &mockComponent{id: id}) + }(i) + } + + // Concurrent reads + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _ = r.GetByKind(KindL2Batcher) + _ = r.GetByChainID(chainID) + _ = r.Len() + }() + } + + wg.Wait() + + // Should have some components (exact count depends on key collisions) + require.Greater(t, r.Len(), 0) +} + +// Tests for type-safe generic accessor functions + +func TestRegistryGet_TypeSafe(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id := NewL2BatcherID2("batcher1", chainID) + component := &mockComponent{id: id.ComponentID, name: "test-batcher"} + + RegistryRegister(r, id, component) + + // Type-safe get + got, ok := RegistryGet[*mockComponent](r, id) + require.True(t, ok) + require.Equal(t, component, got) + + // Wrong type should fail + gotStr, ok := RegistryGet[string](r, id) + require.False(t, ok) + require.Equal(t, "", gotStr) +} + +func TestRegistryGetByKind_TypeSafe(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + batcher1 := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher1", chainID), + name: "batcher1", + } + batcher2 := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher2", chainID), + name: "batcher2", + } + + r.Register(batcher1.id, batcher1) + r.Register(batcher2.id, batcher2) + + // Type-safe get by kind + batchers := RegistryGetByKind[*mockComponent](r, KindL2Batcher) + require.Len(t, batchers, 2) + + // Wrong type returns empty + wrongType := RegistryGetByKind[string](r, KindL2Batcher) + require.Len(t, wrongType, 0) +} + +func TestRegistryGetByChainID_TypeSafe(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + batcher := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher1", chainID), + name: "batcher1", + } + proposer := &mockComponent{ + id: NewComponentID(KindL2Proposer, "proposer1", chainID), + name: "proposer1", + } + + r.Register(batcher.id, batcher) + r.Register(proposer.id, proposer) + + // Get all mockComponents on chain + components := RegistryGetByChainID[*mockComponent](r, chainID) + require.Len(t, components, 2) +} + +func TestRegistryRange_TypeSafe(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + batcher := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher1", chainID), + name: "batcher1", + } + r.Register(batcher.id, batcher) + + // Also register a non-mockComponent + r.Register(NewComponentID(KindL2Proposer, "other", chainID), "not a mockComponent") + + var collected []*mockComponent + RegistryRange(r, func(id ComponentID, component *mockComponent) bool { + collected = append(collected, component) + return true + }) + + // Should only collect mockComponents + require.Len(t, collected, 1) + require.Equal(t, batcher, collected[0]) +} + +func TestRegistryRangeByKind_TypeSafe(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + + batcher := &mockComponent{ + id: NewComponentID(KindL2Batcher, "batcher1", chainID), + name: "batcher1", + } + proposer := &mockComponent{ + id: NewComponentID(KindL2Proposer, "proposer1", chainID), + name: "proposer1", + } + + r.Register(batcher.id, batcher) + r.Register(proposer.id, proposer) + + var collected []*mockComponent + RegistryRangeByKind(r, KindL2Batcher, func(id ComponentID, component *mockComponent) bool { + collected = append(collected, component) + return true + }) + + require.Len(t, collected, 1) + require.Equal(t, batcher, collected[0]) +} + +func TestRegistry_UnregisterUpdatesIndexes(t *testing.T) { + r := NewRegistry() + + chainID := eth.ChainIDFromUInt64(420) + id := NewComponentID(KindL2Batcher, "batcher1", chainID) + r.Register(id, &mockComponent{id: id}) + + // Verify indexes before unregister + require.Len(t, r.IDsByKind(KindL2Batcher), 1) + require.Len(t, r.IDsByChainID(chainID), 1) + + r.Unregister(id) + + // Indexes should be updated + require.Len(t, r.IDsByKind(KindL2Batcher), 0) + require.Len(t, r.IDsByChainID(chainID), 0) +}