diff --git a/apps/server/src/terminal/Manager.test.ts b/apps/server/src/terminal/Manager.test.ts index d45b837c03a17195b13424a6fb1fa1cc59f73a9c..d8639c8015bc572b029dba1bd3d31b12fdae4e1f 100644 --- a/apps/server/src/terminal/Manager.test.ts +++ b/apps/server/src/terminal/Manager.test.ts @@ -26,6 +26,7 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; import * as Path from "effect/Path"; +import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Schedule from "effect/Schedule"; import * as Schema from "effect/Schema"; @@ -1400,6 +1401,50 @@ it.layer( }), ); + it.effect("coalesces live history writes while retaining the latest output on close", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const writes = yield* Queue.unbounded(); + const output = yield* Queue.unbounded(); + const trackedFileSystem = FileSystem.FileSystem.of({ + ...fs, + writeFileString: (filePath, contents, options) => + fs + .writeFileString(filePath, contents, options) + .pipe( + Effect.tap(() => (contents.length > 0 ? Queue.offer(writes, contents) : Effect.void)), + ), + }); + const { manager, ptyAdapter } = yield* createManager(5, { processKillGraceMs: 0 }).pipe( + Effect.provideService(FileSystem.FileSystem, trackedFileSystem), + ); + yield* manager.open(openInput()); + const unsubscribe = yield* manager.subscribe((event) => + event.type === "output" ? Queue.offer(output, event.data).pipe(Effect.asVoid) : Effect.void, + ); + yield* Effect.addFinalizer(() => Effect.sync(unsubscribe)); + const process = ptyAdapter.processes[0]!; + + process.emitData("first"); + yield* Queue.take(output); + yield* TestClock.adjust("200 millis"); + expect(yield* Queue.size(writes)).toBe(0); + + process.emitData(" second"); + yield* Queue.take(output); + yield* TestClock.adjust("50 millis"); + expect(yield* Queue.take(writes)).toBe("first second"); + + process.emitData(" third"); + yield* Queue.take(output); + const close = yield* manager.close({ threadId: "thread-1" }).pipe(Effect.forkScoped); + yield* Effect.yieldNow; + yield* TestClock.adjust("250 millis"); + yield* Fiber.join(close); + expect((yield* manager.open(openInput())).history).toBe("first second third"); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("caps persisted history to configured line limit", () => Effect.gen(function* () { const { manager, ptyAdapter } = yield* createManager(3); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index f72cffa7dbe7dd0954621e32ba517383ce641cb7..684a305d0e9def868640dde7c525527f5c072f0f 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -98,7 +98,7 @@ export { const DEFAULT_HISTORY_LINE_LIMIT = 5_000; const DEFAULT_HISTORY_BYTE_LIMIT = 8 * 1024 * 1024; const MAX_HISTORY_CHUNK_LENGTH = 16 * 1024; -const DEFAULT_PERSIST_DEBOUNCE_MS = 40; +const DEFAULT_PERSIST_DEBOUNCE_MS = 250; const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000; const MAX_SUBPROCESS_POLL_INTERVAL_MS = 60_000; const DEFAULT_PROCESS_KILL_GRACE_MS = 1_000;