diff --git a/apps/server/src/provider/Layers/ProviderHealth.ts b/apps/server/src/provider/Layers/ProviderHealth.ts index 41fc382a6ed6d5f1f3ed76b74e6552af7ce47daa..4eb6a288cb530238cad5b9237dae5348cd7f7b7d 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.ts @@ -13,7 +13,7 @@ import type { ServerProviderStatus, ServerProviderStatusState, } from "@t3tools/contracts"; -import { Effect, Layer, Option, Result, Stream } from "effect"; +import { Array, Effect, Fiber, Layer, Option, Result, Stream } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { @@ -312,56 +312,13 @@ export const checkCodexProviderStatus: Effect.Effect< export const ProviderHealthLive = Layer.effect( ProviderHealth, Effect.gen(function* () { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - let cachedStatuses: ReadonlyArray = [ - { - provider: CODEX_PROVIDER, - status: "warning", - available: false, - authStatus: "unknown", - checkedAt: new Date().toISOString(), - message: "Checking Codex CLI availability...", - }, - ]; - - let readyListeners: Array<(statuses: ReadonlyArray) => void> = []; - let resolved = false; - - const notifyReady = (statuses: ReadonlyArray) => { - resolved = true; - cachedStatuses = statuses; - for (const cb of readyListeners) cb(statuses); - readyListeners = []; - }; - - // Run health checks in the background so they don't block server startup. - checkCodexProviderStatus.pipe( - Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), - Effect.runPromise, - ).then((status) => { - notifyReady([status]); - }).catch(() => { - notifyReady([ - { - provider: CODEX_PROVIDER, - status: "error", - available: false, - authStatus: "unknown", - checkedAt: new Date().toISOString(), - message: "Failed to check Codex CLI status.", - }, - ]); - }); + const codexStatusFiber = yield* checkCodexProviderStatus.pipe( + Effect.map(Array.of), + Effect.forkScoped, + ); return { - getStatuses: Effect.sync(() => cachedStatuses), - onReady: (cb) => { - if (resolved) { - cb(cachedStatuses); - } else { - readyListeners.push(cb); - } - }, + getStatuses: Fiber.join(codexStatusFiber), } satisfies ProviderHealthShape; }), ); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 2f7ed0bc452fbfccb75ffc07169331d9e0797e9c..aee34cc8882d85a26a077ecdf7274a8e3db5959e 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -26,6 +26,7 @@ import { WebSocketRequest, WsPush, WsResponse, + ServerProviderStatus, } from "@t3tools/contracts"; import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import { @@ -268,9 +269,6 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< ), ); - // Read provider statuses lazily so background health checks are reflected. - const getProviderStatuses = () => Effect.runSync(providerHealth.getStatuses); - const clients = yield* Ref.make(new Set()); const logger = createLogger("ws"); @@ -618,6 +616,23 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< const subscriptionsScope = yield* Scope.make("sequential"); yield* Effect.addFinalizer(() => Scope.close(subscriptionsScope, Exit.void)); + // Push updated provider statuses to connected clients once background health checks finish. + let providers: ReadonlyArray = []; + yield* providerHealth.getStatuses.pipe( + Effect.flatMap((statuses) => { + providers = statuses; + return broadcastPush({ + type: "push", + channel: WS_CHANNELS.serverConfigUpdated, + data: { + issues: [], + providers: statuses, + }, + }); + }), + Effect.forkIn(subscriptionsScope), + ); + yield* Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => broadcastPush({ type: "push", @@ -632,23 +647,11 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< channel: WS_CHANNELS.serverConfigUpdated, data: { issues: event.issues, - providers: getProviderStatuses(), + providers, }, }), ).pipe(Effect.forkIn(subscriptionsScope)); - // Push updated provider statuses to connected clients once background health checks finish. - providerHealth.onReady((statuses) => { - broadcastPush({ - type: "push", - channel: WS_CHANNELS.serverConfigUpdated, - data: { - issues: [], - providers: statuses, - }, - }).pipe(Effect.runPromise).catch(() => {}); - }); - yield* Scope.provide(orchestrationReactor.start, subscriptionsScope); let welcomeBootstrapProjectId: ProjectId | undefined; @@ -896,7 +899,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< keybindingsConfigPath, keybindings: keybindingsConfig.keybindings, issues: keybindingsConfig.issues, - providers: getProviderStatuses(), + providers, availableEditors, };