NexusTerm: an SSH session manager for Linux

Connection tree with interleaved folders and hosts, tabbed terminals
grouped by host, a pinned local shell, TOFU host key verification, and
credentials held by the OS keyring.

Terminal bytes travel on a per-session MessagePort with credit-based flow
control, so one session running `cat` on a large file cannot stall the
others. Transports reconnect on their own with jittered backoff, keeping
the scrollback and the tab while replacing only the connection beneath.

156 tests, including integration tests against a real sshd and a
migration exercised against a database built in the old shape.
This commit is contained in:
ylhan
2026-09-17 03:12:24 +08:00
commit 430d78b9f0
88 changed files with 19793 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest'
import { RECONNECT, reconnectDelay } from '../src/shared/session-protocol'
/** The nominal delay before jitter, for a given 1-based attempt. */
function nominal(attempt: number): number {
return Math.min(
RECONNECT.maxDelayMs,
RECONNECT.initialDelayMs * 2 ** Math.max(0, attempt - 1),
)
}
describe('reconnectDelay', () => {
it('stays within the jitter band on every attempt', () => {
for (let attempt = 1; attempt <= RECONNECT.maxAttempts + 5; attempt++) {
const delay = reconnectDelay(attempt)
const base = nominal(attempt)
expect(delay).toBeGreaterThanOrEqual(Math.round(base * (1 - RECONNECT.jitter)) - 1)
expect(delay).toBeLessThanOrEqual(Math.round(base * (1 + RECONNECT.jitter)) + 1)
}
})
it('starts short enough that a blip is barely noticed', () => {
// A first retry the user has to wait through feels like a broken app.
expect(reconnectDelay(1)).toBeLessThanOrEqual(1_500)
})
it('climbs, and does not exceed the ceiling', () => {
expect(nominal(2)).toBeGreaterThan(nominal(1))
for (let attempt = 1; attempt <= 30; attempt++) {
expect(reconnectDelay(attempt)).toBeLessThanOrEqual(
Math.round(RECONNECT.maxDelayMs * (1 + RECONNECT.jitter)) + 1,
)
}
})
it('is never zero or negative', () => {
// A zero delay would turn the retry loop into a busy loop against a server
// that is already having a bad day.
for (let attempt = 1; attempt <= 30; attempt++) {
expect(reconnectDelay(attempt)).toBeGreaterThan(0)
}
})
it('varies between calls, so sessions that drop together do not retry in lockstep', () => {
// This is the whole reason for the jitter: a network failure takes every
// session down at once, and identical delays would have them all hit the
// server on the same tick.
const samples = new Set(Array.from({ length: 40 }, () => reconnectDelay(4)))
expect(samples.size).toBeGreaterThan(1)
})
it('handles a non-positive attempt without producing nonsense', () => {
expect(reconnectDelay(0)).toBeGreaterThan(0)
expect(reconnectDelay(-3)).toBeGreaterThan(0)
})
})
describe('RECONNECT limits', () => {
it('gives up eventually rather than retrying forever', () => {
expect(RECONNECT.maxAttempts).toBeGreaterThan(0)
expect(RECONNECT.maxAttempts).toBeLessThanOrEqual(50)
})
it('spans roughly five minutes of trying', () => {
// Long enough to ride out a reboot or a flaky link, short enough that a
// session pointed at a decommissioned host stops rather than living
// forever in a retry loop.
let total = 0
for (let attempt = 1; attempt <= RECONNECT.maxAttempts; attempt++) {
total += nominal(attempt)
}
expect(total).toBeGreaterThan(60_000)
expect(total).toBeLessThan(15 * 60_000)
})
})