Skip to content

Commit d94eff7

Browse files
mepukaclaude
andcommitted
refactor: migrate 50 services from Context.Tag to Effect.Service
Replace legacy Context.Tag pattern with Effect.Service across all services, reducing boilerplate by ~1400 lines. Updates .of() to .make() at 39 external call sites. Removes unnecessary catchAll on unfailable Scope.close in store-db. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d784546 commit d94eff7

61 files changed

Lines changed: 1883 additions & 3314 deletions

Some content is hidden

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

src/cli/app.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ export const app = Command.make("skygent", configOptions).pipe(
5656
CliLive.pipe(
5757
Layer.provide(
5858
Layer.mergeAll(
59-
Layer.succeed(ConfigOverrides, toConfigOverrides(config)),
60-
Layer.succeed(CredentialsOverrides, toCredentialsOverrides(config)),
61-
Layer.succeed(SyncSettingsOverrides, toSyncSettingsOverrides(config)),
59+
Layer.succeed(ConfigOverrides, ConfigOverrides.make(toConfigOverrides(config))),
60+
Layer.succeed(CredentialsOverrides, CredentialsOverrides.make(toCredentialsOverrides(config))),
61+
Layer.succeed(SyncSettingsOverrides, SyncSettingsOverrides.make(toSyncSettingsOverrides(config))),
6262
DerivationSettingsOverrides.layer
6363
)
6464
)

src/cli/input.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BunStream } from "@effect/platform-bun";
22
import { SystemError, type PlatformError } from "@effect/platform/Error";
3-
import { Context, Layer, Stream } from "effect";
3+
import { Effect, Stream } from "effect";
44
import { fstatSync } from "node:fs";
55
import { isatty } from "node:tty";
66

@@ -29,24 +29,20 @@ const makeLines = () => {
2929
);
3030
};
3131

32-
export class CliInput extends Context.Tag("@skygent/CliInput")<
33-
CliInput,
34-
CliInputService
35-
>() {
36-
static readonly layer = Layer.succeed(
37-
CliInput,
38-
CliInput.of({
39-
lines: makeLines(),
40-
isTTY: Boolean(process.stdin.isTTY || isatty(process.stdin.fd ?? 0)),
41-
isReadable: (() => {
42-
try {
43-
const fd = process.stdin.fd ?? 0;
44-
const stat = fstatSync(fd);
45-
return stat.isFIFO() || stat.isFile() || stat.isSocket();
46-
} catch {
47-
return false;
48-
}
49-
})()
50-
})
51-
);
32+
export class CliInput extends Effect.Service<CliInput>()("@skygent/CliInput", {
33+
succeed: {
34+
lines: makeLines(),
35+
isTTY: Boolean(process.stdin.isTTY || isatty(process.stdin.fd ?? 0)),
36+
isReadable: (() => {
37+
try {
38+
const fd = process.stdin.fd ?? 0;
39+
const stat = fstatSync(fd);
40+
return stat.isFIFO() || stat.isFile() || stat.isSocket();
41+
} catch {
42+
return false;
43+
}
44+
})()
45+
}
46+
}) {
47+
static readonly layer = CliInput.Default;
5248
}

src/cli/logging.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export const makeSyncReporter = (
123123
monitor: ResourceMonitorService,
124124
output: CliOutputService
125125
) =>
126-
SyncReporter.of({
126+
SyncReporter.make({
127127
report: (progress) =>
128128
Effect.gen(function* () {
129129
const format = yield* resolveLogFormat;

src/cli/output.ts

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BunSink } from "@effect/platform-bun";
22
import { SystemError, type PlatformError } from "@effect/platform/Error";
3-
import { Context, Effect, Layer, Sink, Stream } from "effect";
3+
import { Effect, Sink, Stream } from "effect";
44

55
const jsonLine = (value: unknown, pretty?: boolean) =>
66
JSON.stringify(value, null, pretty ? 2 : 0);
@@ -23,51 +23,47 @@ export interface CliOutputService {
2323
readonly writeStderr: (value: string) => Effect.Effect<void, PlatformError>;
2424
}
2525

26-
export class CliOutput extends Context.Tag("@skygent/CliOutput")<
27-
CliOutput,
28-
CliOutputService
29-
>() {
30-
static readonly layer = Layer.succeed(
31-
CliOutput,
32-
(() => {
33-
const stdout = BunSink.fromWritable(
34-
() => process.stdout,
35-
(cause) =>
36-
new SystemError({
37-
module: "Stream",
38-
method: "stdout",
39-
reason: "Unknown",
40-
cause
41-
}),
42-
{ endOnDone: false }
43-
);
44-
const stderr = BunSink.fromWritable(
45-
() => process.stderr,
46-
(cause) =>
47-
new SystemError({
48-
module: "Stream",
49-
method: "stderr",
50-
reason: "Unknown",
51-
cause
52-
}),
53-
{ endOnDone: false }
54-
);
26+
export class CliOutput extends Effect.Service<CliOutput>()("@skygent/CliOutput", {
27+
succeed: (() => {
28+
const stdout = BunSink.fromWritable(
29+
() => process.stdout,
30+
(cause) =>
31+
new SystemError({
32+
module: "Stream",
33+
method: "stdout",
34+
reason: "Unknown",
35+
cause
36+
}),
37+
{ endOnDone: false }
38+
);
39+
const stderr = BunSink.fromWritable(
40+
() => process.stderr,
41+
(cause) =>
42+
new SystemError({
43+
module: "Stream",
44+
method: "stderr",
45+
reason: "Unknown",
46+
cause
47+
}),
48+
{ endOnDone: false }
49+
);
5550

56-
return CliOutput.of({
57-
stdout,
58-
stderr,
59-
writeJson: (value, pretty) =>
60-
writeToSink(stdout, ensureNewline(jsonLine(value, pretty))),
61-
writeText: (value) => writeToSink(stdout, ensureNewline(value)),
62-
writeJsonStream: (stream) =>
63-
stream.pipe(
64-
Stream.map((value) => `${jsonLine(value)}\n`),
65-
Stream.run(stdout)
66-
),
67-
writeStderr: (value) => writeToSink(stderr, ensureNewline(value))
68-
});
69-
})()
70-
);
51+
return {
52+
stdout,
53+
stderr,
54+
writeJson: (value: unknown, pretty?: boolean) =>
55+
writeToSink(stdout, ensureNewline(jsonLine(value, pretty))),
56+
writeText: (value: string) => writeToSink(stdout, ensureNewline(value)),
57+
writeJsonStream: <A, E, R>(stream: Stream.Stream<A, E, R>) =>
58+
stream.pipe(
59+
Stream.map((value) => `${jsonLine(value)}\n`),
60+
Stream.run(stdout)
61+
),
62+
writeStderr: (value: string) => writeToSink(stderr, ensureNewline(value))
63+
};
64+
})()
65+
}) {
66+
static readonly layer = CliOutput.Default;
7167
}
7268

7369
export const writeJson = (value: unknown, pretty?: boolean) =>

src/services/app-config.ts

Lines changed: 54 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FileSystem } from "@effect/platform";
22
import { Path } from "@effect/platform";
3-
import { Config, Context, Effect, Layer, Option, Schema } from "effect";
3+
import { Config, Effect, Option, Schema } from "effect";
44
import { formatSchemaError, pickDefined } from "./shared.js";
55
import { AppConfig, OutputFormat } from "../domain/config.js";
66
import { ConfigError } from "../domain/errors.js";
@@ -81,17 +81,16 @@ type AppConfigOverrides = Partial<AppConfig>;
8181
* }).pipe(Effect.provide(overridesLayer));
8282
* ```
8383
*/
84-
export class ConfigOverrides extends Context.Tag("@skygent/ConfigOverrides")<
85-
ConfigOverrides,
86-
AppConfigOverrides
87-
>() {
84+
export class ConfigOverrides extends Effect.Service<ConfigOverrides>()("@skygent/ConfigOverrides", {
85+
succeed: {} as AppConfigOverrides
86+
}) {
8887
/**
8988
* Default empty configuration overrides layer.
9089
*
9190
* Use this as a base layer when no overrides are needed, or extend it
9291
* with custom overrides using Layer.succeed.
9392
*/
94-
static readonly layer = Layer.succeed(ConfigOverrides, {});
93+
static readonly layer = ConfigOverrides.Default;
9594
}
9695

9796
const PartialAppConfig = Schema.Struct({
@@ -197,10 +196,54 @@ const envOutputFormat = Config.literal("json", "ndjson", "markdown", "table")(
197196
* const runnable = program.pipe(Effect.provide(AppConfigService.layer));
198197
* ```
199198
*/
200-
export class AppConfigService extends Context.Tag("@skygent/AppConfig")<
201-
AppConfigService,
202-
AppConfig
203-
>() {
199+
export class AppConfigService extends Effect.Service<AppConfigService>()("@skygent/AppConfig", {
200+
effect: Effect.gen(function* () {
201+
const { _tag: _, ...overrides } = yield* ConfigOverrides;
202+
const path = yield* Path.Path;
203+
const defaultRoot = resolveDefaultRoot(path);
204+
const configPath = path.join(defaultRoot, configFileName);
205+
206+
const fileConfig = yield* loadFileConfig(configPath);
207+
208+
const envService = yield* Config.string("SKYGENT_SERVICE").pipe(Config.option);
209+
const envStoreRoot = yield* Config.string("SKYGENT_STORE_ROOT").pipe(Config.option);
210+
const envFormat = yield* envOutputFormat.pipe(Config.option);
211+
const envIdentifier = yield* Config.string("SKYGENT_IDENTIFIER").pipe(Config.option);
212+
213+
const envConfig = pickDefined({
214+
service: Option.getOrUndefined(envService),
215+
storeRoot: Option.getOrUndefined(envStoreRoot),
216+
outputFormat: Option.getOrUndefined(envFormat),
217+
identifier: Option.getOrUndefined(envIdentifier)
218+
});
219+
220+
const merged = {
221+
service: defaultService,
222+
storeRoot: defaultRoot,
223+
outputFormat: defaultOutputFormat,
224+
...fileConfig,
225+
...envConfig,
226+
...pickDefined(overrides as Record<string, unknown>)
227+
};
228+
229+
const resolvedStoreRoot = merged.storeRoot ?? defaultRoot;
230+
const normalized = {
231+
...merged,
232+
storeRoot: normalizeStoreRoot(path, resolvedStoreRoot)
233+
};
234+
235+
const decoded = yield* Schema.decodeUnknown(AppConfig)(normalized).pipe(
236+
Effect.mapError((error) =>
237+
ConfigError.make({
238+
message: `Invalid config: ${formatSchemaError(error)}`,
239+
path: configPath,
240+
cause: error
241+
})
242+
)
243+
);
244+
return decoded;
245+
})
246+
}) {
204247
/**
205248
* Layer that constructs the AppConfigService by resolving configuration
206249
* from all sources in priority order.
@@ -226,53 +269,5 @@ export class AppConfigService extends Context.Tag("@skygent/AppConfig")<
226269
* );
227270
* ```
228271
*/
229-
static readonly layer = Layer.effect(
230-
AppConfigService,
231-
Effect.gen(function* () {
232-
const overrides = yield* ConfigOverrides;
233-
const path = yield* Path.Path;
234-
const defaultRoot = resolveDefaultRoot(path);
235-
const configPath = path.join(defaultRoot, configFileName);
236-
237-
const fileConfig = yield* loadFileConfig(configPath);
238-
239-
const envService = yield* Config.string("SKYGENT_SERVICE").pipe(Config.option);
240-
const envStoreRoot = yield* Config.string("SKYGENT_STORE_ROOT").pipe(Config.option);
241-
const envFormat = yield* envOutputFormat.pipe(Config.option);
242-
const envIdentifier = yield* Config.string("SKYGENT_IDENTIFIER").pipe(Config.option);
243-
244-
const envConfig = pickDefined({
245-
service: Option.getOrUndefined(envService),
246-
storeRoot: Option.getOrUndefined(envStoreRoot),
247-
outputFormat: Option.getOrUndefined(envFormat),
248-
identifier: Option.getOrUndefined(envIdentifier)
249-
});
250-
251-
const merged = {
252-
service: defaultService,
253-
storeRoot: defaultRoot,
254-
outputFormat: defaultOutputFormat,
255-
...fileConfig,
256-
...envConfig,
257-
...pickDefined(overrides as Record<string, unknown>)
258-
};
259-
260-
const resolvedStoreRoot = merged.storeRoot ?? defaultRoot;
261-
const normalized = {
262-
...merged,
263-
storeRoot: normalizeStoreRoot(path, resolvedStoreRoot)
264-
};
265-
266-
const decoded = yield* Schema.decodeUnknown(AppConfig)(normalized).pipe(
267-
Effect.mapError((error) =>
268-
ConfigError.make({
269-
message: `Invalid config: ${formatSchemaError(error)}`,
270-
path: configPath,
271-
cause: error
272-
})
273-
)
274-
);
275-
return AppConfigService.of(decoded);
276-
})
277-
);
272+
static readonly layer = AppConfigService.Default;
278273
}

0 commit comments

Comments
 (0)