Skip to content

Commit d2a87ed

Browse files
authored
Extract ScriptInfo and DocumentRegistry to common container (microsoft#1202)
1 parent 5fa386e commit d2a87ed

File tree

3 files changed

+204
-133
lines changed

3 files changed

+204
-133
lines changed

internal/api/api.go

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ type API struct {
2929
host APIHost
3030
options APIOptions
3131

32-
documentRegistry *project.DocumentRegistry
33-
scriptInfosMu sync.RWMutex
34-
scriptInfos map[tspath.Path]*project.ScriptInfo
32+
documentStore *project.DocumentStore
3533
configFileRegistry *project.ConfigFileRegistry
3634

3735
projects handleMap[project.Project]
@@ -47,16 +45,16 @@ var _ project.ProjectHost = (*API)(nil)
4745

4846
func NewAPI(host APIHost, options APIOptions) *API {
4947
api := &API{
50-
host: host,
51-
options: options,
52-
scriptInfos: make(map[tspath.Path]*project.ScriptInfo),
53-
projects: make(handleMap[project.Project]),
54-
files: make(handleMap[ast.SourceFile]),
55-
symbols: make(handleMap[ast.Symbol]),
56-
types: make(handleMap[checker.Type]),
48+
host: host,
49+
options: options,
50+
projects: make(handleMap[project.Project]),
51+
files: make(handleMap[ast.SourceFile]),
52+
symbols: make(handleMap[ast.Symbol]),
53+
types: make(handleMap[checker.Type]),
5754
}
58-
api.documentRegistry = &project.DocumentRegistry{
59-
Options: tspath.ComparePathsOptions{
55+
56+
api.documentStore = project.NewDocumentStore(project.DocumentStoreOptions{
57+
ComparePathsOptions: tspath.ComparePathsOptions{
6058
UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(),
6159
CurrentDirectory: host.GetCurrentDirectory(),
6260
},
@@ -65,7 +63,8 @@ func NewAPI(host APIHost, options APIOptions) *API {
6563
_ = api.releaseHandle(string(FileHandle(file)))
6664
},
6765
},
68-
}
66+
})
67+
6968
api.configFileRegistry = &project.ConfigFileRegistry{
7069
Host: api,
7170
}
@@ -84,7 +83,7 @@ func (api *API) TypingsInstaller() *project.TypingsInstaller {
8483

8584
// DocumentRegistry implements ProjectHost.
8685
func (api *API) DocumentRegistry() *project.DocumentRegistry {
87-
return api.documentRegistry
86+
return api.documentStore.DocumentRegistry()
8887
}
8988

9089
// ConfigFileRegistry implements ProjectHost.
@@ -109,14 +108,12 @@ func (api *API) GetOrCreateScriptInfoForFile(fileName string, path tspath.Path,
109108

110109
// GetScriptInfoByPath implements ProjectHost.
111110
func (api *API) GetScriptInfoByPath(path tspath.Path) *project.ScriptInfo {
112-
api.scriptInfosMu.RLock()
113-
defer api.scriptInfosMu.RUnlock()
114-
return api.scriptInfos[path]
111+
return api.documentStore.GetScriptInfoByPath(path)
115112
}
116113

117114
// OnDiscoveredSymlink implements ProjectHost.
118115
func (api *API) OnDiscoveredSymlink(info *project.ScriptInfo) {
119-
// !!!
116+
api.documentStore.AddRealpathMapping(info)
120117
}
121118

122119
// Log implements ProjectHost.
@@ -376,23 +373,7 @@ func (api *API) releaseHandle(handle string) error {
376373
}
377374

378375
func (api *API) getOrCreateScriptInfo(fileName string, path tspath.Path, scriptKind core.ScriptKind) *project.ScriptInfo {
379-
api.scriptInfosMu.RLock()
380-
info, ok := api.scriptInfos[path]
381-
api.scriptInfosMu.RUnlock()
382-
if ok {
383-
return info
384-
}
385-
386-
content, ok := api.host.FS().ReadFile(fileName)
387-
if !ok {
388-
return nil
389-
}
390-
info = project.NewScriptInfo(fileName, path, scriptKind, api.host.FS())
391-
info.SetTextFromDisk(content)
392-
api.scriptInfosMu.Lock()
393-
defer api.scriptInfosMu.Unlock()
394-
api.scriptInfos[path] = info
395-
return info
376+
return api.documentStore.GetOrCreateScriptInfo(fileName, path, scriptKind, api.host.FS())
396377
}
397378

398379
func (api *API) toAbsoluteFileName(fileName string) string {

internal/project/documentstore.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package project
2+
3+
import (
4+
"sync"
5+
6+
"github.com/microsoft/typescript-go/internal/core"
7+
"github.com/microsoft/typescript-go/internal/tspath"
8+
"github.com/microsoft/typescript-go/internal/vfs"
9+
)
10+
11+
// DocumentStore manages ScriptInfo instances and the DocumentRegistry
12+
// with thread-safe operations.
13+
type DocumentStore struct {
14+
documentRegistry *DocumentRegistry
15+
16+
scriptInfosMu sync.RWMutex
17+
scriptInfos map[tspath.Path]*ScriptInfo
18+
19+
// Contains all the deleted script info's version information so that
20+
// it does not reset when creating script info again
21+
filenameToScriptInfoVersion map[tspath.Path]int
22+
23+
realpathToScriptInfosMu sync.Mutex
24+
realpathToScriptInfos map[tspath.Path]map[*ScriptInfo]struct{}
25+
}
26+
27+
// DocumentStoreOptions contains options for creating a DocumentStore
28+
type DocumentStoreOptions struct {
29+
ComparePathsOptions tspath.ComparePathsOptions
30+
ParsedFileCache ParsedFileCache
31+
Hooks DocumentRegistryHooks
32+
}
33+
34+
// NewDocumentStore creates a new DocumentStore with the given options
35+
func NewDocumentStore(options DocumentStoreOptions) *DocumentStore {
36+
return &DocumentStore{
37+
documentRegistry: &DocumentRegistry{
38+
Options: options.ComparePathsOptions,
39+
parsedFileCache: options.ParsedFileCache,
40+
Hooks: options.Hooks,
41+
},
42+
scriptInfos: make(map[tspath.Path]*ScriptInfo),
43+
filenameToScriptInfoVersion: make(map[tspath.Path]int),
44+
realpathToScriptInfos: make(map[tspath.Path]map[*ScriptInfo]struct{}),
45+
}
46+
}
47+
48+
// DocumentRegistry returns the document registry
49+
func (ds *DocumentStore) DocumentRegistry() *DocumentRegistry {
50+
return ds.documentRegistry
51+
}
52+
53+
// GetScriptInfoByPath returns the ScriptInfo for the given path, or nil if not found
54+
func (ds *DocumentStore) GetScriptInfoByPath(path tspath.Path) *ScriptInfo {
55+
ds.scriptInfosMu.RLock()
56+
defer ds.scriptInfosMu.RUnlock()
57+
if info, ok := ds.scriptInfos[path]; ok && !info.deferredDelete {
58+
return info
59+
}
60+
return nil
61+
}
62+
63+
// GetOrCreateScriptInfo creates or returns an existing ScriptInfo for the given file
64+
func (ds *DocumentStore) GetOrCreateScriptInfo(fileName string, path tspath.Path, scriptKind core.ScriptKind, fs vfs.FS) *ScriptInfo {
65+
return ds.getOrCreateScriptInfoWorker(fileName, path, scriptKind, false, "", true, fs)
66+
}
67+
68+
// GetOrCreateOpenScriptInfo creates or returns an existing ScriptInfo for an opened file
69+
func (ds *DocumentStore) GetOrCreateOpenScriptInfo(fileName string, path tspath.Path, fileContent string, scriptKind core.ScriptKind, fs vfs.FS) *ScriptInfo {
70+
return ds.getOrCreateScriptInfoWorker(fileName, path, scriptKind, true, fileContent, true, fs)
71+
}
72+
73+
// getOrCreateScriptInfoWorker is the internal implementation for creating/getting ScriptInfo
74+
func (ds *DocumentStore) getOrCreateScriptInfoWorker(fileName string, path tspath.Path, scriptKind core.ScriptKind, openedByClient bool, fileContent string, deferredDeleteOk bool, fs vfs.FS) *ScriptInfo {
75+
ds.scriptInfosMu.RLock()
76+
info, ok := ds.scriptInfos[path]
77+
ds.scriptInfosMu.RUnlock()
78+
79+
var fromDisk bool
80+
if !ok {
81+
if !openedByClient && !isDynamicFileName(fileName) {
82+
if content, ok := fs.ReadFile(fileName); !ok {
83+
return nil
84+
} else {
85+
fileContent = content
86+
fromDisk = true
87+
}
88+
}
89+
90+
info = NewScriptInfo(fileName, path, scriptKind, fs)
91+
if fromDisk {
92+
info.SetTextFromDisk(fileContent)
93+
}
94+
95+
ds.scriptInfosMu.Lock()
96+
defer ds.scriptInfosMu.Unlock()
97+
if prevVersion, ok := ds.filenameToScriptInfoVersion[path]; ok {
98+
info.version = prevVersion + 1
99+
delete(ds.filenameToScriptInfoVersion, path)
100+
}
101+
ds.scriptInfos[path] = info
102+
} else if info.deferredDelete {
103+
if !openedByClient && !fs.FileExists(fileName) {
104+
// If the file is not opened by client and the file does not exist on the disk, return
105+
return core.IfElse(deferredDeleteOk, info, nil)
106+
}
107+
info.deferredDelete = false
108+
}
109+
110+
if openedByClient {
111+
info.open(fileContent)
112+
}
113+
114+
return info
115+
}
116+
117+
// DeleteScriptInfo removes a ScriptInfo from the store
118+
func (ds *DocumentStore) DeleteScriptInfo(info *ScriptInfo) {
119+
ds.scriptInfosMu.Lock()
120+
defer ds.scriptInfosMu.Unlock()
121+
122+
ds.filenameToScriptInfoVersion[info.path] = info.version
123+
delete(ds.scriptInfos, info.path)
124+
125+
realpath := info.realpath
126+
if realpath != "" {
127+
ds.realpathToScriptInfosMu.Lock()
128+
defer ds.realpathToScriptInfosMu.Unlock()
129+
delete(ds.realpathToScriptInfos[realpath], info)
130+
}
131+
}
132+
133+
// AddRealpathMapping adds a realpath mapping for a ScriptInfo
134+
func (ds *DocumentStore) AddRealpathMapping(info *ScriptInfo) {
135+
ds.realpathToScriptInfosMu.Lock()
136+
defer ds.realpathToScriptInfosMu.Unlock()
137+
if scriptInfos, ok := ds.realpathToScriptInfos[info.realpath]; ok {
138+
scriptInfos[info] = struct{}{}
139+
} else {
140+
ds.realpathToScriptInfos[info.realpath] = map[*ScriptInfo]struct{}{
141+
info: {},
142+
}
143+
}
144+
}
145+
146+
// SourceFileCount returns the number of documents in the registry
147+
func (ds *DocumentStore) SourceFileCount() int {
148+
return ds.documentRegistry.size()
149+
}
150+
151+
func (ds *DocumentStore) ScriptInfoCount() int {
152+
return len(ds.scriptInfos)
153+
}
154+
155+
// ForEachScriptInfo calls the given function for each ScriptInfo in the store
156+
func (ds *DocumentStore) ForEachScriptInfo(fn func(info *ScriptInfo)) {
157+
ds.scriptInfosMu.RLock()
158+
defer ds.scriptInfosMu.RUnlock()
159+
for _, info := range ds.scriptInfos {
160+
if !info.deferredDelete {
161+
fn(info)
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)