You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Mar 12, 2026. It is now read-only.
Every API call hits the server, even for frequently accessed, rarely-changing data (projects list, people list). This is inefficient for interactive CLI usage.
Current state: Only UserResolver (internal/utils/users.go:12-94) has request-level caching, which doesn't persist across commands.
"Most API responses include HTTP freshness headers: ETag and Last-Modified"
"Store these values and return via If-None-Match and If-Modified-Since headers in subsequent requests"
"A 304 Not Modified response indicates unchanged content, saving bandwidth"
Proposed Solution
1. Create new file internal/api/cache.go
typeCacheEntrystruct {
Data []byteETagstringLastModifiedstringExpiresAt time.Time
}
typeResponseCachestruct {
mu sync.RWMutexentriesmap[string]*CacheEntrymaxSizeintttl time.Duration
}
funcNewResponseCache(maxSizeint, defaultTTL time.Duration) *ResponseCachefunc (c*ResponseCache) Get(keystring) (*CacheEntry, bool)
func (c*ResponseCache) Set(keystring, entry*CacheEntry)
func (c*ResponseCache) Invalidate(patternstring) // For cache busting on mutations
2. Create caching transport internal/api/cache_transport.go
typeCachingTransportstruct {
Base http.RoundTripperCache*ResponseCache
}
func (ct*CachingTransport) RoundTrip(req*http.Request) (*http.Response, error)
The transport should:
For GET requests: check cache, add If-None-Match/If-Modified-Since headers
Handle 304 responses by returning cached data
Store new responses with ETag/Last-Modified
Pass through non-GET requests unchanged
3. Integrate into client
Add cache to Client struct
Invalidate relevant cache entries on POST/PUT/DELETE
Files to Modify
File
Changes
internal/api/cache.go
New file - cache implementation
internal/api/cache_transport.go
New file - HTTP transport with caching
internal/api/client.go:20-25
Add cache field to Client struct
internal/api/client.go:29-37
Initialize cache in NewClient()
internal/api/client.go:110-168
Invalidate cache on POST/PUT/DELETE
internal/api/cache_test.go
New file - cache tests
Cache Strategy
Endpoint Pattern
TTL
Notes
/projects.json
5 min
Rarely changes
/projects/{id}.json
2 min
Individual project
*/people.json
10 min
Team members rarely change
*/todolists.json
1 min
Lists change occasionally
*/todos.json
30 sec
Todos change frequently
Invalidation rules:
POST/PUT/DELETE to any path invalidates cache entries matching that resource type
Example: POST to /buckets/123/todos.json invalidates all /buckets/123/todo* entries
Acceptance Criteria
In-memory cache with configurable max size (default: 100 entries)
Store and send ETag via If-None-Match header
Store and send Last-Modified via If-Modified-Since header
Handle 304 Not Modified by returning cached response
TTL-based expiration per endpoint type
Cache invalidation on mutations (POST/PUT/DELETE)
Thread-safe implementation
LRU eviction when cache is full
Optional: --no-cache flag for commands that need fresh data
Testing
Unit tests for cache operations (get, set, invalidate, expiry)
Test If-None-Match / 304 handling
Test If-Modified-Since / 304 handling
Test cache invalidation on mutations
Test LRU eviction behavior
Test thread safety with concurrent access
Notes
This cache is in-memory and per-process. For a CLI tool, this means:
Cache is warm during interactive sessions with multiple commands
Cache starts fresh on each new bc4 invocation
Consider future enhancement: optional disk-based cache for persistence
Problem
Every API call hits the server, even for frequently accessed, rarely-changing data (projects list, people list). This is inefficient for interactive CLI usage.
Current state: Only
UserResolver(internal/utils/users.go:12-94) has request-level caching, which doesn't persist across commands.Per Basecamp API docs:
Proposed Solution
1. Create new file
internal/api/cache.go2. Create caching transport
internal/api/cache_transport.goThe transport should:
If-None-Match/If-Modified-Sinceheaders3. Integrate into client
ClientstructFiles to Modify
internal/api/cache.gointernal/api/cache_transport.gointernal/api/client.go:20-25Clientstructinternal/api/client.go:29-37NewClient()internal/api/client.go:110-168internal/api/cache_test.goCache Strategy
/projects.json/projects/{id}.json*/people.json*/todolists.json*/todos.jsonInvalidation rules:
/buckets/123/todos.jsoninvalidates all/buckets/123/todo*entriesAcceptance Criteria
ETagviaIf-None-MatchheaderLast-ModifiedviaIf-Modified-Sinceheader--no-cacheflag for commands that need fresh dataTesting
If-None-Match/ 304 handlingIf-Modified-Since/ 304 handlingNotes
This cache is in-memory and per-process. For a CLI tool, this means:
bc4invocation