Skip to content

Commit 4cc1488

Browse files
refactor(Cocoon/Service): Fully adopt Effect-TS patterns in service layer and type conversions
- Implement centralized `Dispatcher` service with Layer-based dependencies to handle RPC routing between `Mountain` and Cocoon's internal handlers - Convert `FileSystem` service methods to return structured `Effect` instances instead of raw promises, enabling proper error propagation (VSCodeFileSystemError) - Align `TypeConverter` method naming with project conventions (`fromAPI` → `FromAPI`) and standardize variable names for consistency - Streamline IPC server setup by consolidating `Dispatcher` module references and strengthening gRPC method type assertions - Update `WorkspaceEdit` converters to handle versioning through `IVersionInformationProvider` interface, ensuring compatibility with `Mountain`'s document state management This refactoring completes the transition of Cocoon's core services to pure Effect-TS patterns, critical for maintaining strict effect tracking across the VS Code API boundary. The changes directly support the 'Opening Files' and 'Saving Files' workflows by ensuring all filesystem operations are properly managed through Mountain's gRPC API.
1 parent 2746499 commit 4cc1488

File tree

9 files changed

+125
-88
lines changed

9 files changed

+125
-88
lines changed

Source/Service/Dispatcher.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @module Dispatcher (IPC)
3+
* @description Provides the Dispatcher service, which routes all incoming RPC
4+
* messages from the Mountain host to the appropriate handlers within Cocoon.
5+
*/
6+
7+
import { Layer } from "effect";
8+
9+
import { Live as LiveCancellation } from "./Cancellation.js";
10+
import { Definition } from "./IPC/Dispatcher/Definition.js";
11+
import { Tag, type Interface } from "./IPC/Dispatcher/Service.js";
12+
import { Live as LiveProtocolAdapter } from "./IPC/ProtocolAdapter.js";
13+
14+
export { Tag, type Interface };
15+
16+
/**
17+
* The live implementation Layer for the Dispatcher service.
18+
* It depends on the ProtocolAdapter (for the underlying transport) and the
19+
* Cancellation service (for handling cancellation signals).
20+
*/
21+
export const Live = Layer.effect(Tag, Definition).pipe(
22+
Layer.provide(Layer.merge(LiveProtocolAdapter, LiveCancellation)),
23+
);

Source/Service/FileSystem/CreateStatEffect.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@
44
*/
55

66
import { Effect } from "effect";
7-
import type { FileStat, FileType, Uri } from "vscode";
7+
import {
8+
FileSystemError as VscFileSystemError,
9+
type FileStat,
10+
type FileType,
11+
type Uri,
12+
} from "vscode";
813

914
import * as TypeConverter from "../../TypeConverter.js";
1015
import { IPC } from "../IPC.js";
1116
import { FileSystemError, MapToVSCodeError } from "./Error.js";
1217

13-
export function CreateStatEffect(URI: Uri) {
18+
export function CreateStatEffect(
19+
URI: Uri,
20+
): Effect.Effect<FileStat, VscFileSystemError, IPC.Interface> {
1421
return Effect.gen(function* (_) {
1522
const IPCService = yield* _(IPC.Tag);
16-
const UriDTO = TypeConverter.URIConverter.FromAPI(URI);
23+
const UriDTO = TypeConverter.URI.fromAPI(URI);
1724
const RawStat = yield* _(
1825
IPCService.SendRequest<any>("$stat", [UriDTO]),
1926
);

Source/Service/FileSystem/Definition.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,49 @@
44
*/
55

66
import { Effect } from "effect";
7+
import { FileSystemError as VscFileSystemError } from "vscode";
78

89
import { FileSystemInformation } from "../FileSystemInformation.js";
910
import { CreateStatEffect } from "./CreateStatEffect.js";
1011
import type { Interface } from "./Service.js";
1112

12-
// Other effect creators would be imported here
13-
// import { CreateReadFileEffect } from "./CreateReadFileEffect.js";
14-
// import { CreateWriteFileEffect } from "./CreateWriteFileEffect.js";
15-
// import { CreateDeleteEffect } from "./CreateDeleteEffect.js";
16-
// etc.
17-
1813
export const Definition = Effect.gen(function* (_) {
1914
const FsInfo = yield* _(FileSystemInformation.Tag);
2015

2116
const ServiceImplementation: Interface = {
22-
// Each method builds and runs the corresponding Effect.
23-
stat: (uri) => Effect.runPromise(CreateStatEffect(uri)),
17+
stat: (uri) => CreateStatEffect(uri),
2418
readDirectory: (uri) =>
25-
Promise.reject(new Error("readDirectory not implemented")),
19+
Effect.fail(
20+
new VscFileSystemError(
21+
`readDirectory not implemented for ${uri}`,
22+
),
23+
),
2624
createDirectory: (uri) =>
27-
Promise.reject(new Error("createDirectory not implemented")),
25+
Effect.fail(
26+
new VscFileSystemError(
27+
`createDirectory not implemented for ${uri}`,
28+
),
29+
),
2830
readFile: (uri) =>
29-
Promise.reject(new Error("readFile not implemented")),
31+
Effect.fail(
32+
new VscFileSystemError(`readFile not implemented for ${uri}`),
33+
),
3034
writeFile: (uri, content) =>
31-
Promise.reject(new Error("writeFile not implemented")),
35+
Effect.fail(
36+
new VscFileSystemError(`writeFile not implemented for ${uri}`),
37+
),
3238
delete: (uri, options) =>
33-
Promise.reject(new Error("delete not implemented")),
39+
Effect.fail(
40+
new VscFileSystemError(`delete not implemented for ${uri}`),
41+
),
3442
rename: (source, target, options) =>
35-
Promise.reject(new Error("rename not implemented")),
43+
Effect.fail(
44+
new VscFileSystemError(`rename not implemented for ${source}`),
45+
),
3646
copy: (source, target, options) =>
37-
Promise.reject(new Error("copy not implemented")),
47+
Effect.fail(
48+
new VscFileSystemError(`copy not implemented for ${source}`),
49+
),
3850
isWritableFileSystem: FsInfo.isWritableFileSystem,
3951
onDidChangeFile: FsInfo.onDidChangeFile,
4052
};

Source/Service/IPC/Client/Acquire.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@ import { Effect } from "effect";
1616

1717
import { Configuration as ConfigurationService } from "../Configuration.js";
1818
import { gRPCConnectionError } from "../Error.js";
19-
import { Release } from "./Release.js";
2019
import type { Interface as ClientService } from "./Service.js";
20+
import { Release } from "./Release.js";
2121

2222
/**
2323
* An `Effect` that loads the gRPC `.proto` file definition from disk.
24-
* @param ProtoPath The absolute path to the `vine.proto` file.
2524
*/
2625
function LoadProtoDefinition(
2726
ProtoPath: string,
@@ -44,8 +43,6 @@ function LoadProtoDefinition(
4443
/**
4544
* An `Effect` that creates an insecure gRPC client instance from a loaded
4645
* package definition.
47-
* @param PackageDefinition The loaded gRPC package definition.
48-
* @param ServerAddress The address of the `Mountain` gRPC server.
4946
*/
5047
function CreateClientInstance(
5148
PackageDefinition: GrpcObject,
@@ -72,9 +69,7 @@ function CreateClientInstance(
7269
}
7370

7471
/**
75-
* An `Effect` that waits for the gRPC client to establish a ready connection
76-
* with the server, with a 10-second timeout.
77-
* @param Client The gRPC client instance.
72+
* An `Effect` that waits for the gRPC client to establish a ready connection.
7873
*/
7974
function WaitForClientReady(
8075
Client: ClientService,

Source/Service/IPC/Server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { Layer } from "effect";
88

99
import type { Configuration } from "./Configuration.js";
10-
import { Dispatcher } from "./Dispatcher/Service.js";
10+
import { Live as LiveDispatcher } from "./Dispatcher.js";
1111
import type { gRPCConnectionError } from "./Error.js";
1212
import { Acquire } from "./Server/Acquire.js";
1313
import { Tag, type Interface as ServerService } from "./Server/Service.js";
@@ -19,4 +19,4 @@ export const Live: Layer.Layer<
1919
ServerService,
2020
gRPCConnectionError,
2121
Configuration | Dispatcher.Interface
22-
> = Layer.scoped(Tag, Acquire).pipe(Layer.provide(Dispatcher));
22+
> = Layer.scoped(Tag, Acquire).pipe(Layer.provide(LiveDispatcher));

Source/Service/IPC/Server/Acquire.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { Effect } from "effect";
1515

1616
import { Configuration as ConfigurationService } from "../Configuration.js";
17-
import { Dispatcher } from "../Dispatcher/Service.js";
17+
import { Dispatcher } from "../Dispatcher.js";
1818
import { gRPCConnectionError } from "../Error.js";
1919
import { CreateServiceImplementation } from "./CreateServiceImplementation.js";
2020
import { Release } from "./Release.js";

Source/Service/IPC/Server/CreateServiceImplementation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,16 @@ export function CreateServiceImplementation(
7676
callback: gRPC.sendUnaryData<Empty>,
7777
) => {
7878
const Notification = call.request;
79-
80-
const ProcessEffect = DecodeValue(Notification.getParams()).pipe(
79+
const ProcessEffect = DecodeValue(
80+
(Notification as any).getParams(),
81+
).pipe(
8182
Effect.flatMap((DecodedParameter) =>
8283
DispatcherService.DispatchNotification(
83-
Notification.getMethod(),
84+
(Notification as any).getMethod(),
8485
DecodedParameter,
8586
),
8687
),
8788
);
88-
8989
Effect.runFork(ProcessEffect);
9090
callback(null, new Empty());
9191
},

Source/TypeConverter/Command/Definition.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,45 +33,45 @@ export class Definition implements Interface {
3333
);
3434
}
3535

36-
private ExecuteDelegatedCommand(ID: string, ...Arguments: any[]): any {
37-
const command = this.DelegatedCommands.get(ID);
38-
if (!command) {
36+
private ExecuteDelegatedCommand(ID: string, ...ArgumentArray: any[]): any {
37+
const Command = this.DelegatedCommands.get(ID);
38+
if (!Command) {
3939
throw new Error(`Unknown delegated command: ${ID}`);
4040
}
4141
return this.CommandService.ExecuteCommand(
42-
command.command,
43-
...(command.arguments ?? []),
42+
Command.command,
43+
...(Command.arguments ?? []),
4444
);
4545
}
4646

4747
public ToInternal(
4848
Command: VSCode.Command,
49-
Disposables: IDisposable[],
49+
DisposableArray: IDisposable[],
5050
): ICommand {
5151
if (!Command) {
5252
return undefined as any;
5353
}
5454

5555
const APICommand = this.LookupAPICommand(Command.command);
5656
if (APICommand) {
57-
const ConvertedArguments =
58-
Command.arguments?.map((argument, i) =>
59-
APICommand.Arguments[i].Convert(argument),
57+
const ConvertedArgumentArray =
58+
Command.arguments?.map((Argument, i) =>
59+
APICommand.Arguments[i].Convert(Argument),
6060
) ?? [];
6161
return {
6262
id: APICommand.InternalID,
6363
title: APICommand.ID,
64-
arguments: ConvertedArguments,
64+
arguments: ConvertedArgumentArray,
6565
};
6666
}
6767

6868
if (
6969
Array.isArray(Command.arguments) &&
70-
Command.arguments.some((argument) => typeof argument === "function")
70+
Command.arguments.some((Argument) => typeof Argument === "function")
7171
) {
7272
const ID = generateUuid();
7373
this.DelegatedCommands.set(ID, Command);
74-
Disposables.push({
74+
DisposableArray.push({
7575
dispose: () => this.DelegatedCommands.delete(ID),
7676
});
7777
return {

Source/TypeConverter/WorkSpaceEdit.ts

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -34,74 +34,74 @@ export interface IVersionInformationProvider {
3434
}
3535

3636
export namespace WorkSpaceEdit {
37-
export function fromAPI(
37+
export function FromAPI(
3838
Edit: VSCode.WorkspaceEdit,
3939
VersionProvider?: IVersionInformationProvider,
4040
): IWorkspaceEdit {
41-
const result: IWorkspaceEdit = { edits: [] };
41+
const Result: IWorkspaceEdit = { edits: [] };
4242

43-
for (const [uri, edits] of Edit.entries()) {
44-
if (edits[0] instanceof ExtHostTypes.TextEdit) {
45-
const resource = URIConverter.fromAPI(uri);
46-
const versionId = VersionProvider?.GetTextDocumentVersion(uri);
47-
for (const edit of edits as VSCode.TextEdit[]) {
48-
result.edits.push({
49-
resource,
50-
textEdit: TextEditConverter.fromAPI(edit),
51-
versionId,
43+
for (const [URI, URIEditArray] of Edit.entries()) {
44+
if (URIEditArray[0] instanceof ExtHostTypes.TextEdit) {
45+
const Resource = URIConverter.FromAPI(URI);
46+
const VersionId = VersionProvider?.GetTextDocumentVersion(URI);
47+
for (const SingleEdit of URIEditArray as VSCode.TextEdit[]) {
48+
Result.edits.push({
49+
resource: Resource,
50+
textEdit: TextEditConverter.FromAPI(SingleEdit),
51+
versionId: VersionId,
5252
} as IWorkspaceTextEdit);
5353
}
5454
} else {
55-
for (const edit of edits as any[]) {
56-
result.edits.push({
57-
oldResource: edit.oldUri
58-
? URIConverter.fromAPI(edit.oldUri)
55+
for (const FileEdit of URIEditArray as any[]) {
56+
Result.edits.push({
57+
oldResource: FileEdit.oldUri
58+
? URIConverter.FromAPI(FileEdit.oldUri)
5959
: undefined,
60-
newResource: edit.newUri
61-
? URIConverter.fromAPI(edit.newUri)
60+
newResource: FileEdit.newUri
61+
? URIConverter.FromAPI(FileEdit.newUri)
6262
: undefined,
63-
options: edit.options,
64-
metadata: edit.metadata,
63+
options: FileEdit.options,
64+
metadata: FileEdit.metadata,
6565
} as IWorkspaceFileEdit);
6666
}
6767
}
6868
}
69-
return result;
69+
return Result;
7070
}
7171

7272
export function ToAPI(DTO: IWorkspaceEdit): VSCode.WorkspaceEdit {
73-
const result = new ExtHostTypes.WorkspaceEdit();
74-
for (const edit of DTO.edits) {
75-
if ("textEdit" in edit) {
76-
const workspaceTextEdit = edit as IWorkspaceTextEdit;
77-
const uri = URIConverter.ToAPI(
78-
workspaceTextEdit.resource as any,
73+
const Result = new ExtHostTypes.WorkspaceEdit();
74+
for (const Edit of DTO.edits) {
75+
if ("textEdit" in Edit) {
76+
const WorkspaceTextEdit = Edit as IWorkspaceTextEdit;
77+
const URI = URIConverter.ToAPI(
78+
WorkspaceTextEdit.resource as any,
7979
);
80-
const textEdits = [
81-
TextEditConverter.ToAPI(workspaceTextEdit.textEdit as any),
80+
const TextEditArray = [
81+
TextEditConverter.ToAPI(WorkspaceTextEdit.textEdit as any),
8282
];
83-
result.set(uri, textEdits);
83+
Result.set(URI, TextEditArray);
8484
} else {
85-
const fileEdit = edit as IWorkspaceFileEdit;
86-
if (fileEdit.oldResource && fileEdit.newResource) {
87-
result.renameFile(
88-
URIConverter.ToAPI(fileEdit.oldResource as any),
89-
URIConverter.ToAPI(fileEdit.newResource as any),
90-
fileEdit.options,
85+
const FileEdit = Edit as IWorkspaceFileEdit;
86+
if (FileEdit.oldResource && FileEdit.newResource) {
87+
Result.renameFile(
88+
URIConverter.ToAPI(FileEdit.oldResource as any),
89+
URIConverter.ToAPI(FileEdit.newResource as any),
90+
FileEdit.options,
9191
);
92-
} else if (fileEdit.newResource) {
93-
result.createFile(
94-
URIConverter.ToAPI(fileEdit.newResource as any),
95-
fileEdit.options,
92+
} else if (FileEdit.newResource) {
93+
Result.createFile(
94+
URIConverter.ToAPI(FileEdit.newResource as any),
95+
FileEdit.options,
9696
);
97-
} else if (fileEdit.oldResource) {
98-
result.deleteFile(
99-
URIConverter.ToAPI(fileEdit.oldResource as any),
100-
fileEdit.options,
97+
} else if (FileEdit.oldResource) {
98+
Result.deleteFile(
99+
URIConverter.ToAPI(FileEdit.oldResource as any),
100+
FileEdit.options,
101101
);
102102
}
103103
}
104104
}
105-
return result;
105+
return Result;
106106
}
107107
}

0 commit comments

Comments
 (0)