Skip to content

Commit e342482

Browse files
committed
perf(shim): add shim-to-runtime mapping cache for O(1) lookups
Add a persistent shim-to-runtime mapping cache that enables O(1) lookups and fixes dynamic shim resolution for globally installed packages. Changes: - Add cache directory to Paths struct (~/.dtvem/cache/) - Generate shim-map.json during reshim operations - Load cache in mapShimToRuntime() with sync.Once for efficiency - Fall back to provider-based lookup if cache is missing This fixes shims for dynamically installed packages (tsc, eslint, black, pytest, etc.) which previously failed with "runtime provider not found" because the fallback returned the shim name as the runtime name. Closes #93 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 92b4731 commit e342482

5 files changed

Lines changed: 361 additions & 14 deletions

File tree

src/cmd/shim/main.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/dtvem/dtvem/src/internal/config"
1414
"github.com/dtvem/dtvem/src/internal/constants"
1515
"github.com/dtvem/dtvem/src/internal/runtime"
16+
"github.com/dtvem/dtvem/src/internal/shim"
1617
"github.com/dtvem/dtvem/src/internal/ui"
1718

1819
// Import runtime providers to register them
@@ -193,26 +194,33 @@ func getShimName() string {
193194
}
194195

195196
// mapShimToRuntime maps a shim name to its runtime
196-
// For example: python3 -> python, pip -> python, npm -> node
197-
// This queries all registered providers for their shims, eliminating the need
198-
// for a central hardcoded mapping.
197+
// For example: python3 -> python, pip -> python, npm -> node, tsc -> node
198+
// First checks the shim map cache (generated by reshim), then falls back
199+
// to querying registered providers.
199200
func mapShimToRuntime(shimName string) string {
201+
// First, try the shim map cache for O(1) lookup
202+
// This handles both core shims and dynamically installed packages (tsc, eslint, black, etc.)
203+
if runtimeName, ok := shim.LookupRuntime(shimName); ok {
204+
return runtimeName
205+
}
206+
207+
// Fall back to provider-based lookup if cache is missing or doesn't have the shim
200208
// Get all registered providers (using ShimProvider interface)
201209
providers := runtime.GetAllShimProviders()
202210

203211
// Check each provider's shims for an exact match first
204212
for _, provider := range providers {
205-
for _, shim := range provider.Shims() {
206-
if shim == shimName {
213+
for _, s := range provider.Shims() {
214+
if s == shimName {
207215
return provider.Name()
208216
}
209217
}
210218
}
211219

212220
// Check for prefix match (e.g., python3 -> python)
213221
for _, provider := range providers {
214-
for _, shim := range provider.Shims() {
215-
if strings.HasPrefix(shimName, shim) {
222+
for _, s := range provider.Shims() {
223+
if strings.HasPrefix(shimName, s) {
216224
return provider.Name()
217225
}
218226
}

src/internal/config/paths.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Paths struct {
1616
Shims string // Shims directory (~/.dtvem/shims)
1717
Versions string // Versions directory (~/.dtvem/versions)
1818
Config string // Config directory (~/.dtvem/config)
19+
Cache string // Cache directory (~/.dtvem/cache)
1920
}
2021

2122
var (
@@ -40,6 +41,7 @@ func initPaths() *Paths {
4041
Shims: filepath.Join(root, "shims"),
4142
Versions: filepath.Join(root, "versions"),
4243
Config: filepath.Join(root, "config"),
44+
Cache: filepath.Join(root, "cache"),
4345
}
4446
}
4547

@@ -90,6 +92,7 @@ func EnsureDirectories() error {
9092
paths.Shims,
9193
paths.Versions,
9294
paths.Config,
95+
paths.Cache,
9396
}
9497

9598
for _, dir := range dirs {
@@ -120,3 +123,19 @@ const LocalConfigDirName = ".dtvem"
120123

121124
// RuntimesFileName is the name of the runtimes configuration file
122125
const RuntimesFileName = "runtimes.json"
126+
127+
// ShimMapFileName is the name of the shim-to-runtime mapping cache file
128+
const ShimMapFileName = "shim-map.json"
129+
130+
// ShimMapPath returns the path to the shim-to-runtime mapping cache file
131+
func ShimMapPath() string {
132+
paths := DefaultPaths()
133+
return filepath.Join(paths.Cache, ShimMapFileName)
134+
}
135+
136+
// ResetPathsCache resets the cached paths, forcing reinitialization on next access.
137+
// This is primarily useful for testing.
138+
func ResetPathsCache() {
139+
pathsOnce = sync.Once{}
140+
defaultPaths = nil
141+
}

src/internal/shim/cache.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Package shim manages shim executables that intercept runtime commands
2+
package shim
3+
4+
import (
5+
"encoding/json"
6+
"os"
7+
"sync"
8+
9+
"github.com/dtvem/dtvem/src/internal/config"
10+
)
11+
12+
// ShimMap represents the shim-to-runtime mapping cache
13+
// The map key is the shim name (e.g., "tsc", "npm", "black")
14+
// The map value is the runtime name (e.g., "node", "python")
15+
type ShimMap map[string]string
16+
17+
var (
18+
shimMapCache ShimMap
19+
shimMapCacheOnce sync.Once
20+
shimMapCacheErr error
21+
)
22+
23+
// LoadShimMap loads the shim-to-runtime mapping from the cache file.
24+
// It uses sync.Once to ensure the cache is only loaded once per process.
25+
// Returns the cached map and any error that occurred during loading.
26+
func LoadShimMap() (ShimMap, error) {
27+
shimMapCacheOnce.Do(func() {
28+
shimMapCache, shimMapCacheErr = loadShimMapFromDisk()
29+
})
30+
return shimMapCache, shimMapCacheErr
31+
}
32+
33+
// loadShimMapFromDisk reads the shim map cache file from disk
34+
func loadShimMapFromDisk() (ShimMap, error) {
35+
cachePath := config.ShimMapPath()
36+
37+
data, err := os.ReadFile(cachePath)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
var shimMap ShimMap
43+
if err := json.Unmarshal(data, &shimMap); err != nil {
44+
return nil, err
45+
}
46+
47+
return shimMap, nil
48+
}
49+
50+
// SaveShimMap writes the shim-to-runtime mapping to the cache file.
51+
// This should be called during reshim operations.
52+
func SaveShimMap(shimMap ShimMap) error {
53+
// Ensure cache directory exists
54+
paths := config.DefaultPaths()
55+
if err := os.MkdirAll(paths.Cache, 0755); err != nil {
56+
return err
57+
}
58+
59+
cachePath := config.ShimMapPath()
60+
61+
data, err := json.MarshalIndent(shimMap, "", " ")
62+
if err != nil {
63+
return err
64+
}
65+
66+
return os.WriteFile(cachePath, data, 0644)
67+
}
68+
69+
// LookupRuntime looks up the runtime for a given shim name using the cache.
70+
// Returns the runtime name and true if found, or empty string and false if not.
71+
func LookupRuntime(shimName string) (string, bool) {
72+
shimMap, err := LoadShimMap()
73+
if err != nil {
74+
return "", false
75+
}
76+
77+
runtime, ok := shimMap[shimName]
78+
return runtime, ok
79+
}
80+
81+
// ResetShimMapCache resets the cached shim map, forcing a reload on next access.
82+
// This is primarily useful for testing.
83+
func ResetShimMapCache() {
84+
shimMapCacheOnce = sync.Once{}
85+
shimMapCache = nil
86+
shimMapCacheErr = nil
87+
}

0 commit comments

Comments
 (0)