···388 return checksums
389 }
390391- pairs := strings.Split(checksumsStr, ",")
392- for _, pair := range pairs {
393 parts := strings.SplitN(strings.TrimSpace(pair), ":", 2)
394 if len(parts) == 2 {
395 platform := strings.TrimSpace(parts[0])
···388 return checksums
389 }
390391+ for pair := range strings.SplitSeq(checksumsStr, ",") {
0392 parts := strings.SplitN(strings.TrimSpace(pair), ":", 2)
393 if len(parts) == 2 {
394 platform := strings.TrimSpace(parts[0])
+2-5
pkg/appview/db/schema.go
···225 var statements []string
226227 // Split on semicolons
228- parts := strings.Split(query, ";")
229-230- for _, part := range parts {
231 // Trim whitespace
232 stmt := strings.TrimSpace(part)
233···237 }
238239 // Skip comment-only statements
240- lines := strings.Split(stmt, "\n")
241 hasCode := false
242- for _, line := range lines {
243 trimmed := strings.TrimSpace(line)
244 if trimmed != "" && !strings.HasPrefix(trimmed, "--") {
245 hasCode = true
···225 var statements []string
226227 // Split on semicolons
228+ for part := range strings.SplitSeq(query, ";") {
00229 // Trim whitespace
230 stmt := strings.TrimSpace(part)
231···235 }
236237 // Skip comment-only statements
0238 hasCode := false
239+ for line := range strings.SplitSeq(stmt, "\n") {
240 trimmed := strings.TrimSpace(line)
241 if trimmed != "" && !strings.HasPrefix(trimmed, "--") {
242 hasCode = true
+1-1
pkg/appview/handlers/opengraph.go
···105106 if licenses != "" {
107 // Show first license if multiple
108- license := strings.Split(licenses, ",")[0]
109 license = strings.TrimSpace(license)
110 card.DrawBadge(license, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeBg, ogcard.ColorText)
111 }
···105106 if licenses != "" {
107 // Show first license if multiple
108+ license, _, _ := strings.Cut(licenses, ",")
109 license = strings.TrimSpace(license)
110 card.DrawBadge(license, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeBg, ogcard.ColorText)
111 }
+11-14
pkg/appview/handlers/repository.go
···89 continue
90 }
9192- wg.Add(1)
93- go func(idx int) {
94- defer wg.Done()
95-96- endpoint := manifests[idx].HoldEndpoint
9798 // Try to get cached status first (instant)
99 if cached := h.HealthChecker.GetCachedStatus(endpoint); cached != nil {
100 mu.Lock()
101- manifests[idx].Reachable = cached.Reachable
102- manifests[idx].Pending = false
103 mu.Unlock()
104 return
105 }
···110 mu.Lock()
111 if ctx.Err() == context.DeadlineExceeded {
112 // Timeout - mark as pending for HTMX polling
113- manifests[idx].Reachable = false
114- manifests[idx].Pending = true
115 } else if err != nil {
116 // Error - mark as unreachable
117- manifests[idx].Reachable = false
118- manifests[idx].Pending = false
119 } else {
120 // Success
121- manifests[idx].Reachable = reachable
122- manifests[idx].Pending = false
123 }
124 mu.Unlock()
125- }(i)
126 }
127128 // Wait for all checks to complete or timeout
···89 continue
90 }
9192+ wg.Go(func() {
93+ endpoint := manifests[i].HoldEndpoint
0009495 // Try to get cached status first (instant)
96 if cached := h.HealthChecker.GetCachedStatus(endpoint); cached != nil {
97 mu.Lock()
98+ manifests[i].Reachable = cached.Reachable
99+ manifests[i].Pending = false
100 mu.Unlock()
101 return
102 }
···107 mu.Lock()
108 if ctx.Err() == context.DeadlineExceeded {
109 // Timeout - mark as pending for HTMX polling
110+ manifests[i].Reachable = false
111+ manifests[i].Pending = true
112 } else if err != nil {
113 // Error - mark as unreachable
114+ manifests[i].Reachable = false
115+ manifests[i].Pending = false
116 } else {
117 // Success
118+ manifests[i].Reachable = reachable
119+ manifests[i].Pending = false
120 }
121 mu.Unlock()
122+ })
123 }
124125 // Wait for all checks to complete or timeout
+7-14
pkg/appview/holdhealth/worker.go
···5354// Start begins the background worker
55func (w *Worker) Start(ctx context.Context) {
56- w.wg.Add(1)
57- go func() {
58- defer w.wg.Done()
59-60 slog.Info("Hold health worker starting background health checks")
6162 // Wait for services to be ready (Docker startup race condition)
···89 w.checker.Cleanup()
90 }
91 }
92- }()
93}
9495// Stop gracefully stops the worker
···154 var statsMu sync.Mutex
155156 for _, endpoint := range uniqueEndpoints {
157- wg.Add(1)
158-159- go func(ep string) {
160- defer wg.Done()
161-162 // Acquire semaphore
163 sem <- struct{}{}
164 defer func() { <-sem }()
165166 // Check health
167- isReachable, err := w.checker.CheckHealth(ctx, ep)
168169 // Update cache
170- w.checker.SetStatus(ep, isReachable, err)
171172 // Update stats
173 statsMu.Lock()
···175 reachable++
176 } else {
177 unreachable++
178- slog.Warn("Hold health worker hold unreachable", "endpoint", ep, "error", err)
179 }
180 statsMu.Unlock()
181- }(endpoint)
182 }
183184 // Wait for all checks to complete
···5354// Start begins the background worker
55func (w *Worker) Start(ctx context.Context) {
56+ w.wg.Go(func() {
00057 slog.Info("Hold health worker starting background health checks")
5859 // Wait for services to be ready (Docker startup race condition)
···86 w.checker.Cleanup()
87 }
88 }
89+ })
90}
9192// Stop gracefully stops the worker
···151 var statsMu sync.Mutex
152153 for _, endpoint := range uniqueEndpoints {
154+ wg.Go(func() {
0000155 // Acquire semaphore
156 sem <- struct{}{}
157 defer func() { <-sem }()
158159 // Check health
160+ isReachable, err := w.checker.CheckHealth(ctx, endpoint)
161162 // Update cache
163+ w.checker.SetStatus(endpoint, isReachable, err)
164165 // Update stats
166 statsMu.Lock()
···168 reachable++
169 } else {
170 unreachable++
171+ slog.Warn("Hold health worker hold unreachable", "endpoint", endpoint, "error", err)
172 }
173 statsMu.Unlock()
174+ })
175 }
176177 // Wait for all checks to complete
+1-3
pkg/appview/licenses/licenses.go
···129 licensesStr = strings.ReplaceAll(licensesStr, " OR ", ",")
130 licensesStr = strings.ReplaceAll(licensesStr, ";", ",")
131132- parts := strings.Split(licensesStr, ",")
133-134 var result []LicenseInfo
135 seen := make(map[string]bool) // Deduplicate
136137- for _, part := range parts {
138 part = strings.TrimSpace(part)
139 if part == "" {
140 continue
···129 licensesStr = strings.ReplaceAll(licensesStr, " OR ", ",")
130 licensesStr = strings.ReplaceAll(licensesStr, ";", ",")
13100132 var result []LicenseInfo
133 seen := make(map[string]bool) // Deduplicate
134135+ for part := range strings.SplitSeq(licensesStr, ",") {
136 part = strings.TrimSpace(part)
137 if part == "" {
138 continue
+5-8
pkg/appview/middleware/auth_test.go
···358 var wg sync.WaitGroup
359 var mu sync.Mutex // Protect results map
360361- for i := range 10 {
362- wg.Add(1)
363- go func(index int, sessionID string) {
364- defer wg.Done()
365-366 req := httptest.NewRequest("GET", "/test", nil)
367 req.AddCookie(&http.Cookie{
368 Name: "atcr_session",
369- Value: sessionID,
370 })
371 w := httptest.NewRecorder()
372373 wrappedHandler.ServeHTTP(w, req)
374375 mu.Lock()
376- results[index] = w.Code
377 mu.Unlock()
378- }(i, sessionIDs[i])
379 }
380381 wg.Wait()
···358 var wg sync.WaitGroup
359 var mu sync.Mutex // Protect results map
360361+ for i := range results {
362+ wg.Go(func() {
000363 req := httptest.NewRequest("GET", "/test", nil)
364 req.AddCookie(&http.Cookie{
365 Name: "atcr_session",
366+ Value: sessionIDs[i],
367 })
368 w := httptest.NewRecorder()
369370 wrappedHandler.ServeHTTP(w, req)
371372 mu.Lock()
373+ results[i] = w.Code
374 mu.Unlock()
375+ })
376 }
377378 wg.Wait()
+2-4
pkg/appview/storage/profile_test.go
···341 // Make 5 concurrent GetProfile calls
342 var wg sync.WaitGroup
343 for range 5 {
344- wg.Add(1)
345- go func() {
346- defer wg.Done()
347 _, err := GetProfile(context.Background(), client)
348 if err != nil {
349 t.Errorf("GetProfile() error = %v", err)
350 }
351- }()
352 }
353354 wg.Wait()
···341 // Make 5 concurrent GetProfile calls
342 var wg sync.WaitGroup
343 for range 5 {
344+ wg.Go(func() {
00345 _, err := GetProfile(context.Background(), client)
346 if err != nil {
347 t.Errorf("GetProfile() error = %v", err)
348 }
349+ })
350 }
351352 wg.Wait()
+42-45
pkg/appview/storage/routing_repository.go
···7import (
8 "context"
9 "log/slog"
01011 "github.com/distribution/distribution/v3"
12)
···18// RoutingRepository routes manifests to ATProto and blobs to external hold service
19// The registry (AppView) is stateless and NEVER stores blobs locally
20// NOTE: A fresh instance is created per-request (see middleware/registry.go)
21-// so no mutex is needed - each request has its own instance
22type RoutingRepository struct {
23 distribution.Repository
24- Ctx *RegistryContext // All context and services (exported for token updates)
25- manifestStore *ManifestStore // Manifest store instance (lazy-initialized)
26- blobStore *ProxyBlobStore // Blob store instance (lazy-initialized)
0027}
2829// NewRoutingRepository creates a new routing repository
···3637// Manifests returns the ATProto-backed manifest service
38func (r *RoutingRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
39- // Lazy-initialize manifest store (no mutex needed - one instance per request)
40- if r.manifestStore == nil {
41 // Ensure blob store is created first (needed for label extraction during push)
42 blobStore := r.Blobs(ctx)
43 r.manifestStore = NewManifestStore(r.Ctx, blobStore)
44- }
45 return r.manifestStore, nil
46}
4748// Blobs returns a proxy blob store that routes to external hold service
49// The registry (AppView) NEVER stores blobs locally - all blobs go through hold service
50func (r *RoutingRepository) Blobs(ctx context.Context) distribution.BlobStore {
51- // Return cached blob store if available (no mutex needed - one instance per request)
52- if r.blobStore != nil {
53- slog.Debug("Returning cached blob store", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository)
54- return r.blobStore
55- }
00005657- // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation
58- // Pull operations use the historical hold DID from the database (blobs are where they were pushed)
59- // Push operations use the discovery-based hold DID from user's profile/default
60- // This allows users to change their default hold and have new pushes go there
61- isPull := false
62- if method, ok := ctx.Value(HTTPRequestMethod).(string); ok {
63- isPull = method == "GET" || method == "HEAD"
64- }
6566- holdDID := r.Ctx.HoldDID // Default to discovery-based DID
67- holdSource := "discovery"
0000000000006869- // Only query database for pull operations
70- if isPull && r.Ctx.Database != nil {
71- // Query database for the latest manifest's hold DID
72- if dbHoldDID, err := r.Ctx.Database.GetLatestHoldDIDForRepo(r.Ctx.DID, r.Ctx.Repository); err == nil && dbHoldDID != "" {
73- // Use hold DID from database (pull case - use historical reference)
74- holdDID = dbHoldDID
75- holdSource = "database"
76- slog.Debug("Using hold from database manifest (pull)", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", dbHoldDID)
77- } else if err != nil {
78- // Log error but don't fail - fall back to discovery-based DID
79- slog.Warn("Failed to query database for hold DID", "component", "storage/blobs", "error", err)
80 }
81- // If dbHoldDID is empty (no manifests yet), fall through to use discovery-based DID
82- }
8384- if holdDID == "" {
85- // This should never happen if middleware is configured correctly
86- panic("hold DID not set in RegistryContext - ensure default_hold_did is configured in middleware")
87- }
8889- slog.Debug("Using hold DID for blobs", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", holdDID, "source", holdSource)
90-91- // Update context with the correct hold DID (may be from database or discovered)
92- r.Ctx.HoldDID = holdDID
9394- // Create and cache proxy blob store
95- r.blobStore = NewProxyBlobStore(r.Ctx)
096 return r.blobStore
97}
98
···7import (
8 "context"
9 "log/slog"
10+ "sync"
1112 "github.com/distribution/distribution/v3"
13)
···19// RoutingRepository routes manifests to ATProto and blobs to external hold service
20// The registry (AppView) is stateless and NEVER stores blobs locally
21// NOTE: A fresh instance is created per-request (see middleware/registry.go)
022type RoutingRepository struct {
23 distribution.Repository
24+ Ctx *RegistryContext // All context and services (exported for token updates)
25+ manifestStore *ManifestStore // Manifest store instance (lazy-initialized)
26+ manifestStoreOnce sync.Once // Ensures thread-safe lazy initialization
27+ blobStore *ProxyBlobStore // Blob store instance (lazy-initialized)
28+ blobStoreOnce sync.Once // Ensures thread-safe lazy initialization
29}
3031// NewRoutingRepository creates a new routing repository
···3839// Manifests returns the ATProto-backed manifest service
40func (r *RoutingRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
41+ r.manifestStoreOnce.Do(func() {
042 // Ensure blob store is created first (needed for label extraction during push)
43 blobStore := r.Blobs(ctx)
44 r.manifestStore = NewManifestStore(r.Ctx, blobStore)
45+ })
46 return r.manifestStore, nil
47}
4849// Blobs returns a proxy blob store that routes to external hold service
50// The registry (AppView) NEVER stores blobs locally - all blobs go through hold service
51func (r *RoutingRepository) Blobs(ctx context.Context) distribution.BlobStore {
52+ r.blobStoreOnce.Do(func() {
53+ // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation
54+ // Pull operations use the historical hold DID from the database (blobs are where they were pushed)
55+ // Push operations use the discovery-based hold DID from user's profile/default
56+ // This allows users to change their default hold and have new pushes go there
57+ isPull := false
58+ if method, ok := ctx.Value(HTTPRequestMethod).(string); ok {
59+ isPull = method == "GET" || method == "HEAD"
60+ }
6162+ holdDID := r.Ctx.HoldDID // Default to discovery-based DID
63+ holdSource := "discovery"
0000006465+ // Only query database for pull operations
66+ if isPull && r.Ctx.Database != nil {
67+ // Query database for the latest manifest's hold DID
68+ if dbHoldDID, err := r.Ctx.Database.GetLatestHoldDIDForRepo(r.Ctx.DID, r.Ctx.Repository); err == nil && dbHoldDID != "" {
69+ // Use hold DID from database (pull case - use historical reference)
70+ holdDID = dbHoldDID
71+ holdSource = "database"
72+ slog.Debug("Using hold from database manifest (pull)", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", dbHoldDID)
73+ } else if err != nil {
74+ // Log error but don't fail - fall back to discovery-based DID
75+ slog.Warn("Failed to query database for hold DID", "component", "storage/blobs", "error", err)
76+ }
77+ // If dbHoldDID is empty (no manifests yet), fall through to use discovery-based DID
78+ }
7980+ if holdDID == "" {
81+ // This should never happen if middleware is configured correctly
82+ panic("hold DID not set in RegistryContext - ensure default_hold_did is configured in middleware")
0000000083 }
008485+ slog.Debug("Using hold DID for blobs", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", holdDID, "source", holdSource)
0008687+ // Update context with the correct hold DID (may be from database or discovered)
88+ r.Ctx.HoldDID = holdDID
008990+ // Create and cache proxy blob store
91+ r.blobStore = NewProxyBlobStore(r.Ctx)
92+ })
93 return r.blobStore
94}
95
+6-10
pkg/appview/storage/routing_repository_test.go
···318319 // Concurrent access to Manifests()
320 for i := 0; i < numGoroutines; i++ {
321- wg.Add(1)
322- go func(index int) {
323- defer wg.Done()
324 store, err := repo.Manifests(context.Background())
325 require.NoError(t, err)
326- manifestStores[index] = store
327- }(i)
328 }
329330 wg.Wait()
···341342 // Concurrent access to Blobs()
343 for i := 0; i < numGoroutines; i++ {
344- wg.Add(1)
345- go func(index int) {
346- defer wg.Done()
347- blobStores[index] = repo.Blobs(context.Background())
348- }(i)
349 }
350351 wg.Wait()
···318319 // Concurrent access to Manifests()
320 for i := 0; i < numGoroutines; i++ {
321+ wg.Go(func() {
00322 store, err := repo.Manifests(context.Background())
323 require.NoError(t, err)
324+ manifestStores[i] = store
325+ })
326 }
327328 wg.Wait()
···339340 // Concurrent access to Blobs()
341 for i := 0; i < numGoroutines; i++ {
342+ wg.Go(func() {
343+ blobStores[i] = repo.Blobs(context.Background())
344+ })
00345 }
346347 wg.Wait()
+5-9
pkg/atproto/directory_test.go
···29 t.Run("concurrent access is thread-safe", func(t *testing.T) {
30 const numGoroutines = 100
31 var wg sync.WaitGroup
32- wg.Add(numGoroutines)
3334 // Channel to collect all directory instances
35 instances := make(chan any, numGoroutines)
3637 // Launch many goroutines concurrently accessing GetDirectory
38 for range numGoroutines {
39- go func() {
40- defer wg.Done()
41 dir := GetDirectory()
42 instances <- dir
43- }()
44 }
4546 // Wait for all goroutines to complete
···120121 const numGoroutines = 50
122 var wg sync.WaitGroup
123- wg.Add(numGoroutines)
124125 instances := make([]any, numGoroutines)
126 var mu sync.Mutex
127128 // Simulate many goroutines trying to get the directory simultaneously
129 for i := 0; i < numGoroutines; i++ {
130- go func(idx int) {
131- defer wg.Done()
132 dir := GetDirectory()
133 mu.Lock()
134- instances[idx] = dir
135 mu.Unlock()
136- }(i)
137 }
138139 wg.Wait()
···29 t.Run("concurrent access is thread-safe", func(t *testing.T) {
30 const numGoroutines = 100
31 var wg sync.WaitGroup
03233 // Channel to collect all directory instances
34 instances := make(chan any, numGoroutines)
3536 // Launch many goroutines concurrently accessing GetDirectory
37 for range numGoroutines {
38+ wg.Go(func() {
039 dir := GetDirectory()
40 instances <- dir
41+ })
42 }
4344 // Wait for all goroutines to complete
···118119 const numGoroutines = 50
120 var wg sync.WaitGroup
0121122 instances := make([]any, numGoroutines)
123 var mu sync.Mutex
124125 // Simulate many goroutines trying to get the directory simultaneously
126 for i := 0; i < numGoroutines; i++ {
127+ wg.Go(func() {
0128 dir := GetDirectory()
129 mu.Lock()
130+ instances[i] = dir
131 mu.Unlock()
132+ })
133 }
134135 wg.Wait()