185 lines · 5.7 KB · TypeScript
RawDownload
import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Scope from "effect/Scope";import * as Semaphore from "effect/Semaphore"; import * as OpenCodeRuntime from "./opencodeRuntime.ts"; const OPENCODE_SERVER_IDLE_TTL = "30 seconds"; interface OpenCodeServerOwnerState {  server: OpenCodeRuntime.OpenCodeServerProcess | null;  serverScope: Scope.Closeable | null;  borrowers: number;  idleCloseFiber: Fiber.Fiber<void, never> | null;} export class OpenCodeServerOwner extends Context.Service<  OpenCodeServerOwner,  {    readonly withServer: <A, E, R>(      use: (server: OpenCodeRuntime.OpenCodeServerProcess) => Effect.Effect<A, E, R>,    ) => Effect.Effect<A, E | OpenCodeRuntime.OpenCodeRuntimeError, R>;  }>()("t3/provider/OpenCodeServerOwner") {} /** Owns the lazy local OpenCode server shared by one provider instance. */export const make = Effect.fn("OpenCodeServerOwner.make")(function* (input: {  readonly binaryPath: string;  readonly directory: string;  readonly serverPassword?: string;  readonly environment?: NodeJS.ProcessEnv;}) {  const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;  const ownerScope = yield* Effect.acquireRelease(Scope.make(), (scope) =>    Scope.close(scope, Exit.void),  );  const mutex = yield* Semaphore.make(1);  const state: OpenCodeServerOwnerState = {    server: null,    serverScope: null,    borrowers: 0,    idleCloseFiber: null,  };   const cancelIdleClose = Effect.fn("OpenCodeServerOwner.cancelIdleClose")(function* () {    const fiber = state.idleCloseFiber;    state.idleCloseFiber = null;    if (fiber !== null) {      yield* Fiber.interrupt(fiber).pipe(Effect.ignore);    }  });   const closeServer = Effect.fn("OpenCodeServerOwner.closeServer")(function* (    expected?: OpenCodeRuntime.OpenCodeServerProcess,  ) {    if (expected !== undefined && state.server !== expected) {      return;    }    const scope = state.serverScope;    state.server = null;    state.serverScope = null;    if (scope !== null) {      yield* Scope.close(scope, Exit.void).pipe(Effect.ignore);    }  });   const watchServerExit = Effect.fn("OpenCodeServerOwner.watchServerExit")(function* (    server: OpenCodeRuntime.OpenCodeServerProcess,  ) {    yield* server.exitCode;    yield* mutex.withPermit(      Effect.gen(function* () {        if (state.server !== server) {          return;        }        yield* cancelIdleClose();        yield* closeServer(server);      }),    );  });   const acquireServer = mutex.withPermit(    Effect.gen(function* () {      yield* cancelIdleClose();      if (state.server !== null) {        if (yield* state.server.isRunning) {          state.borrowers += 1;          return state.server;        }        yield* closeServer(state.server);      }       return yield* Effect.uninterruptibleMask((restore) =>        Effect.gen(function* () {          const serverScope = yield* Scope.make();          const started = yield* Effect.exit(            restore(              runtime                .startOpenCodeServerProcess({                  binaryPath: input.binaryPath,                  directory: input.directory,                  ...(input.serverPassword !== undefined                    ? { serverPassword: input.serverPassword }                    : {}),                  ...(input.environment ? { environment: input.environment } : {}),                })                .pipe(Effect.provideService(Scope.Scope, serverScope)),            ),          );          if (Exit.isFailure(started)) {            yield* Scope.close(serverScope, Exit.void).pipe(Effect.ignore);            return yield* Effect.failCause(started.cause);          }           const server = started.value;          state.server = server;          state.serverScope = serverScope;          state.borrowers = 1;          yield* watchServerExit(server).pipe(Effect.forkIn(ownerScope));          return server;        }),      );    }),  );   const releaseServer = (server: OpenCodeRuntime.OpenCodeServerProcess) =>    mutex.withPermit(      Effect.gen(function* () {        if (state.server !== server) {          return;        }        state.borrowers = Math.max(0, state.borrowers - 1);        if (state.borrowers > 0) {          return;        }        yield* cancelIdleClose();        state.idleCloseFiber = yield* Effect.sleep(OPENCODE_SERVER_IDLE_TTL).pipe(          Effect.andThen(            mutex.withPermit(              Effect.gen(function* () {                if (state.server !== server || state.borrowers > 0) {                  return;                }                state.idleCloseFiber = null;                yield* closeServer(server);              }),            ),          ),          Effect.forkIn(ownerScope),        );      }),    );   yield* Effect.addFinalizer(() =>    mutex.withPermit(      Effect.gen(function* () {        yield* cancelIdleClose();        state.borrowers = 0;        yield* closeServer();      }),    ),  );   return OpenCodeServerOwner.of({    withServer: (use) =>      Effect.uninterruptibleMask((restore) =>        restore(acquireServer).pipe(          Effect.flatMap((server) =>            restore(use(server)).pipe(Effect.ensuring(releaseServer(server))),          ),        ),      ),  });}); /** @public Service construction is part of the canonical Effect module API. */export const layer = (input: {  readonly binaryPath: string;  readonly directory: string;  readonly serverPassword?: string;  readonly environment?: NodeJS.ProcessEnv;}) => Layer.effect(OpenCodeServerOwner, make(input));