diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go index f7b75e3a..2cdbf337 100644 --- a/internal/lsp/cache/cache.go +++ b/internal/lsp/cache/cache.go @@ -2,15 +2,14 @@ package cache import ( "fmt" - "maps" "os" - "sync" "github.com/anderseknert/roast/pkg/util" "github.com/open-policy-agent/opa/ast" "github.com/styrainc/regal/internal/lsp/types" + "github.com/styrainc/regal/internal/util/concurrent" "github.com/styrainc/regal/pkg/report" ) @@ -18,183 +17,119 @@ import ( // diagnostics for each file (including diagnostics gathered from linting files alongside other files). type Cache struct { // fileContents is a map of file URI to raw file contents received from the client - fileContents map[string]string + fileContents *concurrent.Map[string, string] // ignoredFileContents is a similar map of file URI to raw file contents // but it's not queried for project level operations like goto definition, // linting etc. // ignoredFileContents is also cleared on the delete operation. - ignoredFileContents map[string]string + ignoredFileContents *concurrent.Map[string, string] // modules is a map of file URI to parsed AST modules from the latest file contents value - modules map[string]*ast.Module + modules *concurrent.Map[string, *ast.Module] // aggregateData stores the aggregate data from evaluations for each file. // This is used to cache the results of expensive evaluations and can be used // to update aggregate diagostics incrementally. - aggregateData map[string][]report.Aggregate - aggregateDataMu sync.Mutex + aggregateData *concurrent.Map[string, []report.Aggregate] // diagnosticsFile is a map of file URI to diagnostics for that file - diagnosticsFile map[string][]types.Diagnostic + diagnosticsFile *concurrent.Map[string, []types.Diagnostic] // diagnosticsParseErrors is a map of file URI to parse errors for that file - diagnosticsParseErrors map[string][]types.Diagnostic + diagnosticsParseErrors *concurrent.Map[string, []types.Diagnostic] // builtinPositionsFile is a map of file URI to builtin positions for that file - builtinPositionsFile map[string]map[uint][]types.BuiltinPosition + builtinPositionsFile *concurrent.Map[string, map[uint][]types.BuiltinPosition] // keywordLocationsFile is a map of file URI to Rego keyword locations for that file // to be used for hover hints. - keywordLocationsFile map[string]map[uint][]types.KeywordLocation + keywordLocationsFile *concurrent.Map[string, map[uint][]types.KeywordLocation] // fileRefs is a map of file URI to refs that are defined in that file. These are // intended to be used for completions in other files. // fileRefs is expected to be updated when a file is successfully parsed. - fileRefs map[string]map[string]types.Ref - fileContentsMu sync.Mutex - - ignoredFileContentsMu sync.Mutex - - moduleMu sync.Mutex - - diagnosticsFileMu sync.Mutex - - diagnosticsParseMu sync.Mutex - - builtinPositionsMu sync.Mutex - - keywordLocationsMu sync.Mutex - - fileRefMu sync.Mutex + fileRefs *concurrent.Map[string, map[string]types.Ref] } func NewCache() *Cache { return &Cache{ - fileContents: make(map[string]string), - ignoredFileContents: make(map[string]string), - - modules: make(map[string]*ast.Module), - - aggregateData: make(map[string][]report.Aggregate), - - diagnosticsFile: make(map[string][]types.Diagnostic), - diagnosticsParseErrors: make(map[string][]types.Diagnostic), - - builtinPositionsFile: make(map[string]map[uint][]types.BuiltinPosition), - keywordLocationsFile: make(map[string]map[uint][]types.KeywordLocation), - - fileRefs: make(map[string]map[string]types.Ref), + fileContents: concurrent.MapOf(make(map[string]string)), + ignoredFileContents: concurrent.MapOf(make(map[string]string)), + modules: concurrent.MapOf(make(map[string]*ast.Module)), + aggregateData: concurrent.MapOf(make(map[string][]report.Aggregate)), + diagnosticsFile: concurrent.MapOf(make(map[string][]types.Diagnostic)), + diagnosticsParseErrors: concurrent.MapOf(make(map[string][]types.Diagnostic)), + builtinPositionsFile: concurrent.MapOf(make(map[string]map[uint][]types.BuiltinPosition)), + keywordLocationsFile: concurrent.MapOf(make(map[string]map[uint][]types.KeywordLocation)), + fileRefs: concurrent.MapOf(make(map[string]map[string]types.Ref)), } } func (c *Cache) GetAllFiles() map[string]string { - c.fileContentsMu.Lock() - defer c.fileContentsMu.Unlock() - - return maps.Clone(c.fileContents) + return c.fileContents.Clone() } func (c *Cache) GetFileContents(fileURI string) (string, bool) { - c.fileContentsMu.Lock() - defer c.fileContentsMu.Unlock() - - val, ok := c.fileContents[fileURI] - - return val, ok + return c.fileContents.Get(fileURI) } func (c *Cache) SetFileContents(fileURI string, content string) { - c.fileContentsMu.Lock() - defer c.fileContentsMu.Unlock() - - c.fileContents[fileURI] = content + c.fileContents.Set(fileURI, content) } func (c *Cache) GetIgnoredFileContents(fileURI string) (string, bool) { - c.ignoredFileContentsMu.Lock() - defer c.ignoredFileContentsMu.Unlock() - - val, ok := c.ignoredFileContents[fileURI] - - return val, ok + return c.ignoredFileContents.Get(fileURI) } func (c *Cache) SetIgnoredFileContents(fileURI string, content string) { - c.ignoredFileContentsMu.Lock() - defer c.ignoredFileContentsMu.Unlock() - - c.ignoredFileContents[fileURI] = content + c.ignoredFileContents.Set(fileURI, content) } func (c *Cache) GetAllIgnoredFiles() map[string]string { - c.ignoredFileContentsMu.Lock() - defer c.ignoredFileContentsMu.Unlock() - - return maps.Clone(c.ignoredFileContents) + return c.ignoredFileContents.Clone() } func (c *Cache) ClearIgnoredFileContents(fileURI string) { - c.ignoredFileContentsMu.Lock() - defer c.ignoredFileContentsMu.Unlock() - - delete(c.ignoredFileContents, fileURI) + c.ignoredFileContents.Delete(fileURI) } func (c *Cache) GetAllModules() map[string]*ast.Module { - c.moduleMu.Lock() - defer c.moduleMu.Unlock() - - return maps.Clone(c.modules) + return c.modules.Clone() } func (c *Cache) GetModule(fileURI string) (*ast.Module, bool) { - c.moduleMu.Lock() - defer c.moduleMu.Unlock() - - val, ok := c.modules[fileURI] - - return val, ok + return c.modules.Get(fileURI) } func (c *Cache) SetModule(fileURI string, module *ast.Module) { - c.moduleMu.Lock() - defer c.moduleMu.Unlock() - - c.modules[fileURI] = module + c.modules.Set(fileURI, module) } // SetFileAggregates will only set aggregate data for the provided URI. Even if // data for other files is provided, only the specified URI is updated. func (c *Cache) SetFileAggregates(fileURI string, data map[string][]report.Aggregate) { - c.aggregateDataMu.Lock() - defer c.aggregateDataMu.Unlock() - - flattenedAggregates := make([]report.Aggregate, 0) + flattenedAggregates := make([]report.Aggregate, 0, len(data)) for _, aggregates := range data { for _, aggregate := range aggregates { - if aggregate.SourceFile() != fileURI { - continue + if aggregate.SourceFile() == fileURI { + flattenedAggregates = append(flattenedAggregates, aggregate) } - - flattenedAggregates = append(flattenedAggregates, aggregate) } } - c.aggregateData[fileURI] = flattenedAggregates + c.aggregateData.Set(fileURI, flattenedAggregates) } func (c *Cache) SetAggregates(data map[string][]report.Aggregate) { - c.aggregateDataMu.Lock() - defer c.aggregateDataMu.Unlock() - - // clear the state - c.aggregateData = make(map[string][]report.Aggregate) + c.aggregateData.Clear() for _, aggregates := range data { for _, aggregate := range aggregates { - c.aggregateData[aggregate.SourceFile()] = append(c.aggregateData[aggregate.SourceFile()], aggregate) + c.aggregateData.UpdateValue(aggregate.SourceFile(), func(val []report.Aggregate) []report.Aggregate { + return append(val, aggregate) + }) } } } @@ -202,9 +137,6 @@ func (c *Cache) SetAggregates(data map[string][]report.Aggregate) { // GetFileAggregates is used to get aggregate data for a given list of files. // This is only used in tests to validate the cache state. func (c *Cache) GetFileAggregates(fileURIs ...string) map[string][]report.Aggregate { - c.aggregateDataMu.Lock() - defer c.aggregateDataMu.Unlock() - includedFiles := make(map[string]struct{}, len(fileURIs)) for _, fileURI := range fileURIs { includedFiles[fileURI] = struct{}{} @@ -214,7 +146,7 @@ func (c *Cache) GetFileAggregates(fileURIs ...string) map[string][]report.Aggreg allAggregates := make(map[string][]report.Aggregate) - for sourceFile, aggregates := range c.aggregateData { + for sourceFile, aggregates := range c.aggregateData.Clone() { if _, included := includedFiles[sourceFile]; !included && !getAll { continue } @@ -228,160 +160,92 @@ func (c *Cache) GetFileAggregates(fileURIs ...string) map[string][]report.Aggreg } func (c *Cache) GetFileDiagnostics(uri string) ([]types.Diagnostic, bool) { - c.diagnosticsFileMu.Lock() - defer c.diagnosticsFileMu.Unlock() - - val, ok := c.diagnosticsFile[uri] - - return val, ok + return c.diagnosticsFile.Get(uri) } func (c *Cache) SetFileDiagnostics(fileURI string, diags []types.Diagnostic) { - c.diagnosticsFileMu.Lock() - defer c.diagnosticsFileMu.Unlock() - - c.diagnosticsFile[fileURI] = diags + c.diagnosticsFile.Set(fileURI, diags) } // SetFileDiagnosticsForRules will perform a partial update of the diagnostics // for a file given a list of evaluated rules. func (c *Cache) SetFileDiagnosticsForRules(fileURI string, rules []string, diags []types.Diagnostic) { - c.diagnosticsFileMu.Lock() - defer c.diagnosticsFileMu.Unlock() - - ruleKeys := make(map[string]struct{}, len(rules)) - for _, rule := range rules { - ruleKeys[rule] = struct{}{} - } + c.diagnosticsFile.UpdateValue(fileURI, func(current []types.Diagnostic) []types.Diagnostic { + ruleKeys := make(map[string]struct{}, len(rules)) + for _, rule := range rules { + ruleKeys[rule] = struct{}{} + } - preservedDiagnostics := make([]types.Diagnostic, 0) + preservedDiagnostics := make([]types.Diagnostic, 0, len(current)) - for _, diag := range c.diagnosticsFile[fileURI] { - if _, ok := ruleKeys[diag.Code]; !ok { - preservedDiagnostics = append(preservedDiagnostics, diag) + for i := range current { + if _, ok := ruleKeys[current[i].Code]; !ok { + preservedDiagnostics = append(preservedDiagnostics, current[i]) + } } - } - c.diagnosticsFile[fileURI] = append(preservedDiagnostics, diags...) + return append(preservedDiagnostics, diags...) + }) } func (c *Cache) ClearFileDiagnostics() { - c.diagnosticsFileMu.Lock() - defer c.diagnosticsFileMu.Unlock() - - c.diagnosticsFile = make(map[string][]types.Diagnostic) + c.diagnosticsFile.Clear() } func (c *Cache) GetParseErrors(uri string) ([]types.Diagnostic, bool) { - c.diagnosticsParseMu.Lock() - defer c.diagnosticsParseMu.Unlock() - - val, ok := c.diagnosticsParseErrors[uri] - - return val, ok + return c.diagnosticsParseErrors.Get(uri) } func (c *Cache) SetParseErrors(fileURI string, diags []types.Diagnostic) { - c.diagnosticsParseMu.Lock() - defer c.diagnosticsParseMu.Unlock() - - c.diagnosticsParseErrors[fileURI] = diags + c.diagnosticsParseErrors.Set(fileURI, diags) } func (c *Cache) GetBuiltinPositions(fileURI string) (map[uint][]types.BuiltinPosition, bool) { - c.builtinPositionsMu.Lock() - defer c.builtinPositionsMu.Unlock() - - val, ok := c.builtinPositionsFile[fileURI] - - return val, ok + return c.builtinPositionsFile.Get(fileURI) } func (c *Cache) SetBuiltinPositions(fileURI string, positions map[uint][]types.BuiltinPosition) { - c.builtinPositionsMu.Lock() - defer c.builtinPositionsMu.Unlock() - - c.builtinPositionsFile[fileURI] = positions + c.builtinPositionsFile.Set(fileURI, positions) } func (c *Cache) GetAllBuiltInPositions() map[string]map[uint][]types.BuiltinPosition { - c.builtinPositionsMu.Lock() - defer c.builtinPositionsMu.Unlock() - - return maps.Clone(c.builtinPositionsFile) + return c.builtinPositionsFile.Clone() } func (c *Cache) SetKeywordLocations(fileURI string, keywords map[uint][]types.KeywordLocation) { - c.keywordLocationsMu.Lock() - defer c.keywordLocationsMu.Unlock() - - c.keywordLocationsFile[fileURI] = keywords + c.keywordLocationsFile.Set(fileURI, keywords) } func (c *Cache) GetKeywordLocations(fileURI string) (map[uint][]types.KeywordLocation, bool) { - c.keywordLocationsMu.Lock() - defer c.keywordLocationsMu.Unlock() - - val, ok := c.keywordLocationsFile[fileURI] - - return val, ok + return c.keywordLocationsFile.Get(fileURI) } func (c *Cache) SetFileRefs(fileURI string, items map[string]types.Ref) { - c.fileRefMu.Lock() - defer c.fileRefMu.Unlock() - - c.fileRefs[fileURI] = items + c.fileRefs.Set(fileURI, items) } func (c *Cache) GetFileRefs(fileURI string) map[string]types.Ref { - c.fileRefMu.Lock() - defer c.fileRefMu.Unlock() + refs, _ := c.fileRefs.Get(fileURI) - return c.fileRefs[fileURI] + return refs } func (c *Cache) GetAllFileRefs() map[string]map[string]types.Ref { - c.fileRefMu.Lock() - defer c.fileRefMu.Unlock() - - return maps.Clone(c.fileRefs) + return c.fileRefs.Clone() } // Delete removes all cached data for a given URI. Ignored file contents are // also removed if found for a matching URI. func (c *Cache) Delete(fileURI string) { - c.fileContentsMu.Lock() - delete(c.fileContents, fileURI) - c.fileContentsMu.Unlock() - - c.moduleMu.Lock() - delete(c.modules, fileURI) - c.moduleMu.Unlock() - - c.aggregateDataMu.Lock() - delete(c.aggregateData, fileURI) - c.aggregateDataMu.Unlock() - - c.diagnosticsFileMu.Lock() - delete(c.diagnosticsFile, fileURI) - c.diagnosticsFileMu.Unlock() - - c.diagnosticsParseMu.Lock() - delete(c.diagnosticsParseErrors, fileURI) - c.diagnosticsParseMu.Unlock() - - c.builtinPositionsMu.Lock() - delete(c.builtinPositionsFile, fileURI) - c.builtinPositionsMu.Unlock() - - c.fileRefMu.Lock() - delete(c.fileRefs, fileURI) - c.fileRefMu.Unlock() - - c.ignoredFileContentsMu.Lock() - delete(c.ignoredFileContents, fileURI) - c.ignoredFileContentsMu.Unlock() + c.fileContents.Delete(fileURI) + c.ignoredFileContents.Delete(fileURI) + c.modules.Delete(fileURI) + c.aggregateData.Delete(fileURI) + c.diagnosticsFile.Delete(fileURI) + c.diagnosticsParseErrors.Delete(fileURI) + c.builtinPositionsFile.Delete(fileURI) + c.keywordLocationsFile.Delete(fileURI) + c.fileRefs.Delete(fileURI) } func UpdateCacheForURIFromDisk(cache *Cache, fileURI, path string) (bool, string, error) { diff --git a/internal/lsp/connection.go b/internal/lsp/connection.go index 2661ca66..83f78d82 100644 --- a/internal/lsp/connection.go +++ b/internal/lsp/connection.go @@ -25,10 +25,11 @@ import ( "context" "fmt" "io" - "sync" "github.com/anderseknert/roast/pkg/encoding" "github.com/sourcegraph/jsonrpc2" + + "github.com/styrainc/regal/internal/util/concurrent" ) type ConnectionOptions struct { @@ -98,38 +99,14 @@ func logMessages(cfg ConnectionLoggingConfig) jsonrpc2.ConnOpt { return func(c *jsonrpc2.Conn) { // Remember reqs we have received so that we can helpfully show the // request method in OnSend for responses. - var ( - mu sync.Mutex - reqMethods = map[jsonrpc2.ID]string{} - ) - - setMethod := func(id jsonrpc2.ID, method string) { - mu.Lock() - defer mu.Unlock() - - reqMethods[id] = method - } - - getMethod := func(id jsonrpc2.ID) string { - mu.Lock() - defer mu.Unlock() - - return reqMethods[id] - } - - deleteMethod := func(id jsonrpc2.ID) { - mu.Lock() - defer mu.Unlock() - - delete(reqMethods, id) - } + reqMethods := concurrent.MapOf(make(map[jsonrpc2.ID]string)) if cfg.LogInbound { - jsonrpc2.OnRecv(buildRecvHandler(setMethod, logger, cfg))(c) + jsonrpc2.OnRecv(buildRecvHandler(reqMethods.Set, logger, cfg))(c) } if cfg.LogOutbound { - jsonrpc2.OnSend(buildSendHandler(getMethod, deleteMethod, logger, cfg))(c) + jsonrpc2.OnSend(buildSendHandler(reqMethods.GetUnchecked, reqMethods.Delete, logger, cfg))(c) } } } diff --git a/internal/lsp/hover/hover.go b/internal/lsp/hover/hover.go index e6dcaa8e..587d902c 100644 --- a/internal/lsp/hover/hover.go +++ b/internal/lsp/hover/hover.go @@ -6,7 +6,6 @@ import ( "path/filepath" "strconv" "strings" - "sync" "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/types" @@ -15,11 +14,10 @@ import ( "github.com/styrainc/regal/internal/lsp/examples" "github.com/styrainc/regal/internal/lsp/rego" types2 "github.com/styrainc/regal/internal/lsp/types" + "github.com/styrainc/regal/internal/util/concurrent" ) -var builtinCache = make(map[*ast.Builtin]string) //nolint:gochecknoglobals - -var builtinCacheLock = &sync.Mutex{} //nolint:gochecknoglobals +var builtinCache = concurrent.MapOf(make(map[*ast.Builtin]string)) //nolint:gochecknoglobals func writeFunctionSnippet(sb *strings.Builder, builtin *ast.Builtin) { sb.WriteString("```rego\n") @@ -52,13 +50,9 @@ func writeFunctionSnippet(sb *strings.Builder, builtin *ast.Builtin) { } func CreateHoverContent(builtin *ast.Builtin) string { - builtinCacheLock.Lock() - if content, ok := builtinCache[builtin]; ok { - builtinCacheLock.Unlock() - + if content, ok := builtinCache.Get(builtin); ok { return content } - builtinCacheLock.Unlock() link := fmt.Sprintf( "https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-%s-%s", @@ -139,9 +133,7 @@ func CreateHoverContent(builtin *ast.Builtin) string { result := sb.String() - builtinCacheLock.Lock() - builtinCache[builtin] = result - builtinCacheLock.Unlock() + builtinCache.Set(builtin, result) return result } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index eaa56024..c4846695 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -44,6 +44,7 @@ import ( rparse "github.com/styrainc/regal/internal/parse" "github.com/styrainc/regal/internal/update" "github.com/styrainc/regal/internal/util" + "github.com/styrainc/regal/internal/util/concurrent" "github.com/styrainc/regal/internal/web" "github.com/styrainc/regal/pkg/config" "github.com/styrainc/regal/pkg/fixer" @@ -99,7 +100,7 @@ func NewLanguageServer(ctx context.Context, opts *LanguageServerOptions) *Langua configWatcher: lsconfig.NewWatcher(&lsconfig.WatcherOpts{LogFunc: ls.logf}), completionsManager: completions.NewDefaultManager(ctx, c, store), webServer: web.NewServer(c), - loadedBuiltins: make(map[string]map[string]*ast.Builtin), + loadedBuiltins: concurrent.MapOf(make(map[string]map[string]*ast.Builtin)), workspaceDiagnosticsPoll: opts.WorkspaceDiagnosticsPoll, } @@ -117,7 +118,7 @@ type LanguageServer struct { loadedConfig *config.Config loadedConfigEnabledNonAggregateRules []string loadedConfigEnabledAggregateRules []string - loadedBuiltins map[string]map[string]*ast.Builtin + loadedBuiltins *concurrent.Map[string, map[string]*ast.Builtin] clientInitializationOptions types.InitializationOptions @@ -137,8 +138,6 @@ type LanguageServer struct { workspaceRootURI string clientIdentifier clients.Identifier - loadedBuiltinsLock sync.RWMutex - // this is also used to lock the updates to the cache of enabled rules loadedConfigLock sync.Mutex @@ -574,9 +573,7 @@ func (l *LanguageServer) StartConfigWorker(ctx context.Context) { bis := rego.BuiltinsForCapabilities(caps) - l.loadedBuiltinsLock.Lock() - l.loadedBuiltins[capsURL] = bis - l.loadedBuiltinsLock.Unlock() + l.loadedBuiltins.Set(capsURL, bis) // the config may now ignore files that existed in the cache before, // in which case we need to remove them to stop their contents being @@ -2697,15 +2694,12 @@ func (l *LanguageServer) workspacePath() string { // in the server based on the currently loaded capabilities. If there is no // config, then the default for the Regal OPA version is used. func (l *LanguageServer) builtinsForCurrentCapabilities() map[string]*ast.Builtin { - l.loadedBuiltinsLock.RLock() - defer l.loadedBuiltinsLock.RUnlock() - cfg := l.getLoadedConfig() if cfg == nil { return rego.BuiltinsForCapabilities(ast.CapabilitiesForThisVersion()) } - bis, ok := l.loadedBuiltins[cfg.CapabilitiesURL] + bis, ok := l.loadedBuiltins.Get(cfg.CapabilitiesURL) if !ok { return rego.BuiltinsForCapabilities(ast.CapabilitiesForThisVersion()) } diff --git a/internal/util/concurrent/map.go b/internal/util/concurrent/map.go new file mode 100644 index 00000000..c4bcf396 --- /dev/null +++ b/internal/util/concurrent/map.go @@ -0,0 +1,128 @@ +package concurrent + +import ( + "maps" + "sync" +) + +type Map[K comparable, V any] struct { + m map[K]V + murw sync.RWMutex +} + +type ValueTransformer[V any] func(V) V + +func MapOf[K comparable, V any](src map[K]V) *Map[K, V] { + return &Map[K, V]{ + m: src, + murw: sync.RWMutex{}, + } +} + +func (cm *Map[K, V]) Get(k K) (V, bool) { + cm.murw.RLock() + + v, ok := cm.m[k] + + cm.murw.RUnlock() + + return v, ok +} + +func (cm *Map[K, V]) GetUnchecked(k K) V { + cm.murw.RLock() + + v := cm.m[k] + + cm.murw.RUnlock() + + return v +} + +func (cm *Map[K, V]) Set(k K, v V) { + cm.murw.Lock() + + cm.m[k] = v + + cm.murw.Unlock() +} + +func (cm *Map[K, V]) Delete(k K) { + cm.murw.Lock() + + delete(cm.m, k) + + cm.murw.Unlock() +} + +func (cm *Map[K, V]) Keys() []K { + cm.murw.RLock() + + keys := make([]K, 0, len(cm.m)) + + for k := range cm.m { + keys = append(keys, k) + } + + cm.murw.RUnlock() + + return keys +} + +func (cm *Map[K, V]) Values() []V { + cm.murw.RLock() + + vs := make([]V, len(cm.m)) + i := 0 + + for _, v := range cm.m { + vs[i] = v + i++ + } + + cm.murw.RUnlock() + + return vs +} + +func (cm *Map[K, V]) Len() int { + cm.murw.RLock() + + l := len(cm.m) + + cm.murw.RUnlock() + + return l +} + +func (cm *Map[K, V]) Clone() map[K]V { + cm.murw.RLock() + + m := maps.Clone(cm.m) + + cm.murw.RUnlock() + + return m +} + +func (cm *Map[K, V]) Clear() { + cm.murw.Lock() + + clear(cm.m) + + cm.murw.Unlock() +} + +func (cm *Map[K, V]) UpdateValue(key K, transformer ValueTransformer[V]) { + cm.murw.Lock() + + var v V + + if vo, ok := cm.m[key]; ok { + v = vo + } + + cm.m[key] = transformer(v) + + cm.murw.Unlock() +}