Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ claws.log

# Node.js
node_modules/
.claude
3 changes: 1 addition & 2 deletions custom/ecs/tasks/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,7 @@ func (r *TaskRenderer) Navigations(resource dao.Resource) []render.Navigation {
}

// If task is part of a service, add service navigation
if group := task.Group(); strings.HasPrefix(group, "service:") {
serviceName := strings.TrimPrefix(group, "service:")
if serviceName, ok := strings.CutPrefix(task.Group(), "service:"); ok {
navs = append(navs, render.Navigation{
Key: "s",
Label: "Service",
Expand Down
52 changes: 41 additions & 11 deletions internal/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"maps"
"slices"
"strings"
"sync"

"github.com/clawscli/claws/internal/dao"
Expand Down Expand Up @@ -53,6 +54,12 @@ type Registry struct {
aliases map[string]string // alias -> service name or service/resource
displayNames map[string]string // service -> display name for UI
categories []ServiceCategory // ordered list of service categories

// Cached computed values (aliases are immutable after init, safe to cache)
aliasListOnce sync.Once // guards aliasListCache initialization
aliasListCache []string // cached result of GetAliases()
serviceAliasesOnce sync.Once // guards serviceAliasesCache initialization
serviceAliasesCache map[string][]string // cached result of GetAliasesForService() by service
}

// New creates a new Registry
Expand Down Expand Up @@ -281,20 +288,43 @@ func (r *Registry) ResolveAlias(input string) (string, string, bool) {
return input, "", false
}

// GetAliasesForService returns all aliases for a given service
// GetAliasesForService returns all aliases for a given service.
func (r *Registry) GetAliasesForService(service string) []string {
r.mu.RLock()
defer r.mu.RUnlock()
r.serviceAliasesOnce.Do(func() {
r.mu.RLock()
defer r.mu.RUnlock()

r.serviceAliasesCache = make(map[string][]string)
for alias, target := range r.aliases {
svc := target
if idx := strings.Index(target, "/"); idx != -1 {
svc = target[:idx]
}
r.serviceAliasesCache[svc] = append(r.serviceAliasesCache[svc], alias)
}
for svc := range r.serviceAliasesCache {
slices.Sort(r.serviceAliasesCache[svc])
}
})
return slices.Clone(r.serviceAliasesCache[service])
}

// GetAliases returns all aliases (excluding self-referential ones like "sfn" -> "sfn").
func (r *Registry) GetAliases() []string {
r.aliasListOnce.Do(func() {
r.mu.RLock()
defer r.mu.RUnlock()

var aliases []string
for alias, target := range r.aliases {
// Check if target matches service (handle "service" or "service/resource")
if target == service || (len(target) > len(service) && target[:len(service)] == service && target[len(service)] == '/') {
aliases = append(aliases, alias)
var aliases []string
for alias, target := range r.aliases {
if alias != target {
aliases = append(aliases, alias)
}
}
}
slices.Sort(aliases)
return aliases
slices.Sort(aliases)
r.aliasListCache = aliases
})
return slices.Clone(r.aliasListCache)
}

// RegisterCustom registers a custom (hand-written) implementation
Expand Down
69 changes: 69 additions & 0 deletions internal/registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package registry

import (
"context"
"sync"
"testing"

"github.com/clawscli/claws/internal/dao"
Expand Down Expand Up @@ -259,6 +260,74 @@ func TestRegistry_GetAliasesForService_WithResourceAlias(t *testing.T) {
}
}

func TestRegistry_GetAliases(t *testing.T) {
reg := New()
aliases := reg.GetAliases()

if len(aliases) == 0 {
t.Fatal("GetAliases() should return aliases")
}

aliasMap := make(map[string]bool)
for _, a := range aliases {
aliasMap[a] = true
}

for _, expected := range []string{"cfn", "cf", "sg", "cost-explorer"} {
if !aliasMap[expected] {
t.Errorf("GetAliases() should include %q", expected)
}
}
}

func TestRegistry_GetAliases_ExcludesSelfReferential(t *testing.T) {
reg := New()
aliases := reg.GetAliases()

for _, alias := range aliases {
resolved, _, found := reg.ResolveAlias(alias)
if found && alias == resolved {
t.Errorf("GetAliases() should exclude self-referential alias %q", alias)
}
}
}

func TestRegistry_GetAliases_ConcurrentAccess(t *testing.T) {
reg := New()
var wg sync.WaitGroup
const goroutines = 100

wg.Add(goroutines)
for range goroutines {
go func() {
defer wg.Done()
aliases := reg.GetAliases()
if len(aliases) == 0 {
t.Error("GetAliases() should return aliases")
}
}()
}
wg.Wait()
}

func TestRegistry_GetAliasesForService_ConcurrentAccess(t *testing.T) {
reg := New()
var wg sync.WaitGroup
const goroutines = 100

wg.Add(goroutines)
for range goroutines {
go func() {
defer wg.Done()
aliases := reg.GetAliasesForService("cloudformation")
if len(aliases) != 2 {
t.Errorf("GetAliasesForService() returned %d aliases, want 2", len(aliases))
}
}()
}
wg.Wait()
}

func TestRegistry_GetDAO_NotRegistered(t *testing.T) {
reg := New()

Expand Down
Loading