Author SHA1 Message Date
Nixevol 7f30800192 fix: JS SDK 传输失败后换 rid 再泵发送队列 2026-09-30 17:28:33 +08:00
3 changed files with 52 additions and 0 deletions
+10
View File
@@ -1718,3 +1718,13 @@ issue #3 未关闭,`feat/fix-3-downlink-deadlock` 未合入 `main`。下面是
- 原因:原先 12 项备注写着未穷尽仍标通过,交付说明写成「通过 23」。 - 原因:原先 12 项备注写着未穷尽仍标通过,交付说明写成「通过 23」。
- 备选方案:为每个未测子项补验收用例(本波不做,避免为变绿放松断言)。 - 备选方案:为每个未测子项补验收用例(本波不做,避免为变绿放松断言)。
- 影响:汇总改为通过 19、部分通过 4(F03/F08/F21/F22)、失败 0。F19 仍引用仓库内 SDK 清单、本波不重跑。 - 影响:汇总改为通过 19、部分通过 4(F03/F08/F21/F22)、失败 0。F19 仍引用仓库内 SDK 清单、本波不重跑。
### 复审修复 R3-05
1. **JS SDK 传输失败后须再泵发送队列**
- 日期:2026-09-30
- 原条款:DEVELOPMENT 第 5 节 / 附录 SDK 行为约定:发送在途失败后换新 `rid` 并重交;`id` / 正文 / `send_at_ms` 不变。Gitea #69。
- 实际做法:`dispatchSend` 中 `publishUp` 抛出非协议 `APIError` 时,若未 `stopReconnect` 则清 inflight、换新 `rid` 后立刻 `drainSendQueue`;若已 `stopReconnect` 则 `finishSendErr` 结束本次 `send`。不额外再乘一次抖动。
- 原因:原先只换 `rid` 并 `return`,连接未断时 `hello` 不再走,队列停泵,`send()` Promise 永不结束。
- 备选方案:按 `rate_limited` 同一条的退避再泵(否决本波,连接仍在线时立即重交更贴切,且避免与已有等待叠乘抖动)。
- 影响:仅 `sdk/js`;假传输失败一次后会再次上行且 `rid` 已变。
+2
View File
@@ -739,10 +739,12 @@ export class Client {
this.finishSendErr(item, this.stopErr()); this.finishSendErr(item, this.stopErr());
return; return;
} }
// 传输失败但连接未断:换 rid 后立刻再泵,否则 hello 不再走、send 会挂起
item.inflight = false; item.inflight = false;
this.inflight = Math.max(0, this.inflight - 1); this.inflight = Math.max(0, this.inflight - 1);
this.pending.delete(rid); this.pending.delete(rid);
this.regenerateSend(item); this.regenerateSend(item);
void this.drainSendQueue();
return; return;
} }
this.finishSendErr(item, e); this.finishSendErr(item, e);
+40
View File
@@ -126,6 +126,46 @@ describe("nixmsg sdk", () => {
await c.close(); await c.close();
}, 10000); }, 10000);
it("publishUp transport fail retries with new rid", async () => {
const fake = new FakeTransport();
const c = await connectFake(fake);
const at = new Date(1_700_000_000_000);
const attempts: Array<Record<string, unknown>> = [];
let failOnce = true;
fake.publishUpImpl = async (s) => {
const m = JSON.parse(s) as Record<string, unknown>;
if (m.type !== "send") return;
attempts.push(m);
if (failOnce) {
failOnce = false;
throw new Error("transient publish");
}
};
const timer = setInterval(() => {
const sends = fake.findUp("send");
if (sends.length < 1) return;
const last = sends[sends.length - 1]!;
fake.replyOK(String(last.rid), {
id: last.id,
send_at_ms: last.send_at_ms,
state: "scheduled",
});
}, 5);
const res = await c.send(
{ kind: "endpoint", id: "b" },
{ enc: "utf8", data: "hi" },
{ sendAt: at },
);
clearInterval(timer);
expect(attempts.length).toBeGreaterThanOrEqual(2);
expect(String(attempts[1]!.rid)).not.toBe(String(attempts[0]!.rid));
expect(attempts[1]!.id).toBe(attempts[0]!.id);
expect(attempts[1]!.send_at_ms).toBe(attempts[0]!.send_at_ms);
expect((attempts[1]!.body as { data: string }).data).toBe("hi");
expect(res.id).toBe(String(attempts[0]!.id));
await c.close();
}, 5000);
it("register HTTP from ws url", async () => { it("register HTTP from ws url", async () => {
const srv = createServer((req, res) => { const srv = createServer((req, res) => {
expect(req.url).toBe("/api/client/register"); expect(req.url).toBe("/api/client/register");