Skip to content
6 changes: 3 additions & 3 deletions tsc/internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func (s *StdioServer) Run(ctx context.Context) error {
fs = callbackFS
}

projectSession := project.NewSession(&project.SessionInit{
sessionInit := &project.SessionInit{
BackgroundCtx: ctx,
Logger: nil, // TODO: Add logging support
FS: fs,
Expand All @@ -95,9 +95,9 @@ func (s *StdioServer) Run(ctx context.Context) error {
RunExternalCode: s.options.RunExternalCode,
},
Spawner: s.options.ContentMapperSpawner,
})
}

session := NewSession(projectSession, &SessionOptions{
session := NewStandaloneSession(sessionInit, &SessionOptions{
UseBinaryResponses: !s.options.Async, // Only msgpack uses binary responses
})
defer session.Close()
Expand Down
208 changes: 145 additions & 63 deletions tsc/internal/api/session.go

Large diffs are not rendered by default.

50 changes: 45 additions & 5 deletions tsc/internal/api/session_apistate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,51 @@ import (
"testing"

"github.com/microsoft/TypeScript/tsc/internal/bundled"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto"
"github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
"gotest.tools/v3/assert"
)

func TestStandaloneSessionUsesSnapshotHostWithoutProjectSession(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}

const configFileName = "/home/projects/p/tsconfig.json"
init, _ := projecttestutil.GetSessionInitOptions(map[string]any{
configFileName: `{ "compilerOptions": { "strict": true } }`,
"/home/projects/p/src/index.ts": `export const x = 1;`,
}, nil, &projecttestutil.TypingsInstallerOptions{})
session := NewStandaloneSession(init, nil)
defer session.Close()

firstResponse, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: "/home/projects/p/src/index.ts"}},
})
assert.NilError(t, err)
assert.Equal(t, firstResponse.Snapshot, SnapshotID(1))
assert.Equal(t, len(firstResponse.Projects), 1)

response, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenProjects: []DocumentIdentifier{{FileName: configFileName}},
})
assert.NilError(t, err)
assert.Equal(t, response.Snapshot, SnapshotID(2))

programResponse, err := session.handleCreateProgram(context.Background(), &CreateProgramParams{
RootFiles: []DocumentIdentifier{{FileName: "/home/projects/p/src/index.ts"}},
CreateProgramOptions: CreateProgramOptions{
CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue},
},
})
assert.NilError(t, err)
assert.Assert(t, programResponse.Project != nil)
assert.Equal(t, programResponse.Snapshot, SnapshotID(4))
}

// TestSessionTracksAndReleasesAPIRefs verifies that an API session holds at most
// one ref per opened project/file (opens are idempotent) and releases exactly
// those refs when the session is closed, so it never leaks or over-releases refs
Expand All @@ -30,7 +69,8 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
assert.Assert(t, session.compatibilitySnapshot == nil)

_, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenProjects: []DocumentIdentifier{{FileName: configFileName}},
Expand Down Expand Up @@ -63,7 +103,7 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

_, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
Expand Down Expand Up @@ -98,7 +138,7 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)

_, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
Expand Down Expand Up @@ -151,7 +191,7 @@ func TestSessionTracksAndReleasesAPIRefs(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

// Open via a relative path; it should be tracked under the absolute path
Expand Down Expand Up @@ -230,7 +270,7 @@ func TestUpdateSnapshotResponseSkipsUnloadedAncestorProject(t *testing.T) {
assert.Assert(t, ancestorProject != nil)
assert.Assert(t, ancestorProject.CommandLine == nil)

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

response, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{
Expand Down
6 changes: 3 additions & 3 deletions tsc/internal/api/session_completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
Expand Down Expand Up @@ -110,7 +110,7 @@ func TestCompletionOnInferredProject(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
Expand Down Expand Up @@ -156,7 +156,7 @@ func TestCompletionRetriesWithAutoImports(t *testing.T) {
IncludeCompletionsForImportStatements: core.TSTrue,
})

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
Expand Down
33 changes: 26 additions & 7 deletions tsc/internal/api/session_createprogram_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestCreateProgram(t *testing.T) {
})
defer projectSession.Close()

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()
ctx := context.Background()

Expand All @@ -46,6 +46,7 @@ func TestCreateProgram(t *testing.T) {
})
assert.NilError(t, err)
assert.Assert(t, response.Snapshot != baseResponse.Snapshot)
assert.Equal(t, response.Snapshot, SnapshotID(4))
assert.Equal(t, session.latestSnapshot, baseResponse.Snapshot)
assert.Assert(t, response.Project != nil)
assert.DeepEqual(t, response.Project.RootFiles, []string{fileName})
Expand Down Expand Up @@ -118,7 +119,7 @@ func TestCreateProgramWithNoRootFiles(t *testing.T) {
projectSession, _ := projecttestutil.Setup(map[string]any{})
defer projectSession.Close()

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

response, err := session.handleCreateProgram(context.Background(), &CreateProgramParams{
Expand All @@ -138,6 +139,24 @@ func TestCreateProgramWithNoRootFiles(t *testing.T) {
assert.Equal(t, len(project.Program.GetSourceFiles()), 0)
}

func TestCreateProgramFileChangesRequireOldProgram(t *testing.T) {
t.Parallel()

projectSession, _ := projecttestutil.Setup(map[string]any{})
defer projectSession.Close()

session := NewLSPSession(projectSession, nil)
defer session.Close()

_, err := session.handleCreateProgram(context.Background(), &CreateProgramParams{
CreateProgramOptions: CreateProgramOptions{
CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue},
},
FileChanges: &APIFileChanges{InvalidateAll: true},
})
assert.ErrorContains(t, err, "fileChanges requires an oldProgram")
}

func TestCreateProgramRemovesAllRootFiles(t *testing.T) {
t.Parallel()

Expand All @@ -147,7 +166,7 @@ func TestCreateProgramRemovesAllRootFiles(t *testing.T) {
})
defer projectSession.Close()

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()
ctx := context.Background()

Expand Down Expand Up @@ -196,7 +215,7 @@ func TestCreateProgramPreservesRootFileOrder(t *testing.T) {
})
defer projectSession.Close()

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()
ctx := context.Background()

Expand Down Expand Up @@ -236,7 +255,7 @@ func TestCreateProgramReusesProgram(t *testing.T) {
})
defer projectSession.Close()

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()
ctx := context.Background()

Expand Down Expand Up @@ -315,7 +334,7 @@ func TestCreateProgramProjectReferencesAndReuse(t *testing.T) {
})
defer projectSession.Close()

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()
ctx := context.Background()
libReference := &core.ProjectReference{Path: libConfigName, OriginalPath: libConfigName}
Expand Down Expand Up @@ -391,7 +410,7 @@ func TestCreateProgramFromConfiguredProgramDoesNotRetainOtherProjects(t *testing
})
defer projectSession.Close()

session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()
ctx := context.Background()

Expand Down
8 changes: 4 additions & 4 deletions tsc/internal/api/session_temporary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestUpdateTemporarySnapshot(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

ctx := context.Background()
Expand Down Expand Up @@ -114,7 +114,7 @@ func TestUpdateTemporarySnapshotAddsUnopenedFile(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

ctx := context.Background()
Expand Down Expand Up @@ -147,7 +147,7 @@ func TestUpdateTemporarySnapshotRejectsUnsupportedExtension(t *testing.T) {

projectSession, _ := projecttestutil.Setup(map[string]any{})
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

ctx := context.Background()
Expand Down Expand Up @@ -176,7 +176,7 @@ func TestUpdateTemporarySnapshotUsesClientSnapshotAsBase(t *testing.T) {
}
projectSession, _ := projecttestutil.Setup(files)
defer projectSession.Close()
session := NewSession(projectSession, nil)
session := NewLSPSession(projectSession, nil)
defer session.Close()

ctx := context.Background()
Expand Down
16 changes: 13 additions & 3 deletions tsc/internal/ipc/conn_async.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type AsyncConn struct {
pendingMu sync.Mutex
terminal error
writeMu sync.Mutex
handlers sync.WaitGroup
}

// NewAsyncConn creates a new async connection with the given transport and handler.
Expand Down Expand Up @@ -64,7 +65,12 @@ func (c *AsyncConn) SetCollectTiming(enabled bool) {
// Run starts processing messages on the connection.
// It blocks until the context is cancelled or an error occurs.
func (c *AsyncConn) Run(ctx context.Context) (err error) {
defer func() { c.closePendingCalls(err) }()
handlerCtx, cancelHandlers := context.WithCancel(ctx)
defer func() {
c.closePendingCalls(err)
cancelHandlers()
c.handlers.Wait()
Comment thread
andrewbranch marked this conversation as resolved.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think this is similar to #64142

}()
for {
if ctx.Err() != nil {
return ctx.Err()
Expand All @@ -81,9 +87,13 @@ func (c *AsyncConn) Run(ctx context.Context) (err error) {
if msg.IsResponse() {
c.handleResponse(msg)
} else if msg.IsRequest() {
go c.handleRequest(ctx, msg)
c.handlers.Go(func() {
c.handleRequest(handlerCtx, msg)
})
} else if msg.IsNotification() {
go c.handleNotification(ctx, msg)
c.handlers.Go(func() {
c.handleNotification(handlerCtx, msg)
})
}
}
}
Expand Down
Loading