Use Effect fn for config service helpers

This commit is contained in:
elpresidank 2026-06-04 06:31:28 -05:00
parent 3890a598b5
commit 6ba4cf3b32
2 changed files with 193 additions and 177 deletions

View file

@ -1942,6 +1942,27 @@ Notes:
- `cd ts && bun run lint` - `cd ts && bun run lint`
- `git diff --check` - `git diff --check`
### 2026-06-04: Config Service Effect.fn Helper Slice
- Status: migrated and package-verified.
- Completed:
- `ts/packages/flow/src/config/service.ts` now defines its reusable
generator helpers with `Effect.fn` or `Effect.fnUntraced` instead of
arrow functions returning `Effect.gen`.
- `consumeOnceEffect` uses `Effect.fnUntraced`; persistence, push, read,
put/delete, close, and run helpers use named `Effect.fn` wrappers.
- Existing persistence read/write catch/logging behavior is preserved through
`Effect.fn` post-processing functions.
- The focused scan for config-service helper `=> Effect.gen` patterns is
clean.
- Verification:
- `cd ts/packages/flow && bunx --bun vitest run src/__tests__/config-service.test.ts`
- `cd ts && bun run check:tsgo`
- `cd ts && bun run build`
- `cd ts && bun run test`
- `cd ts && bun run lint`
- `git diff --check`
## Subagent Findings To Preserve ## Subagent Findings To Preserve
- MCP/workbench: - MCP/workbench:
@ -2067,9 +2088,9 @@ Notes:
- Gateway dispatcher static service registries, streaming membership, and - Gateway dispatcher static service registries, streaming membership, and
scoped requestor cache now use Effect `HashMap`/`HashSet`; gateway scoped requestor cache now use Effect `HashMap`/`HashSet`; gateway
term-bearing service membership sets now use Effect `HashSet` too. term-bearing service membership sets now use Effect `HashSet` too.
- FlowManager and KnowledgeCore `() => Effect.gen(...)` factories are - FlowManager, KnowledgeCore, and ConfigService `() => Effect.gen(...)`
normalized to `Effect.fn` / `Effect.fnUntraced`. Config and Librarian factories are normalized to `Effect.fn` / `Effect.fnUntraced`. Librarian
helper factories still need focused follow-up slices. helper factories still need a focused follow-up slice.
- ConfigService and KnowledgeCore operation dispatch now use `effect/Match` - ConfigService and KnowledgeCore operation dispatch now use `effect/Match`
with `Match.exhaustive`; FlowManager and Librarian operation dispatch now with `Match.exhaustive`; FlowManager and Librarian operation dispatch now
use `effect/Match` with runtime-preserving `Match.orElse` fallbacks. use `effect/Match` with runtime-preserving `Match.orElse` fallbacks.

View file

@ -313,11 +313,8 @@ const updateHandles = (
pushProducer: handles.pushProducer === undefined ? state.pushProducer : handles.pushProducer, pushProducer: handles.pushProducer === undefined ? state.pushProducer : handles.pushProducer,
})); }));
const persistStateEffect = ( const persistStateEffect = Effect.fn("ConfigService.persistState")(
persistPath: string | null, function* (persistPath: string | null, state: ConfigServiceState) {
state: ConfigServiceState,
): Effect.Effect<void> =>
Effect.gen(function* () {
if (persistPath === null) return; if (persistPath === null) return;
const payload = { const payload = {
version: state.version, version: state.version,
@ -332,16 +329,18 @@ const persistStateEffect = (
try: () => writeTextFile(persistPath, json), try: () => writeTextFile(persistPath, json),
catch: (cause) => configServiceError("persist-write", cause), catch: (cause) => configServiceError("persist-write", cause),
}); });
}).pipe( },
(effect) =>
effect.pipe(
Effect.catch((err) => Effect.catch((err) =>
Effect.logError("[ConfigService] Failed to persist config", {error: err.message}), Effect.logError("[ConfigService] Failed to persist config", {error: err.message}),
), ),
),
); );
const pushConfigWithStateEffect = ( const pushConfigWithStateEffect = Effect.fn("ConfigService.pushConfigWithState")(function* (
state: ConfigServiceState, state: ConfigServiceState,
): Effect.Effect<void, ConfigServiceError> => ) {
Effect.gen(function* () {
const pushProducer = state.pushProducer; const pushProducer = state.pushProducer;
if (pushProducer === null) return; if (pushProducer === null) return;
@ -357,10 +356,8 @@ const pushConfigWithStateEffect = (
yield* Effect.log(`[ConfigService] Pushed configuration version ${state.version}`); yield* Effect.log(`[ConfigService] Pushed configuration version ${state.version}`);
}); });
const readPersistedConfigEffect = ( const readPersistedConfigEffect = Effect.fn("ConfigService.readPersistedConfig")(
persistPath: string, function* (persistPath: string) {
): Effect.Effect<PersistedConfig | null> =>
Effect.gen(function* () {
const raw = yield* Effect.tryPromise({ const raw = yield* Effect.tryPromise({
try: () => readTextFile(persistPath), try: () => readTextFile(persistPath),
catch: (cause) => configServiceError("persist-read", cause), catch: (cause) => configServiceError("persist-read", cause),
@ -368,12 +365,15 @@ const readPersistedConfigEffect = (
return yield* S.decodeUnknownEffect(PersistedConfigJsonSchema)(raw).pipe( return yield* S.decodeUnknownEffect(PersistedConfigJsonSchema)(raw).pipe(
Effect.mapError((cause) => configServiceError("persist-decode", cause)), Effect.mapError((cause) => configServiceError("persist-decode", cause)),
); );
}).pipe( },
(effect) =>
effect.pipe(
Effect.catch(() => Effect.catch(() =>
Effect.log("[ConfigService] No persisted config found, starting fresh").pipe( Effect.log("[ConfigService] No persisted config found, starting fresh").pipe(
Effect.flatMap(() => Effect.succeed<PersistedConfig | null>(null)), Effect.flatMap(() => Effect.succeed<PersistedConfig | null>(null)),
) )
), ),
),
); );
const handleGetWithState = ( const handleGetWithState = (
@ -515,12 +515,11 @@ const applyDeleteStringKeys = (
}; };
}; };
const handlePutEffect = ( const handlePutEffect = Effect.fn("ConfigService.handlePut")(function* (
stateRef: SynchronizedRef.SynchronizedRef<ConfigServiceState>, stateRef: SynchronizedRef.SynchronizedRef<ConfigServiceState>,
persistPath: string | null, persistPath: string | null,
request: ConfigRequest, request: ConfigRequest,
): Effect.Effect<ConfigResponse, ConfigServiceError> => ) {
Effect.gen(function* () {
const values = configValues(request); const values = configValues(request);
if (values.length === 0) return yield* configServiceError("put", "Put requires config values"); if (values.length === 0) return yield* configServiceError("put", "Put requires config values");
@ -531,12 +530,11 @@ const handlePutEffect = (
return {version: next.version}; return {version: next.version};
}); });
const handleDeleteEffect = ( const handleDeleteEffect = Effect.fn("ConfigService.handleDelete")(function* (
stateRef: SynchronizedRef.SynchronizedRef<ConfigServiceState>, stateRef: SynchronizedRef.SynchronizedRef<ConfigServiceState>,
persistPath: string | null, persistPath: string | null,
request: ConfigRequest, request: ConfigRequest,
): Effect.Effect<ConfigResponse, ConfigServiceError> => ) {
Effect.gen(function* () {
const workspace = workspaceFor(request); const workspace = workspaceFor(request);
const keysByObject = objectKeys(request); const keysByObject = objectKeys(request);
@ -639,10 +637,9 @@ const handleConfigDumpWithState = (
config: configDumpForState(state, workspaceFor(request)), config: configDumpForState(state, workspaceFor(request)),
}); });
const closeConfigResourcesEffect = ( const closeConfigResourcesEffect = Effect.fn("ConfigService.closeResources")(function* (
stateRef: SynchronizedRef.SynchronizedRef<ConfigServiceState>, stateRef: SynchronizedRef.SynchronizedRef<ConfigServiceState>,
): Effect.Effect<void, ConfigServiceError> => ) {
Effect.gen(function* () {
const state = yield* SynchronizedRef.get(stateRef); const state = yield* SynchronizedRef.get(stateRef);
const consumer = state.consumer; const consumer = state.consumer;
@ -674,10 +671,9 @@ const closeConfigResourcesEffect = (
}); });
}); });
const consumeOnceEffect = ( const consumeOnceEffect = Effect.fnUntraced(function* (
service: ConfigService, service: ConfigService,
): Effect.Effect<void, ConfigServiceError> => ) {
Effect.gen(function* () {
const state = yield* SynchronizedRef.get(service.state); const state = yield* SynchronizedRef.get(service.state);
const consumer = state.consumer; const consumer = state.consumer;
if (consumer === null) { if (consumer === null) {
@ -697,10 +693,9 @@ const consumeOnceEffect = (
}); });
}); });
const runConfigServiceEffect = ( const runConfigServiceEffect = Effect.fn("ConfigService.run")(function* (
service: ConfigService, service: ConfigService,
): Effect.Effect<void, ConfigServiceError> => ) {
Effect.gen(function* () {
yield* service.loadFromDiskEffect; yield* service.loadFromDiskEffect;
const responseProducer = yield* Effect.tryPromise({ const responseProducer = yield* Effect.tryPromise({