Space multi-line command replies MESH_MULTI_SEND_DELAY_MS apart (v0.21.5)
CI / build + unit (pull_request) Failing after 21s
CI / build + unit (pull_request) Failing after 21s
Production incident: '#rss get lublin' returned an array of lines and CommandService sent each one back-to-back within the same second — on the real mesh 3 of 4 items were dropped in RF (the owner saw only the last one). CommandService.onRx now waits MESH_MULTI_SEND_DELAY_MS (new env, default 5000 = 5 s, read at construction like the other timing knobs) between consecutive lines of an array reply — no delay before the first line. The delay sits in the shared per-line loop, so both the respond() path and the legacy sendText fallback are covered by one await.
This commit is contained in:
@@ -30,6 +30,7 @@ function makeService(
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.COMMAND_CHANNEL; // default channel 2 under test
|
||||
process.env.MESH_MULTI_SEND_DELAY_MS = '0'; // no spacing under test unless a test opts in — the real default (5000) would slow the suite down
|
||||
});
|
||||
|
||||
describe('CommandService.onRx', () => {
|
||||
@@ -124,6 +125,31 @@ describe('CommandService.onRx', () => {
|
||||
expect(mesh.sendText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('spaces consecutive multi-line sends by MESH_MULTI_SEND_DELAY_MS — no delay before the first line', async () => {
|
||||
process.env.MESH_MULTI_SEND_DELAY_MS = '10'; // short so the test stays fast
|
||||
const sentAt: number[] = [];
|
||||
const respond = vi.fn(async () => {
|
||||
sentAt.push(Date.now());
|
||||
return { id: 1 };
|
||||
});
|
||||
try {
|
||||
const [svc, mesh] = makeService(undefined, respond);
|
||||
svc.register('multi3', () => ['a: one', 'b: two', 'c: three']);
|
||||
|
||||
const t0 = Date.now();
|
||||
await svc.onRx(2, '#multi3', 42, 9);
|
||||
|
||||
expect(mesh.respond).toHaveBeenCalledTimes(3);
|
||||
// the first line goes out immediately (no delay before it) — only microtask-scale overhead between t0 and the first send
|
||||
expect(sentAt[0] - t0).toBeLessThan(8);
|
||||
// each subsequent line waits at least MESH_MULTI_SEND_DELAY_MS after the previous one landed
|
||||
expect(sentAt[1] - sentAt[0]).toBeGreaterThanOrEqual(10);
|
||||
expect(sentAt[2] - sentAt[1]).toBeGreaterThanOrEqual(10);
|
||||
} finally {
|
||||
delete process.env.MESH_MULTI_SEND_DELAY_MS;
|
||||
}
|
||||
});
|
||||
|
||||
it('sends each line of an array reply via the legacy sendText fallback when there is no packet id', async () => {
|
||||
const [svc, mesh] = makeService();
|
||||
svc.register('multi', () => ['one: first item', 'two: second item']);
|
||||
|
||||
@@ -27,6 +27,11 @@ export class CommandService {
|
||||
process.env.COMMAND_CHANNEL ?? '2',
|
||||
10,
|
||||
);
|
||||
// spacing between consecutive multi-line reply messages — back-to-back packets get dropped in RF on a real mesh; each packet needs time to land. Read at construction like the other env config.
|
||||
private readonly multiSendDelayMs = parseInt(
|
||||
process.env.MESH_MULTI_SEND_DELAY_MS ?? '5000',
|
||||
10,
|
||||
);
|
||||
// single registration point: the constructor registers every built-in command — add the next one there (or via register())
|
||||
private readonly commands = new Map<string, CommandHandler>();
|
||||
// prefix handlers own a whole family (`#rss`, `#rss add ...`) instead of one exact name
|
||||
@@ -83,8 +88,15 @@ export class CommandService {
|
||||
try {
|
||||
const reply = await handler({ text: trimmed, from, id });
|
||||
if (reply === undefined || reply === null) return; // void = no reply
|
||||
// an array reply sends one message per line, all threaded to the same trigger packet id
|
||||
for (const line of Array.isArray(reply) ? reply : [reply]) {
|
||||
// an array reply sends one message per line, all threaded to the same trigger packet id — spaced so each packet has time to land in RF (no delay before the first line)
|
||||
const lines = Array.isArray(reply) ? reply : [reply];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (i > 0 && this.multiSendDelayMs > 0) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, this.multiSendDelayMs),
|
||||
);
|
||||
}
|
||||
const line = lines[i];
|
||||
if (id !== undefined && this.mesh) {
|
||||
// respond() auto-routes each line: rides the original record's channel with replyId = id (threaded quote)
|
||||
await this.mesh.respond(id, line);
|
||||
|
||||
Reference in New Issue
Block a user