···388388 return checksums
389389 }
390390391391- pairs := strings.Split(checksumsStr, ",")
392392- for _, pair := range pairs {
391391+ for pair := range strings.SplitSeq(checksumsStr, ",") {
393392 parts := strings.SplitN(strings.TrimSpace(pair), ":", 2)
394393 if len(parts) == 2 {
395394 platform := strings.TrimSpace(parts[0])
+2-5
pkg/appview/db/schema.go
···225225 var statements []string
226226227227 // Split on semicolons
228228- parts := strings.Split(query, ";")
229229-230230- for _, part := range parts {
228228+ for part := range strings.SplitSeq(query, ";") {
231229 // Trim whitespace
232230 stmt := strings.TrimSpace(part)
233231···237235 }
238236239237 // Skip comment-only statements
240240- lines := strings.Split(stmt, "\n")
241238 hasCode := false
242242- for _, line := range lines {
239239+ for line := range strings.SplitSeq(stmt, "\n") {
243240 trimmed := strings.TrimSpace(line)
244241 if trimmed != "" && !strings.HasPrefix(trimmed, "--") {
245242 hasCode = true
+1-1
pkg/appview/handlers/opengraph.go
···105105106106 if licenses != "" {
107107 // Show first license if multiple
108108- license := strings.Split(licenses, ",")[0]
108108+ license, _, _ := strings.Cut(licenses, ",")
109109 license = strings.TrimSpace(license)
110110 card.DrawBadge(license, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeBg, ogcard.ColorText)
111111 }
···53535454// Start begins the background worker
5555func (w *Worker) Start(ctx context.Context) {
5656- w.wg.Add(1)
5757- go func() {
5858- defer w.wg.Done()
5959-5656+ w.wg.Go(func() {
6057 slog.Info("Hold health worker starting background health checks")
61586259 // Wait for services to be ready (Docker startup race condition)
···8986 w.checker.Cleanup()
9087 }
9188 }
9292- }()
8989+ })
9390}
94919592// Stop gracefully stops the worker
···154151 var statsMu sync.Mutex
155152156153 for _, endpoint := range uniqueEndpoints {
157157- wg.Add(1)
158158-159159- go func(ep string) {
160160- defer wg.Done()
161161-154154+ wg.Go(func() {
162155 // Acquire semaphore
163156 sem <- struct{}{}
164157 defer func() { <-sem }()
165158166159 // Check health
167167- isReachable, err := w.checker.CheckHealth(ctx, ep)
160160+ isReachable, err := w.checker.CheckHealth(ctx, endpoint)
168161169162 // Update cache
170170- w.checker.SetStatus(ep, isReachable, err)
163163+ w.checker.SetStatus(endpoint, isReachable, err)
171164172165 // Update stats
173166 statsMu.Lock()
···175168 reachable++
176169 } else {
177170 unreachable++
178178- slog.Warn("Hold health worker hold unreachable", "endpoint", ep, "error", err)
171171+ slog.Warn("Hold health worker hold unreachable", "endpoint", endpoint, "error", err)
179172 }
180173 statsMu.Unlock()
181181- }(endpoint)
174174+ })
182175 }
183176184177 // Wait for all checks to complete
+1-3
pkg/appview/licenses/licenses.go
···129129 licensesStr = strings.ReplaceAll(licensesStr, " OR ", ",")
130130 licensesStr = strings.ReplaceAll(licensesStr, ";", ",")
131131132132- parts := strings.Split(licensesStr, ",")
133133-134132 var result []LicenseInfo
135133 seen := make(map[string]bool) // Deduplicate
136134137137- for _, part := range parts {
135135+ for part := range strings.SplitSeq(licensesStr, ",") {
138136 part = strings.TrimSpace(part)
139137 if part == "" {
140138 continue
+5-8
pkg/appview/middleware/auth_test.go
···358358 var wg sync.WaitGroup
359359 var mu sync.Mutex // Protect results map
360360361361- for i := range 10 {
362362- wg.Add(1)
363363- go func(index int, sessionID string) {
364364- defer wg.Done()
365365-361361+ for i := range results {
362362+ wg.Go(func() {
366363 req := httptest.NewRequest("GET", "/test", nil)
367364 req.AddCookie(&http.Cookie{
368365 Name: "atcr_session",
369369- Value: sessionID,
366366+ Value: sessionIDs[i],
370367 })
371368 w := httptest.NewRecorder()
372369373370 wrappedHandler.ServeHTTP(w, req)
374371375372 mu.Lock()
376376- results[index] = w.Code
373373+ results[i] = w.Code
377374 mu.Unlock()
378378- }(i, sessionIDs[i])
375375+ })
379376 }
380377381378 wg.Wait()
+2-4
pkg/appview/storage/profile_test.go
···341341 // Make 5 concurrent GetProfile calls
342342 var wg sync.WaitGroup
343343 for range 5 {
344344- wg.Add(1)
345345- go func() {
346346- defer wg.Done()
344344+ wg.Go(func() {
347345 _, err := GetProfile(context.Background(), client)
348346 if err != nil {
349347 t.Errorf("GetProfile() error = %v", err)
350348 }
351351- }()
349349+ })
352350 }
353351354352 wg.Wait()
+42-45
pkg/appview/storage/routing_repository.go
···77import (
88 "context"
99 "log/slog"
1010+ "sync"
10111112 "github.com/distribution/distribution/v3"
1213)
···1819// RoutingRepository routes manifests to ATProto and blobs to external hold service
1920// The registry (AppView) is stateless and NEVER stores blobs locally
2021// NOTE: A fresh instance is created per-request (see middleware/registry.go)
2121-// so no mutex is needed - each request has its own instance
2222type RoutingRepository struct {
2323 distribution.Repository
2424- Ctx *RegistryContext // All context and services (exported for token updates)
2525- manifestStore *ManifestStore // Manifest store instance (lazy-initialized)
2626- blobStore *ProxyBlobStore // Blob store instance (lazy-initialized)
2424+ Ctx *RegistryContext // All context and services (exported for token updates)
2525+ manifestStore *ManifestStore // Manifest store instance (lazy-initialized)
2626+ manifestStoreOnce sync.Once // Ensures thread-safe lazy initialization
2727+ blobStore *ProxyBlobStore // Blob store instance (lazy-initialized)
2828+ blobStoreOnce sync.Once // Ensures thread-safe lazy initialization
2729}
28302931// NewRoutingRepository creates a new routing repository
···36383739// Manifests returns the ATProto-backed manifest service
3840func (r *RoutingRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
3939- // Lazy-initialize manifest store (no mutex needed - one instance per request)
4040- if r.manifestStore == nil {
4141+ r.manifestStoreOnce.Do(func() {
4142 // Ensure blob store is created first (needed for label extraction during push)
4243 blobStore := r.Blobs(ctx)
4344 r.manifestStore = NewManifestStore(r.Ctx, blobStore)
4444- }
4545+ })
4546 return r.manifestStore, nil
4647}
47484849// Blobs returns a proxy blob store that routes to external hold service
4950// The registry (AppView) NEVER stores blobs locally - all blobs go through hold service
5051func (r *RoutingRepository) Blobs(ctx context.Context) distribution.BlobStore {
5151- // Return cached blob store if available (no mutex needed - one instance per request)
5252- if r.blobStore != nil {
5353- slog.Debug("Returning cached blob store", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository)
5454- return r.blobStore
5555- }
5252+ r.blobStoreOnce.Do(func() {
5353+ // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation
5454+ // Pull operations use the historical hold DID from the database (blobs are where they were pushed)
5555+ // Push operations use the discovery-based hold DID from user's profile/default
5656+ // This allows users to change their default hold and have new pushes go there
5757+ isPull := false
5858+ if method, ok := ctx.Value(HTTPRequestMethod).(string); ok {
5959+ isPull = method == "GET" || method == "HEAD"
6060+ }
56615757- // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation
5858- // Pull operations use the historical hold DID from the database (blobs are where they were pushed)
5959- // Push operations use the discovery-based hold DID from user's profile/default
6060- // This allows users to change their default hold and have new pushes go there
6161- isPull := false
6262- if method, ok := ctx.Value(HTTPRequestMethod).(string); ok {
6363- isPull = method == "GET" || method == "HEAD"
6464- }
6262+ holdDID := r.Ctx.HoldDID // Default to discovery-based DID
6363+ holdSource := "discovery"
65646666- holdDID := r.Ctx.HoldDID // Default to discovery-based DID
6767- holdSource := "discovery"
6565+ // Only query database for pull operations
6666+ if isPull && r.Ctx.Database != nil {
6767+ // Query database for the latest manifest's hold DID
6868+ if dbHoldDID, err := r.Ctx.Database.GetLatestHoldDIDForRepo(r.Ctx.DID, r.Ctx.Repository); err == nil && dbHoldDID != "" {
6969+ // Use hold DID from database (pull case - use historical reference)
7070+ holdDID = dbHoldDID
7171+ holdSource = "database"
7272+ slog.Debug("Using hold from database manifest (pull)", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", dbHoldDID)
7373+ } else if err != nil {
7474+ // Log error but don't fail - fall back to discovery-based DID
7575+ slog.Warn("Failed to query database for hold DID", "component", "storage/blobs", "error", err)
7676+ }
7777+ // If dbHoldDID is empty (no manifests yet), fall through to use discovery-based DID
7878+ }
68796969- // Only query database for pull operations
7070- if isPull && r.Ctx.Database != nil {
7171- // Query database for the latest manifest's hold DID
7272- if dbHoldDID, err := r.Ctx.Database.GetLatestHoldDIDForRepo(r.Ctx.DID, r.Ctx.Repository); err == nil && dbHoldDID != "" {
7373- // Use hold DID from database (pull case - use historical reference)
7474- holdDID = dbHoldDID
7575- holdSource = "database"
7676- slog.Debug("Using hold from database manifest (pull)", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", dbHoldDID)
7777- } else if err != nil {
7878- // Log error but don't fail - fall back to discovery-based DID
7979- slog.Warn("Failed to query database for hold DID", "component", "storage/blobs", "error", err)
8080+ if holdDID == "" {
8181+ // This should never happen if middleware is configured correctly
8282+ panic("hold DID not set in RegistryContext - ensure default_hold_did is configured in middleware")
8083 }
8181- // If dbHoldDID is empty (no manifests yet), fall through to use discovery-based DID
8282- }
83848484- if holdDID == "" {
8585- // This should never happen if middleware is configured correctly
8686- panic("hold DID not set in RegistryContext - ensure default_hold_did is configured in middleware")
8787- }
8585+ slog.Debug("Using hold DID for blobs", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", holdDID, "source", holdSource)
88868989- slog.Debug("Using hold DID for blobs", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", holdDID, "source", holdSource)
9090-9191- // Update context with the correct hold DID (may be from database or discovered)
9292- r.Ctx.HoldDID = holdDID
8787+ // Update context with the correct hold DID (may be from database or discovered)
8888+ r.Ctx.HoldDID = holdDID
93899494- // Create and cache proxy blob store
9595- r.blobStore = NewProxyBlobStore(r.Ctx)
9090+ // Create and cache proxy blob store
9191+ r.blobStore = NewProxyBlobStore(r.Ctx)
9292+ })
9693 return r.blobStore
9794}
9895
+6-10
pkg/appview/storage/routing_repository_test.go
···318318319319 // Concurrent access to Manifests()
320320 for i := 0; i < numGoroutines; i++ {
321321- wg.Add(1)
322322- go func(index int) {
323323- defer wg.Done()
321321+ wg.Go(func() {
324322 store, err := repo.Manifests(context.Background())
325323 require.NoError(t, err)
326326- manifestStores[index] = store
327327- }(i)
324324+ manifestStores[i] = store
325325+ })
328326 }
329327330328 wg.Wait()
···341339342340 // Concurrent access to Blobs()
343341 for i := 0; i < numGoroutines; i++ {
344344- wg.Add(1)
345345- go func(index int) {
346346- defer wg.Done()
347347- blobStores[index] = repo.Blobs(context.Background())
348348- }(i)
342342+ wg.Go(func() {
343343+ blobStores[i] = repo.Blobs(context.Background())
344344+ })
349345 }
350346351347 wg.Wait()
+5-9
pkg/atproto/directory_test.go
···2929 t.Run("concurrent access is thread-safe", func(t *testing.T) {
3030 const numGoroutines = 100
3131 var wg sync.WaitGroup
3232- wg.Add(numGoroutines)
33323433 // Channel to collect all directory instances
3534 instances := make(chan any, numGoroutines)
36353736 // Launch many goroutines concurrently accessing GetDirectory
3837 for range numGoroutines {
3939- go func() {
4040- defer wg.Done()
3838+ wg.Go(func() {
4139 dir := GetDirectory()
4240 instances <- dir
4343- }()
4141+ })
4442 }
45434644 // Wait for all goroutines to complete
···120118121119 const numGoroutines = 50
122120 var wg sync.WaitGroup
123123- wg.Add(numGoroutines)
124121125122 instances := make([]any, numGoroutines)
126123 var mu sync.Mutex
127124128125 // Simulate many goroutines trying to get the directory simultaneously
129126 for i := 0; i < numGoroutines; i++ {
130130- go func(idx int) {
131131- defer wg.Done()
127127+ wg.Go(func() {
132128 dir := GetDirectory()
133129 mu.Lock()
134134- instances[idx] = dir
130130+ instances[i] = dir
135131 mu.Unlock()
136136- }(i)
132132+ })
137133 }
138134139135 wg.Wait()