Skip to content
Source
Sign inSign up

OpenCodeServerOwner.ts

188 lines5.9 KB

1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Exit from "effect/Exit";
4import * as Fiber from "effect/Fiber";
5import * as Layer from "effect/Layer";
6import * as Scope from "effect/Scope";
7import * as Semaphore from "effect/Semaphore";
8​
9import * as OpenCodeRuntime from "./OpenCodeRuntime.ts";
10​
11const OPENCODE_SERVER_IDLE_TTL = "30 seconds";
12​
13interface OpenCodeServerOwnerState {
14 server: OpenCodeRuntime.OpenCodeServerProcess | null;
15 serverScope: Scope.Closeable | null;
16 borrowers: number;
17 idleCloseFiber: Fiber.Fiber<void, never> | null;
19​
20export class OpenCodeServerOwner extends Context.Service<
21 OpenCodeServerOwner,
22 {
23 readonly withServer: <A, E, R>(
24 use: (server: OpenCodeRuntime.OpenCodeServerProcess) => Effect.Effect<A, E, R>,
25 ) => Effect.Effect<A, E | OpenCodeRuntime.OpenCodeRuntimeError, R>;
26 }
27>()("@t3tools/provider-opencode/server/OpenCodeServerOwner") {}
28​
29export const make = Effect.fn("OpenCodeServerOwner.make")(function* (input: {
30 readonly binaryPath: string;
31 readonly directory: string;
32 readonly serverPassword?: string;
33 readonly environment?: NodeJS.ProcessEnv;
34 readonly verify?: (url: string) => Effect.Effect<string, OpenCodeRuntime.OpenCodeRuntimeError>;
35}) {
36 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
37 const ownerScope = yield* Effect.acquireRelease(Scope.make(), (scope) =>
38 Scope.close(scope, Exit.void),
39 );
40 const mutex = yield* Semaphore.make(1);
41 const state: OpenCodeServerOwnerState = {
42 server: null,
43 serverScope: null,
44 borrowers: 0,
45 idleCloseFiber: null,
46 };
47​
48 const cancelIdleClose = Effect.fn("OpenCodeServerOwner.cancelIdleClose")(function* () {
49 const fiber = state.idleCloseFiber;
50 state.idleCloseFiber = null;
51 if (fiber !== null) {
52 yield* Fiber.interrupt(fiber).pipe(Effect.ignore);
53 }
54 });
55​
56 const closeServer = Effect.fn("OpenCodeServerOwner.closeServer")(function* (
57 expected?: OpenCodeRuntime.OpenCodeServerProcess,
58 ) {
59 if (expected !== undefined && state.server !== expected) {
60 return;
61 }
62 const scope = state.serverScope;
63 state.server = null;
64 state.serverScope = null;
65 if (scope !== null) {
66 yield* Scope.close(scope, Exit.void).pipe(Effect.ignore);
67 }
68 });
69​
70 const watchServerExit = Effect.fn("OpenCodeServerOwner.watchServerExit")(function* (
71 server: OpenCodeRuntime.OpenCodeServerProcess,
72 ) {
73 yield* server.exitCode;
74 yield* mutex.withPermit(
75 Effect.gen(function* () {
76 if (state.server !== server) {
77 return;
78 }
79 yield* cancelIdleClose();
80 yield* closeServer(server);
81 }),
82 );
83 });
84​
85 const acquireServer = mutex.withPermit(
86 Effect.gen(function* () {
87 yield* cancelIdleClose();
88 if (state.server !== null) {
89 if (yield* state.server.isRunning) {
90 state.borrowers += 1;
91 return state.server;
92 }
93 yield* closeServer(state.server);
94 }
95​
96 return yield* Effect.uninterruptibleMask((restore) =>
97 Effect.gen(function* () {
98 const serverScope = yield* Scope.make();
99 const started = yield* Effect.exit(
100 restore(
101 runtime
102 .startOpenCodeServerProcess({
103 binaryPath: input.binaryPath,
104 directory: input.directory,
105 ...(input.serverPassword !== undefined
106 ? { serverPassword: input.serverPassword }
107 : {}),
108 ...(input.environment ? { environment: input.environment } : {}),
109 ...(input.verify ? { verify: input.verify } : {}),
110 })
111 .pipe(Effect.provideService(Scope.Scope, serverScope)),
112 ),
113 );
114 if (Exit.isFailure(started)) {
115 yield* Scope.close(serverScope, Exit.void).pipe(Effect.ignore);
116 return yield* Effect.failCause(started.cause);
117 }
118​
119 const server = started.value;
120 state.server = server;
121 state.serverScope = serverScope;
122 state.borrowers = 1;
123 yield* watchServerExit(server).pipe(Effect.forkIn(ownerScope));
124 return server;
125 }),
126 );
127 }),
128 );
129​
130 const releaseServer = (server: OpenCodeRuntime.OpenCodeServerProcess) =>
131 mutex.withPermit(
132 Effect.gen(function* () {
133 if (state.server !== server) {
134 return;
135 }
136 state.borrowers = Math.max(0, state.borrowers - 1);
137 if (state.borrowers > 0) {
138 return;
139 }
140 yield* cancelIdleClose();
141 state.idleCloseFiber = yield* Effect.sleep(OPENCODE_SERVER_IDLE_TTL).pipe(
142 Effect.andThen(
143 mutex.withPermit(
144 Effect.gen(function* () {
145 if (state.server !== server || state.borrowers > 0) {
146 return;
147 }
148 state.idleCloseFiber = null;
149 yield* closeServer(server);
150 }),
151 ),
152 ),
153 Effect.forkIn(ownerScope),
154 );
155 }),
156 );
157​
158 yield* Effect.addFinalizer(() =>
159 mutex.withPermit(
160 Effect.gen(function* () {
161 yield* cancelIdleClose();
162 state.borrowers = 0;
163 yield* closeServer();
164 }),
165 ),
166 );
167​
168 return OpenCodeServerOwner.of({
169 withServer: Effect.fn("OpenCodeServerOwner.withServer")(function (use) {
170 return Effect.uninterruptibleMask((restore) =>
171 restore(acquireServer).pipe(
172 Effect.flatMap((server) =>
173 restore(use(server)).pipe(Effect.ensuring(releaseServer(server))),
174 ),
175 ),
176 );
177 }),
178 });
179});
180​
181/** @public */
182export const layer = (input: {
183 readonly binaryPath: string;
184 readonly directory: string;
185 readonly serverPassword?: string;
186 readonly environment?: NodeJS.ProcessEnv;
187 readonly verify?: (url: string) => Effect.Effect<string, OpenCodeRuntime.OpenCodeRuntimeError>;
188}) => Layer.effect(OpenCodeServerOwner, make(input));