Skip to content

Commit 40143ff

Browse files
zshannonclaude
andcommitted
Add cache expiration and flush endpoint to TypeScript server
- Add TTL-based expiration to LRU cache entries (24h for found, 60s for not found) - Implement /flush-cache POST endpoint to manually clear cache - Update cache to check expiration on retrieval and refetch stale entries - Update README with version parameter docs and new endpoint This improves cache behavior by allowing quick refresh of "not found" entries while keeping successful lookups cached longer for performance. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 88e7408 commit 40143ff

File tree

2 files changed

+100
-25
lines changed

2 files changed

+100
-25
lines changed

server/README.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ A high-performance HTTP server that provides TypeScript type checking and JavaSc
77
- **TypeScript type checking**: Full TypeScript type checking with detailed diagnostics
88
- **JavaScript bundling**: Uses esbuild for fast, optimized JavaScript output
99
- **React support**: Built-in React global transform for JSX
10-
- **In-memory compilation**: No file system I/O required
11-
- **Module caching**: Node modules loaded once at startup for fast performance
10+
- **S3-backed module system**: Node modules and type definitions loaded from S3
11+
- **LRU caching**: In-memory cache for S3 content with configurable size
1212
- **High performance**: ~155ms average build time, ~208ms typecheck (including network latency)
1313

1414
## API
@@ -30,13 +30,8 @@ Returns server health and statistics.
3030
"status": "healthy",
3131
"version": "1.0.0",
3232
"uptime": "26s",
33-
"modules": {
34-
"TotalFiles": 2557,
35-
"TypeDefinitions": 482,
36-
"JavaScriptFiles": 2056,
37-
"PackageFiles": 19,
38-
"LoadErrors": 0
39-
}
33+
"cache_size": "32 MB",
34+
"cache_entries": 150
4035
}
4136
```
4237

@@ -46,7 +41,8 @@ Type checks TypeScript code without compilation.
4641
**Request:**
4742
```json
4843
{
49-
"code": "export const hello: string = 123"
44+
"code": "export const hello: string = 123",
45+
"version": "0.0.1"
5046
}
5147
```
5248

@@ -79,7 +75,8 @@ Compiles and bundles TypeScript code to JavaScript.
7975
**Request:**
8076
```json
8177
{
82-
"code": "export const hello: string = \"world\""
78+
"code": "export const hello: string = \"world\"",
79+
"version": "0.0.1"
8380
}
8481
```
8582

@@ -118,6 +115,24 @@ If type errors are found, returns them without attempting to build:
118115
}
119116
```
120117

118+
### `POST /flush-cache`
119+
Flushes the in-memory LRU cache for S3 content.
120+
121+
**Request:**
122+
```
123+
POST /flush-cache
124+
```
125+
126+
**Response:**
127+
```json
128+
{
129+
"status": "success",
130+
"message": "Cache flushed successfully",
131+
"entries_cleared": 150,
132+
"timestamp": 1702934567
133+
}
134+
```
135+
121136
## Deployment
122137

123138
### Fly.io

server/server.go

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,19 @@ var (
4343

4444
// Cache configuration
4545
cacheSize int64
46+
47+
// Cache TTL settings
48+
cacheTTLSuccess = 24 * time.Hour // TTL for found entries
49+
cacheTTLNotFound = 60 * time.Second // TTL for not found entries
4650
)
4751

4852
type CacheEntry struct {
49-
Exists bool // false if not found in S3
50-
IsFile bool // true for file, false for directory
51-
Content []byte // file content (only for files)
52-
Files []string // immediate child files (only for dirs)
53-
Dirs []string // immediate child dirs (only for dirs)
53+
Exists bool // false if not found in S3
54+
IsFile bool // true for file, false for directory
55+
Content []byte // file content (only for files)
56+
Files []string // immediate child files (only for dirs)
57+
Dirs []string // immediate child dirs (only for dirs)
58+
ExpiresAt time.Time // expiration time for this entry
5459
}
5560

5661
type TypecheckRequest struct {
@@ -115,11 +120,21 @@ func getFromCache(ctx context.Context, version, path string) *CacheEntry {
115120
// Check cache first
116121
cacheMutex.RLock()
117122
if cached, ok := cache.Get(cacheKey); ok {
123+
// Check if entry has expired
124+
if time.Now().Before(cached.ExpiresAt) {
125+
cacheMutex.RUnlock()
126+
s3CacheHits.Inc()
127+
return cached
128+
}
129+
// Entry expired, will refetch
130+
cacheMutex.RUnlock()
131+
// Remove expired entry
132+
cacheMutex.Lock()
133+
cache.Remove(cacheKey)
134+
cacheMutex.Unlock()
135+
} else {
118136
cacheMutex.RUnlock()
119-
s3CacheHits.Inc()
120-
return cached
121137
}
122-
cacheMutex.RUnlock()
123138

124139
s3CacheMisses.Inc()
125140

@@ -137,9 +152,10 @@ func getFromCache(ctx context.Context, version, path string) *CacheEntry {
137152
if err == nil {
138153
// log.Printf("S3 file found: %s (%d bytes)", cacheKey, len(content))
139154
entry := &CacheEntry{
140-
Exists: true,
141-
IsFile: true,
142-
Content: content,
155+
Exists: true,
156+
IsFile: true,
157+
Content: content,
158+
ExpiresAt: time.Now().Add(cacheTTLSuccess),
143159
}
144160
cacheMutex.Lock()
145161
cache.Add(cacheKey, entry)
@@ -169,8 +185,11 @@ func getFromCache(ctx context.Context, version, path string) *CacheEntry {
169185
if err != nil {
170186
s3ListErrors.Inc()
171187
// log.Printf("S3 list error for key %s: %v", cacheKey, err)
172-
// Cache as non-existent
173-
entry := &CacheEntry{Exists: false}
188+
// Cache as non-existent with shorter TTL
189+
entry := &CacheEntry{
190+
Exists: false,
191+
ExpiresAt: time.Now().Add(cacheTTLNotFound),
192+
}
174193
cacheMutex.Lock()
175194
cache.Add(cacheKey, entry)
176195
cacheMutex.Unlock()
@@ -208,6 +227,13 @@ func getFromCache(ctx context.Context, version, path string) *CacheEntry {
208227
}
209228
}
210229

230+
// Set expiration based on whether entry was found
231+
if entry.Exists {
232+
entry.ExpiresAt = time.Now().Add(cacheTTLSuccess)
233+
} else {
234+
entry.ExpiresAt = time.Now().Add(cacheTTLNotFound)
235+
}
236+
211237
// Cache the result
212238
cacheMutex.Lock()
213239
cache.Add(cacheKey, entry)
@@ -829,6 +855,39 @@ func build(w http.ResponseWriter, req *http.Request) {
829855
json.NewEncoder(w).Encode(response)
830856
}
831857

858+
func flushCache(w http.ResponseWriter, req *http.Request) {
859+
if req.Method != http.MethodPost {
860+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
861+
return
862+
}
863+
864+
// Get cache metrics before flush
865+
var entriesBefore int
866+
cacheMutex.RLock()
867+
if cache != nil {
868+
entriesBefore = cache.Len()
869+
}
870+
cacheMutex.RUnlock()
871+
872+
// Flush the cache
873+
cacheMutex.Lock()
874+
if cache != nil {
875+
cache.Purge()
876+
}
877+
cacheMutex.Unlock()
878+
879+
// Return flush summary
880+
response := map[string]interface{}{
881+
"status": "success",
882+
"message": "Cache flushed successfully",
883+
"entries_cleared": entriesBefore,
884+
"timestamp": time.Now().Unix(),
885+
}
886+
887+
w.Header().Set("Content-Type", "application/json")
888+
json.NewEncoder(w).Encode(response)
889+
}
890+
832891
func main() {
833892
log.Printf("TypeScript Go Server v%s starting...", serverVersion)
834893

@@ -883,13 +942,14 @@ func main() {
883942
http.HandleFunc("/health", loggingMiddleware(health))
884943
http.HandleFunc("/typecheck", loggingMiddleware(typecheck))
885944
http.HandleFunc("/build", loggingMiddleware(build))
945+
http.HandleFunc("/flush-cache", loggingMiddleware(flushCache))
886946
http.HandleFunc("/", loggingMiddleware(hello))
887947

888948
// Start Prometheus metrics server on port 9091
889949
go startMetricsServer()
890950

891951
log.Printf("Server ready! Listening on :8080...")
892-
log.Printf("Endpoints: /, /health, /typecheck, /build")
952+
log.Printf("Endpoints: /, /health, /typecheck, /build, /flush-cache")
893953
log.Printf("Metrics available at :9091/metrics")
894954

895955
if err := http.ListenAndServe(":8080", nil); err != nil {

0 commit comments

Comments
 (0)