163 lines
6.7 KiB
JavaScript
163 lines
6.7 KiB
JavaScript
import { describe, expect, test } from 'bun:test';
|
|
import { createDownstreamScheduler, DOWNSTREAM_CHUNK_BYTES } from './downstream-scheduler.js';
|
|
import { encodeTunnelFrame, decodeTunnelFrame, TunnelFrameType } from './tunnel-codec.js';
|
|
|
|
const tick = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
const body = (streamId, value = 0, size = DOWNSTREAM_CHUNK_BYTES) =>
|
|
encodeTunnelFrame(TunnelFrameType.HttpBody, streamId, new Uint8Array(size).fill(value));
|
|
|
|
function fixture() {
|
|
const sent = [];
|
|
const errors = [];
|
|
let time = 0;
|
|
const scheduler = createDownstreamScheduler({
|
|
sendBatch: async frames => { sent.push(...frames); },
|
|
onError: error => errors.push(error.message),
|
|
now: () => time,
|
|
});
|
|
return { scheduler, sent, errors, advance: ms => { time += ms; } };
|
|
}
|
|
|
|
describe('relay downstream scheduling', () => {
|
|
test('stops without end-client ACKs even when the relay socket accepts everything', async () => {
|
|
const { scheduler, sent } = fixture();
|
|
const pending = Array.from({ length: 100 }, () => scheduler.send(body(1)));
|
|
await tick();
|
|
const bytes = sent.reduce((sum, frame) => sum + frame.length, 0);
|
|
expect(bytes).toBeLessThanOrEqual(64 * 1024);
|
|
expect(bytes).toBeGreaterThan(48 * 1024);
|
|
await tick();
|
|
expect(sent.reduce((sum, frame) => sum + frame.length, 0)).toBe(bytes);
|
|
scheduler.acknowledge(bytes);
|
|
await tick();
|
|
expect(sent.reduce((sum, frame) => sum + frame.length, 0)).toBeGreaterThan(bytes);
|
|
scheduler.close();
|
|
await Promise.all(pending);
|
|
});
|
|
|
|
test('rotates streams while preserving every delta and each stream end', async () => {
|
|
const { scheduler, sent } = fixture();
|
|
const pending = [];
|
|
for (let value = 0; value < 3; value++) pending.push(scheduler.send(body(1, value)));
|
|
pending.push(scheduler.send(encodeTunnelFrame(TunnelFrameType.StreamEnd, 1, new Uint8Array())));
|
|
pending.push(scheduler.send(body(3, 99, 10)));
|
|
await Promise.all(pending);
|
|
expect(sent.map(frame => decodeTunnelFrame(frame).streamId)).toEqual([1, 3, 1, 1, 1]);
|
|
expect(sent.filter(frame => decodeTunnelFrame(frame).streamId === 1).map(frame => decodeTunnelFrame(frame).payload[0])).toEqual([0, 1, 2, undefined]);
|
|
scheduler.close();
|
|
});
|
|
|
|
test('keepalives bypass credit; cancellation and close release blocked producers', async () => {
|
|
const { scheduler, sent } = fixture();
|
|
const pending = Array.from({ length: 20 }, () => scheduler.send(body(1)));
|
|
await tick();
|
|
const before = sent.length;
|
|
await scheduler.send(encodeTunnelFrame(TunnelFrameType.Pong, 0, new Uint8Array()));
|
|
expect(sent.length).toBe(before + 1);
|
|
scheduler.cancel(1);
|
|
await Promise.all(pending);
|
|
const blocked = scheduler.send(body(3));
|
|
scheduler.close();
|
|
await blocked;
|
|
// Teardown makes late ACKs harmless.
|
|
scheduler.acknowledge(123);
|
|
});
|
|
|
|
test('rejects forged, replayed, regressing and partial-frame ACKs', async () => {
|
|
const { scheduler, sent } = fixture();
|
|
await scheduler.send(body(1));
|
|
const bytes = sent[0].length;
|
|
for (const value of [0, -1, 1, bytes + 1, NaN, Infinity]) {
|
|
expect(() => scheduler.acknowledge(value)).toThrow();
|
|
}
|
|
scheduler.acknowledge(bytes);
|
|
expect(() => scheduler.acknowledge(bytes)).toThrow();
|
|
scheduler.close();
|
|
});
|
|
|
|
test('bounds pending memory and reports overflow instead of silently dropping deltas', async () => {
|
|
const { scheduler, errors } = fixture();
|
|
const pending = Array.from({ length: 600 }, () => scheduler.send(body(1)));
|
|
await Promise.all(pending);
|
|
expect(errors).toEqual(['relay downstream queue limit exceeded']);
|
|
});
|
|
|
|
test('send failure settles selected and queued producers', async () => {
|
|
const errors = [];
|
|
const scheduler = createDownstreamScheduler({
|
|
sendBatch: async () => { throw new Error('wire failed'); },
|
|
onError: error => errors.push(error.message),
|
|
});
|
|
await Promise.all([scheduler.send(body(1)), scheduler.send(body(3))]);
|
|
expect(errors).toEqual(['wire failed']);
|
|
});
|
|
|
|
test('teardown releases a producer even while encryption is still pending', async () => {
|
|
let release;
|
|
let started = false;
|
|
const scheduler = createDownstreamScheduler({
|
|
sendBatch: () => new Promise(resolve => { started = true; release = resolve; }),
|
|
onError: error => { throw error; },
|
|
});
|
|
const pending = scheduler.send(body(1));
|
|
await tick();
|
|
expect(started).toBe(true);
|
|
scheduler.close();
|
|
await pending;
|
|
release();
|
|
await tick();
|
|
});
|
|
|
|
test('sparse traffic cannot inflate the window for a later bulk burst', async () => {
|
|
const { scheduler, sent, advance } = fixture();
|
|
let acknowledged = 0;
|
|
for (let i = 0; i < 100; i++) {
|
|
const frame = body(1);
|
|
await scheduler.send(frame);
|
|
advance(50);
|
|
acknowledged += frame.length;
|
|
scheduler.acknowledge(acknowledged);
|
|
}
|
|
const before = sent.length;
|
|
const pending = Array.from({ length: 100 }, () => scheduler.send(body(1)));
|
|
await tick();
|
|
expect(sent.slice(before).reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(64 * 1024);
|
|
scheduler.close();
|
|
await Promise.all(pending);
|
|
});
|
|
|
|
test('adapts to a clear high-RTT path, then contracts when delivery backs up', async () => {
|
|
const { scheduler, sent, advance, errors } = fixture();
|
|
let acknowledged = 0;
|
|
let maxFlight = 0;
|
|
const pending = Array.from({ length: 400 }, () => scheduler.send(body(1)));
|
|
for (let round = 0; round < 12; round++) {
|
|
// Keep enough queued output to exercise growth all the way to the cap.
|
|
while (pending.length < sent.length + 400) pending.push(scheduler.send(body(1)));
|
|
await tick();
|
|
const delivered = sent.reduce((sum, frame) => sum + frame.length, 0);
|
|
maxFlight = Math.max(maxFlight, delivered - acknowledged);
|
|
expect(delivered - acknowledged).toBeLessThanOrEqual(1024 * 1024);
|
|
advance(200);
|
|
scheduler.acknowledge(delivered);
|
|
acknowledged = delivered;
|
|
}
|
|
// Reach the cap with a backlogged sender, allowing for whole-frame sizing.
|
|
expect(maxFlight).toBeGreaterThan(1024 * 1024 - body(1).length);
|
|
// Keep the sender backlogged while ACK latency jumps to a second.
|
|
for (let round = 0; round < 12; round++) {
|
|
for (let i = 0; i < 30; i++) pending.push(scheduler.send(body(1)));
|
|
await tick();
|
|
const delivered = sent.reduce((sum, frame) => sum + frame.length, 0);
|
|
advance(1000);
|
|
scheduler.acknowledge(delivered);
|
|
acknowledged = delivered;
|
|
}
|
|
await tick();
|
|
expect(sent.reduce((sum, frame) => sum + frame.length, 0) - acknowledged).toBeLessThanOrEqual(64 * 1024);
|
|
expect(errors).toEqual([]);
|
|
scheduler.close();
|
|
await Promise.all(pending);
|
|
});
|
|
});
|