Skip to content

Commit 7aeb140

Browse files
refactor(Wind): Refine Effect services and remove local dependency submodules
This commit refines the newly adopted Effect-TS architecture in the `Wind` service layer and shifts the dependency strategy for the core Effect libraries. **Dependency Management:** Removes `Module/effect` and `Module/effect-website` Git submodules and the `.gitmodules` configuration. This indicates a shift to managing these libraries via standard package management (npm/pnpm) rather than local source pinning, simplifying the repository structure and build process. **Service Robustness:** - **Health:** Refactored service checks to consistently use `Effect.succeed` or `Effect.fail`, ensuring predictable error channels. Simplified `checkAllServices` logic to directly invoke the checker instance. - **IPC:** Added strict typing for `InvokeArgs` from Tauri. Improved error mapping with explicit `mapError` pipelines and fixed promise handling in `listen` to prevent hanging fibers. - **Error Handling:** Added `override name` getters to custom errors in `IPC`, `Mountain`, and `Telemetry` services for better stack trace clarity. **Type Safety & Hygiene:** - Renamed `SyncResult` to `MountainSyncResult` in `MountainSync` to prevent naming collisions. - Removed unused imports (e.g., `pipe` from `effect`) and stale exports (e.g., `ConfigurationWithSyncLive`) from the public barrel. - Updated `Bootstrap` to use native method-chaining `.pipe()` over the functional utility import. These changes stabilize the post-migration codebase, improve developer experience with better error reporting, and align dependency management with standard Node.js practices.
1 parent 6bfd8cd commit 7aeb140

57 files changed

Lines changed: 278 additions & 210 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitmodules

Lines changed: 0 additions & 9 deletions
This file was deleted.

Module/effect

Lines changed: 0 additions & 1 deletion
This file was deleted.

Module/effect-website

Lines changed: 0 additions & 1 deletion
This file was deleted.

Source/Effect/Bootstrap.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
* - Stage 6: Health checks (via Health service)
1515
*/
1616

17-
import { Effect, Layer, pipe, Context } from "effect";
17+
import { Effect, Layer, Context } from "effect";
1818
import { EnvironmentTag, type EnvironmentInfo } from "./Environment.js";
19-
import { Telemetry, withSpan } from "./Telemetry.js";
20-
import { Sandbox } from "./Sandbox.js";
21-
import { Configuration } from "./Configuration.js";
19+
import { Telemetry, TelemetryTag, withSpan } from "./Telemetry.js";
20+
import { Sandbox, type SandboxService } from "./Sandbox.js";
21+
import { Configuration, ConfigurationTag } from "./Configuration.js";
2222
import { MountainTag } from "./Mountain.js";
2323
import { HealthTag } from "./Health.js";
2424

@@ -49,7 +49,7 @@ export interface BootstrapResult {
4949
}
5050

5151
export interface BootstrapService {
52-
readonly run: (options?: BootstrapOptions) => Effect.Effect<BootstrapResult>;
52+
readonly run: (options?: BootstrapOptions) => Effect.Effect<BootstrapResult, never, typeof Sandbox | typeof TelemetryTag | typeof EnvironmentTag | typeof MountainTag | typeof HealthTag | typeof ConfigurationTag>;
5353
}
5454

5555
// ============================================================================
@@ -234,7 +234,7 @@ const stage6_HealthCheck = withSpan(
234234
// ============================================================================
235235

236236
const makeBootstrap = (): BootstrapService => ({
237-
run: (options): Effect.Effect<BootstrapResult, never, never> =>
237+
run: (options): Effect.Effect<BootstrapResult, never, typeof Sandbox | typeof TelemetryTag | typeof EnvironmentTag | typeof MountainTag | typeof HealthTag | typeof ConfigurationTag> =>
238238
Effect.gen(function* () {
239239
const telemetry = yield* Telemetry;
240240

@@ -344,10 +344,9 @@ export const BootstrapMock = Layer.effect(BootstrapTag, Effect.succeed(makeMockB
344344
// ============================================================================
345345

346346
export const runBootstrap = (options?: BootstrapOptions) =>
347-
pipe(
348-
Effect.gen(function* () {
349-
const bootstrap = yield* BootstrapTag;
350-
return yield* bootstrap.run(options);
351-
}),
347+
Effect.gen(function* () {
348+
const bootstrap = yield* BootstrapTag;
349+
return yield* bootstrap.run(options);
350+
}).pipe(
352351
Effect.provide(BootstrapLive),
353-
);
352+
);

Source/Effect/Configuration.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -300,14 +300,15 @@ export const makeMockConfiguration = (
300300
overrides?: Partial<ISandboxConfiguration>,
301301
): ConfigurationService => {
302302
const mockConfig: ISandboxConfiguration = {
303-
zoomLevel: 0,
304-
userEnv: {},
305-
workspace: {
306-
id: "mock-workspace",
307-
uri: "mock://workspace",
308-
},
309-
...overrides,
310-
};
303+
zoomLevel: 0,
304+
userEnv: {},
305+
workspace: {
306+
id: "mock-workspace",
307+
uri: "mock://workspace",
308+
name: "Mock Workspace",
309+
},
310+
...overrides,
311+
};
311312

312313
return {
313314
get: Effect.succeed(mockConfig),

Source/Effect/Health.ts

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ export interface SystemHealth {
3939
}
4040

4141
export interface HealthService {
42-
readonly checkService: (serviceName: string) => Effect.Effect<ServiceHealth>;
43-
readonly checkAllServices: () => Effect.Effect<SystemHealth>;
44-
readonly getOverallStatus: () => Effect.Effect<HealthStatus>;
42+
readonly checkService: (serviceName: string) => Effect.Effect<ServiceHealth, never>;
43+
readonly checkAllServices: () => Effect.Effect<SystemHealth, never>;
44+
readonly getOverallStatus: () => Effect.Effect<HealthStatus, never>;
4545
readonly monitorService: (
4646
serviceName: string,
4747
intervalMs: number,
48-
) => Effect.Effect<void>;
48+
) => Effect.Effect<void, never>;
4949
}
5050

5151
// ============================================================================
@@ -80,13 +80,13 @@ const createServiceHealthWithNoResponseTime = (
8080
name: string,
8181
status: HealthStatus,
8282
message: string,
83-
details?: Readonly<Record<string, unknown>>,
84-
): Omit<ServiceHealth, "responseTime"> => ({
83+
): ServiceHealth => ({
8584
serviceName: name,
8685
status,
8786
message,
8887
lastChecked: Date.now(),
89-
details,
88+
responseTime: 0,
89+
details: undefined,
9090
});
9191

9292
const makeHealthChecker = (): HealthService => ({
@@ -101,22 +101,26 @@ const makeHealthChecker = (): HealthService => ({
101101
case "environment":
102102
// Environment is always available
103103
const envTime = Date.now() - startTime;
104-
return createServiceHealth(
105-
"Environment",
106-
"healthy",
107-
"Environment service available",
108-
envTime,
104+
return Effect.succeed(
105+
createServiceHealth(
106+
"Environment",
107+
"healthy",
108+
"Environment service available",
109+
envTime,
110+
),
109111
);
110112

111113
case "telemetry":
112114
// Check telemetry by logging a metric
113115
yield* telemetry.log("info", "[Health] Telemetry health check");
114116
const telemetryTime = Date.now() - startTime;
115-
return createServiceHealth(
116-
"Telemetry",
117-
"healthy",
118-
"Telemetry service available",
119-
telemetryTime,
117+
return Effect.succeed(
118+
createServiceHealth(
119+
"Telemetry",
120+
"healthy",
121+
"Telemetry service available",
122+
telemetryTime,
123+
),
120124
);
121125

122126
case "mountain": {
@@ -134,7 +138,7 @@ const makeHealthChecker = (): HealthService => ({
134138
);
135139
}).pipe(
136140
Effect.catchAll((error) =>
137-
Effect.sync(() =>
141+
Effect.succeed(
138142
createServiceHealth(
139143
"Mountain",
140144
"unhealthy",
@@ -148,7 +152,7 @@ const makeHealthChecker = (): HealthService => ({
148152

149153
case "ipc": {
150154
// Check IPC by invoking a lightweight command
151-
const ipc = yield* IPCTag;
155+
const _ipc = yield* IPCTag;
152156
const ipcTime = Date.now() - startTime;
153157
return yield* Effect.tryPromise({
154158
try: async () => {
@@ -160,19 +164,19 @@ const makeHealthChecker = (): HealthService => ({
160164
ipcTime,
161165
);
162166
},
163-
catch: (error) =>
167+
catch: () =>
164168
createServiceHealth(
165169
"IPC",
166170
"unhealthy",
167-
`IPC service error: ${String(error)}`,
171+
"IPC service error",
168172
Date.now() - startTime,
169173
),
170174
});
171175
}
172176

173177
case "configuration": {
174178
// Check Configuration service
175-
const config = yield* ConfigurationTag;
179+
const _config = yield* ConfigurationTag;
176180
const configTime = Date.now() - startTime;
177181
return yield* Effect.tryPromise({
178182
try: async () => {
@@ -184,18 +188,18 @@ const makeHealthChecker = (): HealthService => ({
184188
configTime,
185189
);
186190
},
187-
catch: (error) =>
191+
catch: () =>
188192
createServiceHealth(
189193
"Configuration",
190194
"unhealthy",
191-
`Configuration service error: ${String(error)}`,
195+
"Configuration service error",
192196
Date.now() - startTime,
193197
),
194198
});
195199
}
196200

197201
default:
198-
return Effect.sync(() =>
202+
return Effect.succeed(
199203
createServiceHealthWithNoResponseTime(
200204
serviceName,
201205
"unknown",
@@ -210,21 +214,13 @@ const makeHealthChecker = (): HealthService => ({
210214
const env = yield* EnvironmentTag;
211215
const envInfo = yield* env.getInfo;
212216
const services = ["environment", "telemetry", "mountain", "ipc", "configuration"] as const;
217+
const healthChecker = makeHealthChecker();
213218

214219
const serviceHealthChecks = services.map((service) =>
215-
Effect.all([
216-
Effect.succeed(service).pipe(Effect.flatMap((serviceName) =>
217-
// Need to pass the service to checkService
218-
Effect.succeed(serviceName).pipe(Effect.flatMap((s) =>
219-
// Recursively call checkAllServices to simulate the check
220-
Effect.succeed(createServiceHealth(s, "healthy", "Service available", 0)),
221-
)),
222-
)),
223-
]),
220+
healthChecker.checkService(service),
224221
);
225222

226-
const results = yield* Effect.all(serviceHealthChecks);
227-
const healthResults = results.flat();
223+
const healthResults = yield* Effect.all(serviceHealthChecks);
228224

229225
// Determine overall status
230226
const unhealthyCount = healthResults.filter((h) => h.status === "unhealthy").length;
@@ -258,7 +254,7 @@ const makeHealthChecker = (): HealthService => ({
258254

259255
monitorService: (serviceName: string, intervalMs: number) =>
260256
Effect.gen(function* () {
261-
// Periodic health check using Effect.schedule
257+
// Periodic health check using Effect.repeat
262258
yield* makeHealthChecker().checkService(serviceName).pipe(
263259
Effect.repeat(Schedule.spaced(`${intervalMs} millis`)),
264260
);
@@ -317,13 +313,7 @@ export const makeMockHealth = (overrides?: Partial<Record<string, HealthStatus>>
317313

318314
getOverallStatus: () => Effect.succeed("healthy" as const),
319315

320-
monitorService: (serviceName: string, intervalMs: number) =>
321-
Effect.gen(function* () {
322-
yield* Effect.repeat(
323-
Effect.succeed(serviceName),
324-
Schedule.spaced(`${intervalMs} millis`),
325-
);
326-
}),
316+
monitorService: () => Effect.void,
327317
});
328318

329319
export const HealthMock = Layer.effect(HealthTag, Effect.succeed(makeMockHealth()));

Source/Effect/IPC.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { emit, listen } from "@tauri-apps/api/event";
99
import { invoke as tauriInvoke } from "@tauri-apps/api/core";
10+
import type { InvokeArgs } from "@tauri-apps/api/core";
1011
import { Context, Effect, Layer, Stream } from "effect";
1112

1213
import { SandboxNotReadyError, type IPCMessage } from "../Types/Sandbox.js";
@@ -28,8 +29,9 @@ export class IPCInvokeError extends Error {
2829
this._cause = cause;
2930
Object.setPrototypeOf(this, IPCInvokeError.prototype);
3031
}
32+
override get name() { return "IPCInvokeError"; }
3133
get channel() { return this._channel; }
32-
get cause() { return this._cause; }
34+
override get cause() { return this._cause; }
3335
}
3436

3537
export class IPCSendError extends Error {
@@ -45,8 +47,9 @@ export class IPCSendError extends Error {
4547
this._cause = cause;
4648
Object.setPrototypeOf(this, IPCSendError.prototype);
4749
}
50+
override get name() { return "IPCSendError"; }
4851
get channel() { return this._channel; }
49-
get cause() { return this._cause; }
52+
override get cause() { return this._cause; }
5053
}
5154

5255
export class IPCSubscriptionError extends Error {
@@ -55,7 +58,7 @@ export class IPCSubscriptionError extends Error {
5558
readonly _cause: unknown;
5659
constructor(
5760
channel: string,
58-
cause: unknown,
61+
cause: unknown,
5962
) {
6063
super(
6164
`IPC subscription failed on channel '${channel}': ${String(cause)}`,
@@ -64,8 +67,9 @@ export class IPCSubscriptionError extends Error {
6467
this._cause = cause;
6568
Object.setPrototypeOf(this, IPCSubscriptionError.prototype);
6669
}
70+
override get name() { return "IPCSubscriptionError"; }
6771
get channel() { return this._channel; }
68-
get cause() { return this._cause; }
72+
override get cause() { return this._cause; }
6973
}
7074

7175
// ============================================================================
@@ -114,11 +118,11 @@ export const IPCTauriLive = Layer.effect(
114118
IPC,
115119
Effect.gen(function* () {
116120
// Verify Tauri is available
117-
const isTauri =
121+
const isTauriAvailable =
118122
typeof window !== "undefined" &&
119123
(window as any).__TAURI__ !== undefined;
120124

121-
if (!isTauri) {
125+
if (!isTauriAvailable) {
122126
return yield* Effect.die(new SandboxNotReadyError());
123127
}
124128

@@ -127,12 +131,19 @@ export const IPCTauriLive = Layer.effect(
127131
Effect.try({
128132
try: () => emit(channel, args.length === 1 ? args[0] : args),
129133
catch: (error) => new IPCSendError(channel, error),
130-
});
134+
}).pipe(
135+
Effect.mapError((error) => new IPCSendError(channel, error)),
136+
);
131137

132138
// Atom: invoke
133139
const invoke_ = (channel: string) => (args: ReadonlyArray<unknown>) =>
134140
Effect.tryPromise({
135-
try: () => tauriInvoke(channel, args.length === 1 ? args[0] : args),
141+
try: () => {
142+
const invokeArgs: InvokeArgs | undefined = args.length === 1
143+
? (args[0] as InvokeArgs)
144+
: (args as unknown as InvokeArgs);
145+
return tauriInvoke(channel, invokeArgs);
146+
},
136147
catch: (error) => new IPCInvokeError(channel, error),
137148
});
138149

@@ -164,18 +175,14 @@ export const IPCTauriLive = Layer.effect(
164175
channel: string,
165176
): Effect.Effect<IPCMessage, IPCSubscriptionError> =>
166177
Effect.async((resume) => {
167-
listen(
168-
channel,
169-
(event) => {
170-
resume(
171-
Effect.succeed({
172-
channel,
173-
args: [event.payload],
174-
}),
175-
);
176-
},
177-
{ once: true },
178-
).catch((error) => {
178+
listen(channel, (event) => {
179+
resume(
180+
Effect.succeed({
181+
channel,
182+
args: [event.payload],
183+
}),
184+
);
185+
}).catch((error) => {
179186
resume(
180187
Effect.fail(new IPCSubscriptionError(channel, error)),
181188
);

0 commit comments

Comments
 (0)