Skip to content

Commit 618e1cb

Browse files
committed
perf(hub): avoid discarded scrollback splice results
1 parent d0fcce5 commit 618e1cb

2 files changed

Lines changed: 52 additions & 1 deletion

File tree

packages/hub/src/node/__tests__/host-terminals.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,55 @@ describe('devframeTerminalHost stream lifecycle', () => {
179179
expect(session.buffer!.includes('line-0')).toBe(false)
180180
})
181181

182+
it('evicts one scrollback chunk without producing discarded splice results', async () => {
183+
const { host, sinks } = createTerminalHost()
184+
const buffer: string[] = []
185+
const splice = vi.spyOn(buffer, 'splice')
186+
const chunks = Array.from({ length: 1200 }, (_, i) => `line-${i}`)
187+
const stream = new ReadableStream<string>({
188+
start(controller) {
189+
for (const chunk of chunks)
190+
controller.enqueue(chunk)
191+
controller.close()
192+
},
193+
})
194+
const session: DevframeTerminalSession = { id: 't', title: 'T', status: 'running', stream, buffer }
195+
host.register(session)
196+
try {
197+
await waitUntil(() => expect(sinks.get('t')?.closed).toBe(true))
198+
expect(session.buffer).toBe(buffer)
199+
expect([...buffer]).toEqual(chunks.slice(-1000))
200+
expect(sinks.get('t')?.write.mock.calls).toEqual(chunks.map(chunk => [chunk]))
201+
expect(splice).not.toHaveBeenCalled()
202+
}
203+
finally {
204+
splice.mockRestore()
205+
host.remove(session)
206+
}
207+
})
208+
209+
it('trims an oversized supplied scrollback buffer on the next chunk', async () => {
210+
const { host, sinks } = createTerminalHost()
211+
const buffer = Array.from({ length: 1500 }, (_, i) => `line-${i}`)
212+
const expected = [...buffer, 'latest'].slice(-1000)
213+
const stream = new ReadableStream<string>({
214+
start(controller) {
215+
controller.enqueue('latest')
216+
controller.close()
217+
},
218+
})
219+
const session: DevframeTerminalSession = { id: 't', title: 'T', status: 'running', stream, buffer }
220+
host.register(session)
221+
try {
222+
await waitUntil(() => expect(sinks.get('t')?.closed).toBe(true))
223+
expect(session.buffer).toBe(buffer)
224+
expect(buffer).toEqual(expected)
225+
}
226+
finally {
227+
host.remove(session)
228+
}
229+
})
230+
182231
it('rejects restarting a terminated child-process session', async () => {
183232
const { host, sinks } = createTerminalHost()
184233
const session = await host.startChildProcess(

packages/hub/src/node/host-terminals.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ export class DevframeTerminalsHost implements DevframeTerminalsHostType {
159159
// Mirror to the legacy session.buffer used by `terminals:read`:
160160
// a bounded tail kept for the snapshot endpoint.
161161
sessionBuffer.push(result.value)
162-
if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT)
162+
if (sessionBuffer.length === TERMINAL_BUFFER_LIMIT + 1)
163+
sessionBuffer.shift()
164+
else if (sessionBuffer.length > TERMINAL_BUFFER_LIMIT)
163165
sessionBuffer.splice(0, sessionBuffer.length - TERMINAL_BUFFER_LIMIT)
164166
sink?.write(result.value)
165167
}

0 commit comments

Comments
 (0)