authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-07-27 15:18:55-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-27 21:18:55+02:00
log6e3b73884bf40eeb2859cb16f1200c8e2f9cf7c3
tree95283e1b2a8ebe78cc3548429fa7228ee38c4c4f
parent5ba6ef7f84a5586e3795264cae348404dd408cb5
signature Signed by PGP key B5690EEEBB952194

fix(server): bound editor discovery during config loading (#4291)


2 files changed, 30 insertions(+), 1 deletions(-)

apps/server/src/server.test.ts+18
......@@ -73,6 +73,7 @@ const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
7373
7474import * as ServerConfig from "./config.ts";
7575import { makeRoutesLayer } from "./server.ts";
76import { resolveAvailableEditorsForConfig } from "./ws.ts";
7677import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts";
7778import * as GitManager from "./git/GitManager.ts";
7879import * as Keybindings from "./keybindings.ts";
......@@ -3763,6 +3764,23 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
37633764 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
37643765 );
37653766
3767 it.effect("does not block server config when editor discovery never resolves", () =>
3768 Effect.gen(function* () {
3769 const discoveryInterrupted = yield* Deferred.make<void>();
3770 const responseFiber = yield* resolveAvailableEditorsForConfig(
3771 Effect.never.pipe(
3772 Effect.onInterrupt(() => Deferred.succeed(discoveryInterrupted, undefined)),
3773 ),
3774 ).pipe(Effect.forkChild);
3775
3776 yield* TestClock.adjust(Duration.seconds(5));
3777
3778 const availableEditors = yield* Fiber.join(responseFiber);
3779 yield* Deferred.await(discoveryInterrupted);
3780 assert.deepEqual(availableEditors, []);
3781 }),
3782 );
3783
37663784 it.effect(
37673785 "rejects websocket rpc handshake when a session token is only provided via query string",
37683786 () =>
apps/server/src/ws.ts+12-1
......@@ -123,6 +123,15 @@ import * as RelayClient from "@t3tools/shared/relayClient";
123123const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchCommandError);
124124
125125const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
126const EDITOR_DISCOVERY_TIMEOUT = Duration.seconds(5);
127
128export const resolveAvailableEditorsForConfig = <A, E, R>(
129 discovery: Effect.Effect<ReadonlyArray<A>, E, R>,
130) =>
131 discovery.pipe(
132 Effect.timeoutOption(EDITOR_DISCOVERY_TIMEOUT),
133 Effect.map(Option.getOrElse(() => [])),
134 );
126135
127136function unexpectedCompatibilityError(error: never): never {
128137 throw new Error(`Unhandled compatibility error: ${String(error)}`);
......@@ -1087,7 +1096,9 @@ const makeWsRpcLayer = (
10871096 keybindings: keybindingsConfig.keybindings,
10881097 issues: keybindingsConfig.issues,
10891098 providers,
1090 availableEditors: yield* externalLauncher.resolveAvailableEditors(),
1099 availableEditors: yield* resolveAvailableEditorsForConfig(
1100 externalLauncher.resolveAvailableEditors(),
1101 ),
10911102 observability: {
10921103 logsDirectoryPath: config.logsDir,
10931104 localTracingEnabled: true,