[K-02][critical] JS SDK:Node 下确认失败导致进程崩溃、浏览器因全局 Buffer 无法连接、断线后发送挂起、确认失败后不再重试 #59

Closed
opened 2026-09-30 13:57:06 +08:00 by nixevol · 2 comments
Owner

编号:K-02 严重级:critical 工作线:SDK(sdk/*) 来源:审查 S-01、S-04、S-06、S-07、S-09、S-11、S-12、S-13、S-14、S-19、S-20、S-21、S-22
依赖:K-00 (#57)(对外语义条目) 被依赖:无

结论与统一方案

本 issue 汇总涉及 JS SDK 的条目,由 JS SDK 负责人一次改完(集中在 client.ts、mqtt.ts、types.ts)。下方"问题明细"是跨四套的原文,请只看 JS 的部分。

条目 严重级 JS SDK 的改法
S-04 Node 下确认失败产生未处理的 Promise 拒绝,进程退出(已复现) critical msg / receipt 处理函数整体 try/catch,或调用处 .catch(记录日志);失败后的状态处理按 S-06
S-01 断线后已发出的发送永不完成(含 PUBACK 前断线) critical close 事件里对旧 client 调 end(true),并加 generation 编号防止重复完成;断线时把在途条目置回未在途,按原 id 重交
S-06 自动确认失败后跳过置为 acked,重推被忽略 high 回调成功就置为 acked,再发 ack
S-07 浏览器没有全局 Buffer,所有上行抛 ReferenceError high publishUp 直接把字符串传给 publishAsync(Uint8Array 先解码),不再用 Buffer
S-09 重连后不恢复上下线订阅 medium 保存 {ids, all},每次握手成功后重发 presence.watch
S-11 心跳用库默认 60 秒 medium 设 keepalive: 30
S-12 顶号原因写 "0x8E"、fatal 后不失败挂起请求 medium 顶号原因改 taken_over;fatal 后失败挂起中的其他请求(fatal 同步处理已正确)
S-13 请求无超时、断线不失败、logout 不收尾 medium 默认请求超时 60 秒;断线以 not_connected 失败非发送请求;logout 与 close 同样收尾
S-14 rate_limited 固定 1 秒重试 medium 按 K-00 的退避参数
S-16 / S-24 对外语义 medium / low 按 K-00 统一(服务器不可达时 connect() 必须在超时后返回)
S-19 用 UTF-16 长度比较字节上限、离线入队不检查 low 按 UTF-8 字节计长;入队和 drain 前都检查
S-20 回执去重集合没有上限 low 10000 条 LRU
S-21 撤回事件可能重复、已撤回的消息仍交给应用 low 去重表增加 revoked 状态,撤回后跳过回调和 ack,不重复发事件
S-22 close() 后定时器未清理,Node 进程约 60 秒才退出(已复现) low close 时清理退避与连接超时定时器,并 unref()
S-23 工程问题(JS 部分) low 假传输不从 index.ts 导出;示例只打印令牌前缀
补充:重连退避双重翻倍。mqtt.ts:41-45 每次连接失败既 attempt++ 又调用 markOffline(),types.ts:174-182 的 delay(attempt) 按次数翻倍,:204-206 又把 base 翻倍;实际间隔在断线后约 1、4、16、30 秒,首次连接失败约 2、8、30 秒(第一轮审查判为"基本符合"有误) medium 按 K-00 (#57) 修订后的"重连退避"算法,只保留一个连续失败计数
补充:限速后重交复用原 rid(client.ts:505-533 用 item.frame.rid 和预先序列化的 payload 重发) low 按 K-00 修订后的"rate_limited 重交退避":每次重交重新生成 rid 并重新序列化,消息 id、正文、send_at_ms 不变;和 S-01 的重交共用同一段代码

标"补充"的条目来自第二轮 SDK 审查,总审查人已对照代码核实,证据写在条目里(没有对应的原文段落)。

改动文件

sdk/js/src/client.ts、mqtt.ts、types.ts、index.ts,sdk/js/test/*,示例与 README。

与其他问题的交互 / 冲突说明

  • 只改 sdk/js;对外语义条目以 K-00 为准。
  • 浏览器冒烟测试可复用网页线已有的 Playwright 依赖。

验收与测试

  • vitest 监听 unhandledRejection 并断言为 0:覆盖 ack 发布抛错、回调中 close、ack 回 not_found。
  • 去掉全局 Buffer 后跑 connect + send;加 Playwright 浏览器冒烟。
  • ack 失败一次后再注入同一条 msg:发出 ack 且不重复回调。
  • msg 后紧跟 revoked:回调 0 次、撤回事件 1 次。
  • 子进程跑 connect + close,2 秒内退出。
  • 断线重连后在途发送以原 id 重交并完成;服务器不可达时 connect() 在超时后返回 not_connected。
  • 退避单测(比较去掉抖动的标称值):连续失败 6 次,间隔为 1、2、4、8、16、30 秒;稳定在线 61 秒后断开,下一次约 1 秒。
  • 连续回 2 次 rate_limited:三次发出的 rid 互不相同,消息 id 与 send_at_ms 相同。

问题明细(各区审查原文,证据含文件与行号)

以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。

[S-01] 断线时已发出、未收到 resp 的发送永不重交,调用方挂起,在途额度泄漏(Go / JS / Python / Java)

  • 严重级:critical
  • 分类:逻辑 / 与PRD不符
  • 现象与影响:发送帧 publish 后、resp 回来前断线,这一条会一直标记为“在途”。重连后的 drain/pump 只挑未在途的条目,所以永不按原消息号重交。
    • 调用方挂起情况:Go 的 Send 在 ctx 没有截止时间时永久阻塞;JS 的 Promise 永远不会完成;Python 调用时若已在线,会 wait(timeout=None) 永久阻塞;Java 等 1 小时后抛 busy,但条目仍留在队列里。
    • 在途计数永不回退:累计 100 次后该进程再也发不出消息,最终队列报满。
    • JS 更严重:MQTT.js 在 reconnectPeriod:0 下,非主动断开时只清掉 volatile 回调,QoS1 publish 的回调永不触发,所以在 PUBACK 之前断线也会卡住。
    • Java:PUBACK 之前断线时,pumpSends 直接把这条发送判为失败并出队,不会重交。
  • 证据:
    • Go:send.go:170-180(publish 成功后 rf := <-ch 既无超时,也没有断线出口)、send.go:141-153(只挑 !it.inflight)、connect.go:57-64(OnOffline 没有处理在途条目)。
    • JS:client.ts:494-543、client.ts:174-177、mqtt.ts:93-99(断线后新建 client,不 end 旧的);MQTT.js client.js:380-384、:882-904、:985-987、:490-496。
    • Python:client.py:861-873(只挑 rid=="")、client.py:605-640、client.py:323。
    • Java:Client.java:1000-1006、Client.java:1019-1029(publish 抛异常就判失败并出队)、Transport.java:383-391。
  • 文档依据:
    • PRD F19:“重连后按原消息号再交;已经发出但没等到结果的也一样”。
    • DEVELOPMENT 9 发送:“包括已发出但没收到 resp 的……重连后按原消息号、原请求内容再交”。
    • DEVELOPMENT 5:“提交成功但还没发出 resp 就断线时,客户端用同一消息号重试”。
  • 为何不是故意设计:DEVIATIONS 没有相关条目;各 SDK 的注释都写着“网络错误:保留队列”,意图就是重交。
  • 解决方案:
    1. 在“断线且将重连”的钩子里遍历发送队列:删除 pending rid,置回未在途(Python/Java 设 pending.rid=""),按实际状态重算在途数;帧内容保持原样(原 id、原 send_at_ms)。服务器按消息号防重,会返回原结果。
    2. Go 的 dispatchSend 改为 select 三路:ch、每次连接一个的 connGone、c.ctx.Done()。
    3. JS 在 close 事件里对旧 client 调 end(true),并加 generation 编号,防止同一条被重复完成。
    4. Java 的 publish 失败在未停止重连时回退为未在途,不判失败。
    5. 可选:等 resp 超时(如 60 秒)也按原 id 重交,兜底 mochi 在 inflight 满时静默丢弃的 resp。
  • 改动文件:sdk/go/send.go、connect.go;sdk/js/src/client.ts、mqtt.ts;sdk/python/src/nixmsg/client.py;sdk/java/.../Client.java
  • 与其他模块的交互/冲突风险:不需要服务端配合(依赖已有的消息号防重)。和 S-13 改的是同一个断线钩子。
  • 需补测试:假传输中 publish 后不回 resp,模拟断线重连,断言以同 id、同 send_at_ms 重发并完成;循环 150 次后仍能发送;Java 模拟 publish 抛异常,断言重交而非失败。
  • 置信度:代码阅读确定;MQTT.js 行为已按库源码核对。

[S-04] JS 在 Node 下确认失败会产生未处理的 Promise 拒绝,进程崩溃(JS)

  • 严重级:critical(Node;浏览器下是报错日志,后果见 S-6)
  • 分类:逻辑
  • 现象与影响:收到 msg 和 receipt 时调用处用 void 丢弃了 Promise,内部 await 确认请求又没有 try/catch。下面任一情况都会让 Node 进程以退出码 1 结束:
    • 回调执行期间断线;
    • 回调执行中调用了 close();
    • 服务端对 ack 回错误(例如发送方已删除后返回 not_found)。
  • 证据:
    • client.ts:286,289,326,358,393,398,413-437。
    • 复现(esbuild 打包后经管道传给 node,不落盘):ack 发布抛错时,进程打印 Error: not connected ... at Client.handleMsg 后退出;回调中调用 close() 时,出现 APIError: closed,exit code = 1。
  • 文档依据:DEVELOPMENT 9“回调抛错则不发,打出错误”;PRD F08。
  • 为何不是故意设计:没有相关偏差记录。
  • 解决方案:两个处理函数整体 try/catch,或在调用处 .catch(记录日志);失败后的状态处理按 S-6。
  • 改动文件:sdk/js/src/client.ts
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:vitest 监听 unhandledRejection 并断言为 0;覆盖 ack 发布抛错、回调中 close、ack 回 not_found 三种用例。
  • 置信度:已用测试复现。

[S-06] 确认失败后没有恢复:重推被忽略、重复回执不再确认、手动 ack 吞掉失败(Python / Java / JS)

  • 严重级:high
  • 分类:协议一致性 / 逻辑
  • 现象与影响:
    1. 自动确认:回调成功后 ack 失败,Python/Java 吞掉异常,去重状态停在“已交未确认”;JS 跳过了置为 acked 这一步。之后服务器重推同一条,SDK 走“已交未确认就忽略”的分支,永不再确认。结果是:保留消息最终过期,不保留消息被丢弃,发送方拿到 expired/dropped,但应用其实已经处理过;确认超时之前这条还占着 32 个推送窗口中的一个。Go 在回调成功后直接置为 acked,重推时会再 ack,这是正确做法。
    2. 重复回执:Python/Java 遇到重复的 receipt_id 直接 return,不发 receipt_ack。服务器每次 PushPending(每个接收方 ack 都会唤醒发送方)都会重推 receipt_id 最小的 64 条未确认回执。一旦首次确认失败(例如断线后还在消化积压,_request 直接返回 NotConnected),这条回执就永远占着窗口;积满 64 条后,该发送方再也收不到新回执。
    3. 手动确认:Python 的 ack() 和 Java 的 ack() 失败时不报错(Java 的 future 正常完成),应用无从得知。
  • 证据:
    • Python:client.py:756-758,798-803,836-851。
    • Java:Client.java:884-887,928-934,965-984。
    • JS:client.ts:352-360。
    • 对照 Go:receive.go:136-141,202-206。
    • 服务端:push.go:88-237,456-495,549-560。
  • 文档依据:DEVELOPMENT 9“已确认过的再次到达:直接再发一次 ack”;6.4“回执……可能重复,SDK 按 receipt_id 去重”。
  • 为何不是故意设计:文档里的“忽略”针对的是回调还在执行、或手动模式等应用确认的情形。Go 的实现与此一致,另外三套偏离了。
  • 解决方案:
    1. 自动模式下回调成功就置为 acked,再发 ack。
    2. 重复回执同样发 receipt_ack(可按 id 节流)。
    3. 手动 ack 失败时 Python 抛异常、Java 的 future 以异常完成。
  • 改动文件:Python client.py、Java Client.java、JS client.ts
  • 与其他模块的交互/冲突风险:与 S-1、S-13 配合,让断线时的 ack 快速失败,而不是等 60 秒。服务端“每次唤醒重推 64 条回执”的放大问题见待核实第 3 条。
  • 需补测试:ack 失败一次后再注入同一条 msg,断言发出 ack 且不重复回调;重复回执每次都 ack;手动 ack 失败时报错。
  • 置信度:代码阅读确定。

[S-07] JS 在浏览器里无法连接:用了 Node 专有的全局 Buffer(JS)

  • 严重级:high
  • 分类:与PRD不符
  • 现象与影响:publishUp 调用 Buffer.from(...)。浏览器没有全局 Buffer(MQTT.js 浏览器包自带 Buffer,但不挂到全局;webpack 5 和 Vite 默认也不注入),所以包括 hello 在内的所有上行都抛 ReferenceError,握手永远失败并无限重连。
  • 证据:
    • mqtt.ts:139;mqtt/dist/mqtt.esm.js 里 Buffer 只是模块内变量。
    • 复现:以浏览器平台打包后删除 globalThis.Buffer,再调 publishUp,得到 ReferenceError: Buffer is not defined。
  • 文档依据:PRD F19 要求“浏览器和 Node.js”、支持近两年主流浏览器;TASKS 6.8“JS 同时支持浏览器和 Node”。
  • 为何不是故意设计:DEVIATIONS S1.7 第 4 条只说明没起真实浏览器做跨源验证,没有放弃浏览器支持。
  • 解决方案:直接把字符串传给 publishAsync(Uint8Array 先解码),不再用 Buffer。
  • 改动文件:sdk/js/src/mqtt.ts
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:去掉全局 Buffer 后跑 connect+send;另加 Playwright 浏览器冒烟测试(Q/W 线已有这项依赖)。
  • 置信度:代码阅读确定,并在 Node 中模拟复现;未在真实浏览器运行。

[S-09] Go / JS 重连后不恢复上下线订阅(Go / JS)

  • 严重级:medium
  • 分类:与PRD不符
  • 现象与影响:watch 只发一次,SDK 不记住应用的选择。任何一次重连(网络抖动、管理员踢下线、服务器重启)之后,上下线通知都会静默停止。Python/Java 已经实现了记忆和重新订阅。
  • 证据:Go api.go:74-82、connect.go:148-216;JS client.ts:601-606;对照 Python client.py:585-593、Java Client.java:672-683。
  • 文档依据:PRD F04“SDK 在重连后按应用上次的选择重新订阅”。
  • 为何不是故意设计:没有偏差记录,另外两套已经实现。
  • 解决方案:保存 {ids, all},每次握手成功后异步重发 presence.watch。
  • 改动文件:Go api.go、connect.go;JS client.ts
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:watch 后模拟重连,断言新握手后再次发出同样参数的 presence.watch。
  • 置信度:代码阅读确定。

[S-11] JS 和 Java 的心跳是 60 秒,文档规定 30 秒(JS / Java)

  • 严重级:medium
  • 分类:协议一致性
  • 现象与影响:两套都没设 keepalive,用的是库默认值 60 秒。离线判定因此变成约 90 秒(PRD 要求约 45 秒),影响在线状态和宽限期。
  • 证据:mqtt.ts:57-69 加 MQTT.js client.js:63;Transport.java:313-321 加 Mqtt5Connect.DEFAULT_KEEP_ALIVE=60。
  • 文档依据:DEVELOPMENT 5“SDK 默认 30”;PRD F03。
  • 为何不是故意设计:没有偏差记录,Go/Python 都是 30。
  • 解决方案:JS 设 keepalive: 30,Java 设 .keepAlive(30)。
  • 改动文件:mqtt.ts、Transport.java
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:断言连接参数中的 keepalive 为 30。
  • 置信度:代码阅读确定(库默认值已核对源码和字节码)。

[S-12] fatal 与顶号的处理时序、原因上报不一致(Go / Python / Java;Go / JS 原因名)

  • 严重级:medium
  • 分类:协议一致性 / 并发
  • 现象与影响:服务端的顺序是:先清令牌,再发 fatal,20ms 后以 0x98 断开。
    1. Go/Python/Java 把 fatal 放进串行业务队列异步处理。如果处理线程正忙(执行回调或等 ack),断线会先被处理:Python/Java 立刻重连,Go 的退避 Func(0)=0 也立刻重连,拿作废令牌连上得到 0x86,于是先上报 auth_failed(session_invalid);Python/Java 随后再报一次真实原因;Go 的 failAuth 调 cancel 后,downLoop 在 select 里可能直接退出,fatal 原因根本没上报。JS 在收包回调里同步处理 fatal,是正确的。
    2. Go 每个状态事件起一个 goroutine,顺序不保证。
    3. Go/JS 被顶号时原因写 "0x8E",应为 taken_over。
    4. Go/JS fatal 之后不失败挂起中的其他请求。
  • 证据:服务端 session.go:316-336、broker.go:274-292;Go receive.go:38-42,75-87、connect.go:120,135-146、backoff.go:27-29、transport_mqtt.go:81-92(只看 t.stopped,fatal 没有设置它);Python client.py:652-655,735-745;Java Client.java:752,863-875;JS client.ts:207,304-311。
  • 文档依据:DEVELOPMENT 6.8;9 第 6 条“收到 fatal……事件 auth_failed 带原因”;5“原因 taken_over”。
  • 为何不是故意设计:没有偏差记录;JS 已经按同步方式处理。
  • 解决方案:
    1. 在收包路径识别 fatal 后立即置停止标志、失败发送队列和所有挂起请求、只上报一次原因、异步停止传输(Go 还要设 transport 的 stopped);之后到来的 CONNACK 拒绝不再覆盖原因。
    2. Go 的状态事件改为有序派发。
    3. Go/JS 顶号原因改为 taken_over。
  • 改动文件:Go receive.go、connect.go、transport_mqtt.go;Python client.py;Java Client.java;JS client.ts
  • 与其他模块的交互/冲突风险:和 S-5、S-8 同文件。
  • 需补测试:处理线程阻塞时注入 fatal 加断线,断言只出现一次正确原因、没有重连尝试;真机上停用、删除、重置密码分别断言原因为 disabled、deleted、password_reset。
  • 置信度:代码阅读确定;时序竞态部分为“较高”。

[S-13] 断线时非发送请求不失败、JS 请求无超时;Go / JS 的 logout 不收尾(Go / JS;Python / Java 部分)

  • 严重级:medium
  • 分类:协议一致性
  • 现象与影响:
    • 断线时挂起中的 recall、群操作等请求:JS 永不结束,Go 只能等调用方 ctx,Python/Java 要等满 60 秒;Python/Java 的 down 线程还会卡在 ack 上。
    • Go 的 Logout 不失败发送队列、不停止传输、不发 offline 事件;JS 的 logout 不失败队列、不发 offline 事件。
  • 证据:JS client.ts:413-437,718-725;Go receive.go:322-337、connect.go:232-245;Python client.py:913;Java Client.java:1055。
  • 文档依据:PRD F19“除发送以外的请求在离线时直接失败”;DEVELOPMENT 9“停止重连时队列里的发送全部以对应错误结束”。
  • 为何不是故意设计:没有偏差记录,Python/Java 的 logout 已经收尾。
  • 解决方案:断线钩子里以 not_connected 失败非发送请求;Go/JS 默认请求超时 60 秒;logout 做和 close 相同的收尾。
  • 改动文件:Go receive.go、connect.go;JS client.ts;Python client.py、Java Client.java(断线失败挂起请求)
  • 与其他模块的交互/冲突风险:和 S-1 同一断线钩子。
  • 需补测试:recall 未得到响应时断线,断言立即报错;logout 后断言队列清空并发出 offline。
  • 置信度:代码阅读确定。

[S-14] 发送收到 rate_limited 后重交没有退避(Python / Java 热循环;Go / JS 固定 1 秒)

  • 严重级:medium
  • 分类:协议一致性
  • 现象与影响:Python/Java 清空 rid 后立即唤醒发送泵,形成按往返时间计的重试风暴,持续打满服务器令牌桶。Go/JS 固定 1 秒重试,不会随连续限流拉长。
  • 证据:Python client.py:682-689;Java Client.java:797-805;Go send.go:186-195;JS client.ts:528-533。
  • 文档依据:DEVELOPMENT 9“按退避自动重交”。
  • 为何不是故意设计:没有偏差记录,文档明确要求退避。
  • 解决方案:每条发送记录连续限流次数,按 1 秒起、翻倍、上限 30 秒、±30% 抖动退避,成功后清零。
  • 改动文件:四套 send/pump 相关文件
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:连续回 3 次 rate_limited,断言重发间隔约为 1、2、4 秒。
  • 置信度:代码阅读确定。

[S-19] 本地整帧检查有缺口(四套)

  • 严重级:low
  • 分类:协议一致性
  • 现象与影响:JS 用字符串长度(UTF-16 码元)比较字节上限;四套在离线时入队的帧都不检查,握手后也不复查。超限帧会被服务器直接断开,重连后重交同一帧,形成无限循环。
  • 证据:JS client.ts:478-482;Go send.go:96-102。
  • 文档依据:DEVELOPMENT 9 本地检查一段。
  • 为何不是故意设计:文档明确要求本地拦截,正是为了避免这个循环。
  • 解决方案:JS 按 UTF-8 字节计长;四套在入队和 drain 前都按握手给出的 max_frame_bytes 检查。
  • 改动文件:四套 send 相关文件
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:离线时发送超大 meta 应报 frame_too_large;用中文 meta 构造“UTF-16 未超、UTF-8 超限”的用例。
  • 置信度:代码阅读确定。

[S-20] Go / JS 的回执去重集合没有上限(Go / JS)

  • 严重级:low
  • 分类:数据(资源)
  • 现象与影响:去重集合只增不删,长期运行的发送端内存持续增长。
  • 证据:Go client.go:72、receive.go:201-208;JS client.ts:93。
  • 文档依据:DEVELOPMENT 9 去重容量 10000 的约定(Python/Java 已按此为回执设上限)。
  • 为何不是故意设计:Python/Java 已按 10000 条设上限。
  • 解决方案:改为 10000 条的 LRU。
  • 改动文件:Go client.go、receive.go;JS client.ts
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:注入 10001 个不同回执,断言集合大小不超过 10000。
  • 置信度:代码阅读确定。

[S-21] JS 撤回事件可能重复、已撤回的消息仍交给应用(JS;手动模式四套都可能重复)

  • 严重级:low
  • 分类:协议一致性
  • 现象与影响:JS 回调还没执行时收到 revoked,会先发一次撤回事件,但回调照样执行;自动 ack 的结果又触发第二次撤回事件。手动模式下,四套在 revoked 之后应用再调 ack 时,都会再发一次撤回事件。
  • 证据:JS client.ts:323-360,378-411;Go receive.go:157-185;Python client.py:836-848;Java Client.java:965-977。
  • 文档依据:DEVELOPMENT 6.4“还没交给应用,直接丢弃”。
  • 为何不是故意设计:文档要求直接丢弃。
  • 解决方案:去重表增加 revoked 状态,撤回后跳过回调和 ack,并避免重复发事件。
  • 改动文件:JS client.ts;手动模式四套同改
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:msg 后紧跟 revoked,断言回调 0 次、撤回事件 1 次。
  • 置信度:代码阅读确定。

[S-22] JS close() 后定时器没有清理,Node 进程约 60 秒才退出(JS)

  • 严重级:low
  • 分类:质量
  • 现象与影响:退避对象的 60 秒定时器和连接超时定时器在 close 时都没有清理。
  • 证据:
    • types.ts:184-195、mqtt.ts:130-132。
    • 复现:close 1ms 就返回,但进程过了 60012ms 才退出。
  • 文档依据:无直接条款,属资源清理。
  • 为何不是故意设计:没有理由让进程在 close 后滞留。
  • 解决方案:close 时清理这两个定时器,并调用 unref()。
  • 改动文件:types.ts、mqtt.ts、client.ts
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:用子进程跑 connect+close,断言 2 秒内退出。
  • 置信度:已用测试复现。

复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。

**编号**:K-02 **严重级**:critical **工作线**:SDK(sdk/*) **来源**:审查 S-01、S-04、S-06、S-07、S-09、S-11、S-12、S-13、S-14、S-19、S-20、S-21、S-22 **依赖**:K-00 (#57)(对外语义条目) **被依赖**:无 ### 结论与统一方案 本 issue 汇总涉及 JS SDK 的条目,由 JS SDK 负责人一次改完(集中在 `client.ts`、`mqtt.ts`、`types.ts`)。下方"问题明细"是跨四套的原文,请只看 JS 的部分。 | 条目 | 严重级 | JS SDK 的改法 | |---|---|---| | S-04 Node 下确认失败产生未处理的 Promise 拒绝,进程退出(已复现) | critical | msg / receipt 处理函数整体 try/catch,或调用处 `.catch(记录日志)`;失败后的状态处理按 S-06 | | S-01 断线后已发出的发送永不完成(含 PUBACK 前断线) | critical | `close` 事件里对旧 client 调 `end(true)`,并加 generation 编号防止重复完成;断线时把在途条目置回未在途,按原 id 重交 | | S-06 自动确认失败后跳过置为 acked,重推被忽略 | high | 回调成功就置为 acked,再发 ack | | S-07 浏览器没有全局 Buffer,所有上行抛 ReferenceError | high | `publishUp` 直接把字符串传给 `publishAsync`(Uint8Array 先解码),不再用 Buffer | | S-09 重连后不恢复上下线订阅 | medium | 保存 `{ids, all}`,每次握手成功后重发 presence.watch | | S-11 心跳用库默认 60 秒 | medium | 设 `keepalive: 30` | | S-12 顶号原因写 "0x8E"、fatal 后不失败挂起请求 | medium | 顶号原因改 `taken_over`;fatal 后失败挂起中的其他请求(fatal 同步处理已正确) | | S-13 请求无超时、断线不失败、`logout` 不收尾 | medium | 默认请求超时 60 秒;断线以 `not_connected` 失败非发送请求;`logout` 与 `close` 同样收尾 | | S-14 rate_limited 固定 1 秒重试 | medium | 按 K-00 的退避参数 | | S-16 / S-24 对外语义 | medium / low | 按 K-00 统一(服务器不可达时 `connect()` 必须在超时后返回) | | S-19 用 UTF-16 长度比较字节上限、离线入队不检查 | low | 按 UTF-8 字节计长;入队和 drain 前都检查 | | S-20 回执去重集合没有上限 | low | 10000 条 LRU | | S-21 撤回事件可能重复、已撤回的消息仍交给应用 | low | 去重表增加 revoked 状态,撤回后跳过回调和 ack,不重复发事件 | | S-22 `close()` 后定时器未清理,Node 进程约 60 秒才退出(已复现) | low | close 时清理退避与连接超时定时器,并 `unref()` | | S-23 工程问题(JS 部分) | low | 假传输不从 `index.ts` 导出;示例只打印令牌前缀 | | 补充:重连退避双重翻倍。`mqtt.ts:41-45` 每次连接失败既 `attempt++` 又调用 `markOffline()`,`types.ts:174-182` 的 `delay(attempt)` 按次数翻倍,`:204-206` 又把 base 翻倍;实际间隔在断线后约 1、4、16、30 秒,首次连接失败约 2、8、30 秒(第一轮审查判为"基本符合"有误) | medium | 按 K-00 (#57) 修订后的"重连退避"算法,只保留一个连续失败计数 | | 补充:限速后重交复用原 rid(`client.ts:505-533` 用 `item.frame.rid` 和预先序列化的 payload 重发) | low | 按 K-00 修订后的"rate_limited 重交退避":每次重交重新生成 rid 并重新序列化,消息 id、正文、`send_at_ms` 不变;和 S-01 的重交共用同一段代码 | 标"补充"的条目来自第二轮 SDK 审查,总审查人已对照代码核实,证据写在条目里(没有对应的原文段落)。 ### 改动文件 `sdk/js/src/client.ts`、`mqtt.ts`、`types.ts`、`index.ts`,`sdk/js/test/*`,示例与 README。 ### 与其他问题的交互 / 冲突说明 - 只改 sdk/js;对外语义条目以 K-00 为准。 - 浏览器冒烟测试可复用网页线已有的 Playwright 依赖。 ### 验收与测试 - vitest 监听 `unhandledRejection` 并断言为 0:覆盖 ack 发布抛错、回调中 close、ack 回 not_found。 - 去掉全局 Buffer 后跑 connect + send;加 Playwright 浏览器冒烟。 - ack 失败一次后再注入同一条 msg:发出 ack 且不重复回调。 - msg 后紧跟 revoked:回调 0 次、撤回事件 1 次。 - 子进程跑 connect + close,2 秒内退出。 - 断线重连后在途发送以原 id 重交并完成;服务器不可达时 `connect()` 在超时后返回 `not_connected`。 - 退避单测(比较去掉抖动的标称值):连续失败 6 次,间隔为 1、2、4、8、16、30 秒;稳定在线 61 秒后断开,下一次约 1 秒。 - 连续回 2 次 rate_limited:三次发出的 rid 互不相同,消息 id 与 `send_at_ms` 相同。 --- ### 问题明细(各区审查原文,证据含文件与行号) > 以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。**解决方案以本 issue 上方的"结论与统一方案"为准**;原文里的方案与之不一致时,按上方执行。 #### [S-01] 断线时已发出、未收到 resp 的发送永不重交,调用方挂起,在途额度泄漏(Go / JS / Python / Java) - **严重级**:critical - **分类**:逻辑 / 与PRD不符 - **现象与影响**:发送帧 publish 后、resp 回来前断线,这一条会一直标记为“在途”。重连后的 drain/pump 只挑未在途的条目,所以永不按原消息号重交。 - 调用方挂起情况:Go 的 `Send` 在 ctx 没有截止时间时永久阻塞;JS 的 Promise 永远不会完成;Python 调用时若已在线,会 `wait(timeout=None)` 永久阻塞;Java 等 1 小时后抛 busy,但条目仍留在队列里。 - 在途计数永不回退:累计 100 次后该进程再也发不出消息,最终队列报满。 - JS 更严重:MQTT.js 在 `reconnectPeriod:0` 下,非主动断开时只清掉 volatile 回调,QoS1 publish 的回调永不触发,所以在 PUBACK 之前断线也会卡住。 - Java:PUBACK 之前断线时,`pumpSends` 直接把这条发送判为失败并出队,不会重交。 - **证据**: - Go:`send.go:170-180`(publish 成功后 `rf := <-ch` 既无超时,也没有断线出口)、`send.go:141-153`(只挑 `!it.inflight`)、`connect.go:57-64`(OnOffline 没有处理在途条目)。 - JS:`client.ts:494-543`、`client.ts:174-177`、`mqtt.ts:93-99`(断线后新建 client,不 `end` 旧的);MQTT.js `client.js:380-384`、`:882-904`、`:985-987`、`:490-496`。 - Python:`client.py:861-873`(只挑 `rid==""`)、`client.py:605-640`、`client.py:323`。 - Java:`Client.java:1000-1006`、`Client.java:1019-1029`(publish 抛异常就判失败并出队)、`Transport.java:383-391`。 - **文档依据**: - PRD F19:“重连后按原消息号再交;已经发出但没等到结果的也一样”。 - DEVELOPMENT 9 发送:“包括已发出但没收到 resp 的……重连后按原消息号、原请求内容再交”。 - DEVELOPMENT 5:“提交成功但还没发出 resp 就断线时,客户端用同一消息号重试”。 - **为何不是故意设计**:DEVIATIONS 没有相关条目;各 SDK 的注释都写着“网络错误:保留队列”,意图就是重交。 - **解决方案**: 1. 在“断线且将重连”的钩子里遍历发送队列:删除 pending rid,置回未在途(Python/Java 设 `pending.rid=""`),按实际状态重算在途数;帧内容保持原样(原 id、原 `send_at_ms`)。服务器按消息号防重,会返回原结果。 2. Go 的 `dispatchSend` 改为 `select` 三路:`ch`、每次连接一个的 `connGone`、`c.ctx.Done()`。 3. JS 在 `close` 事件里对旧 client 调 `end(true)`,并加 generation 编号,防止同一条被重复完成。 4. Java 的 publish 失败在未停止重连时回退为未在途,不判失败。 5. 可选:等 resp 超时(如 60 秒)也按原 id 重交,兜底 mochi 在 inflight 满时静默丢弃的 resp。 - **改动文件**:`sdk/go/send.go`、`connect.go`;`sdk/js/src/client.ts`、`mqtt.ts`;`sdk/python/src/nixmsg/client.py`;`sdk/java/.../Client.java` - **与其他模块的交互/冲突风险**:不需要服务端配合(依赖已有的消息号防重)。和 S-13 改的是同一个断线钩子。 - **需补测试**:假传输中 publish 后不回 resp,模拟断线重连,断言以同 id、同 `send_at_ms` 重发并完成;循环 150 次后仍能发送;Java 模拟 publish 抛异常,断言重交而非失败。 - **置信度**:代码阅读确定;MQTT.js 行为已按库源码核对。 #### [S-04] JS 在 Node 下确认失败会产生未处理的 Promise 拒绝,进程崩溃(JS) - **严重级**:critical(Node;浏览器下是报错日志,后果见 S-6) - **分类**:逻辑 - **现象与影响**:收到 msg 和 receipt 时调用处用 `void` 丢弃了 Promise,内部 `await` 确认请求又没有 try/catch。下面任一情况都会让 Node 进程以退出码 1 结束: - 回调执行期间断线; - 回调执行中调用了 `close()`; - 服务端对 ack 回错误(例如发送方已删除后返回 `not_found`)。 - **证据**: - `client.ts:286,289,326,358,393,398,413-437`。 - 复现(esbuild 打包后经管道传给 node,不落盘):ack 发布抛错时,进程打印 `Error: not connected ... at Client.handleMsg` 后退出;回调中调用 `close()` 时,出现 `APIError: closed`,`exit code = 1`。 - **文档依据**:DEVELOPMENT 9“回调抛错则不发,打出错误”;PRD F08。 - **为何不是故意设计**:没有相关偏差记录。 - **解决方案**:两个处理函数整体 try/catch,或在调用处 `.catch(记录日志)`;失败后的状态处理按 S-6。 - **改动文件**:`sdk/js/src/client.ts` - **与其他模块的交互/冲突风险**:无。 - **需补测试**:vitest 监听 `unhandledRejection` 并断言为 0;覆盖 ack 发布抛错、回调中 close、ack 回 not_found 三种用例。 - **置信度**:已用测试复现。 #### [S-06] 确认失败后没有恢复:重推被忽略、重复回执不再确认、手动 ack 吞掉失败(Python / Java / JS) - **严重级**:high - **分类**:协议一致性 / 逻辑 - **现象与影响**: 1. **自动确认**:回调成功后 ack 失败,Python/Java 吞掉异常,去重状态停在“已交未确认”;JS 跳过了置为 acked 这一步。之后服务器重推同一条,SDK 走“已交未确认就忽略”的分支,永不再确认。结果是:保留消息最终过期,不保留消息被丢弃,发送方拿到 expired/dropped,但应用其实已经处理过;确认超时之前这条还占着 32 个推送窗口中的一个。Go 在回调成功后直接置为 acked,重推时会再 ack,这是正确做法。 2. **重复回执**:Python/Java 遇到重复的 receipt_id 直接 return,不发 receipt_ack。服务器每次 PushPending(每个接收方 ack 都会唤醒发送方)都会重推 receipt_id 最小的 64 条未确认回执。一旦首次确认失败(例如断线后还在消化积压,`_request` 直接返回 NotConnected),这条回执就永远占着窗口;积满 64 条后,该发送方再也收不到新回执。 3. **手动确认**:Python 的 `ack()` 和 Java 的 `ack()` 失败时不报错(Java 的 future 正常完成),应用无从得知。 - **证据**: - Python:`client.py:756-758,798-803,836-851`。 - Java:`Client.java:884-887,928-934,965-984`。 - JS:`client.ts:352-360`。 - 对照 Go:`receive.go:136-141,202-206`。 - 服务端:`push.go:88-237,456-495,549-560`。 - **文档依据**:DEVELOPMENT 9“已确认过的再次到达:直接再发一次 ack”;6.4“回执……可能重复,SDK 按 receipt_id 去重”。 - **为何不是故意设计**:文档里的“忽略”针对的是回调还在执行、或手动模式等应用确认的情形。Go 的实现与此一致,另外三套偏离了。 - **解决方案**: 1. 自动模式下回调成功就置为 acked,再发 ack。 2. 重复回执同样发 receipt_ack(可按 id 节流)。 3. 手动 ack 失败时 Python 抛异常、Java 的 future 以异常完成。 - **改动文件**:Python `client.py`、Java `Client.java`、JS `client.ts` - **与其他模块的交互/冲突风险**:与 S-1、S-13 配合,让断线时的 ack 快速失败,而不是等 60 秒。服务端“每次唤醒重推 64 条回执”的放大问题见待核实第 3 条。 - **需补测试**:ack 失败一次后再注入同一条 msg,断言发出 ack 且不重复回调;重复回执每次都 ack;手动 ack 失败时报错。 - **置信度**:代码阅读确定。 #### [S-07] JS 在浏览器里无法连接:用了 Node 专有的全局 `Buffer`(JS) - **严重级**:high - **分类**:与PRD不符 - **现象与影响**:`publishUp` 调用 `Buffer.from(...)`。浏览器没有全局 Buffer(MQTT.js 浏览器包自带 Buffer,但不挂到全局;webpack 5 和 Vite 默认也不注入),所以包括 hello 在内的所有上行都抛 ReferenceError,握手永远失败并无限重连。 - **证据**: - `mqtt.ts:139`;`mqtt/dist/mqtt.esm.js` 里 Buffer 只是模块内变量。 - 复现:以浏览器平台打包后删除 `globalThis.Buffer`,再调 `publishUp`,得到 `ReferenceError: Buffer is not defined`。 - **文档依据**:PRD F19 要求“浏览器和 Node.js”、支持近两年主流浏览器;TASKS 6.8“JS 同时支持浏览器和 Node”。 - **为何不是故意设计**:DEVIATIONS S1.7 第 4 条只说明没起真实浏览器做跨源验证,没有放弃浏览器支持。 - **解决方案**:直接把字符串传给 `publishAsync`(Uint8Array 先解码),不再用 Buffer。 - **改动文件**:`sdk/js/src/mqtt.ts` - **与其他模块的交互/冲突风险**:无。 - **需补测试**:去掉全局 Buffer 后跑 connect+send;另加 Playwright 浏览器冒烟测试(Q/W 线已有这项依赖)。 - **置信度**:代码阅读确定,并在 Node 中模拟复现;未在真实浏览器运行。 #### [S-09] Go / JS 重连后不恢复上下线订阅(Go / JS) - **严重级**:medium - **分类**:与PRD不符 - **现象与影响**:watch 只发一次,SDK 不记住应用的选择。任何一次重连(网络抖动、管理员踢下线、服务器重启)之后,上下线通知都会静默停止。Python/Java 已经实现了记忆和重新订阅。 - **证据**:Go `api.go:74-82`、`connect.go:148-216`;JS `client.ts:601-606`;对照 Python `client.py:585-593`、Java `Client.java:672-683`。 - **文档依据**:PRD F04“SDK 在重连后按应用上次的选择重新订阅”。 - **为何不是故意设计**:没有偏差记录,另外两套已经实现。 - **解决方案**:保存 `{ids, all}`,每次握手成功后异步重发 presence.watch。 - **改动文件**:Go `api.go`、`connect.go`;JS `client.ts` - **与其他模块的交互/冲突风险**:无。 - **需补测试**:watch 后模拟重连,断言新握手后再次发出同样参数的 presence.watch。 - **置信度**:代码阅读确定。 #### [S-11] JS 和 Java 的心跳是 60 秒,文档规定 30 秒(JS / Java) - **严重级**:medium - **分类**:协议一致性 - **现象与影响**:两套都没设 keepalive,用的是库默认值 60 秒。离线判定因此变成约 90 秒(PRD 要求约 45 秒),影响在线状态和宽限期。 - **证据**:`mqtt.ts:57-69` 加 MQTT.js `client.js:63`;`Transport.java:313-321` 加 `Mqtt5Connect.DEFAULT_KEEP_ALIVE=60`。 - **文档依据**:DEVELOPMENT 5“SDK 默认 30”;PRD F03。 - **为何不是故意设计**:没有偏差记录,Go/Python 都是 30。 - **解决方案**:JS 设 `keepalive: 30`,Java 设 `.keepAlive(30)`。 - **改动文件**:`mqtt.ts`、`Transport.java` - **与其他模块的交互/冲突风险**:无。 - **需补测试**:断言连接参数中的 keepalive 为 30。 - **置信度**:代码阅读确定(库默认值已核对源码和字节码)。 #### [S-12] fatal 与顶号的处理时序、原因上报不一致(Go / Python / Java;Go / JS 原因名) - **严重级**:medium - **分类**:协议一致性 / 并发 - **现象与影响**:服务端的顺序是:先清令牌,再发 fatal,20ms 后以 0x98 断开。 1. Go/Python/Java 把 fatal 放进串行业务队列异步处理。如果处理线程正忙(执行回调或等 ack),断线会先被处理:Python/Java 立刻重连,Go 的退避 `Func(0)=0` 也立刻重连,拿作废令牌连上得到 0x86,于是先上报 `auth_failed(session_invalid)`;Python/Java 随后再报一次真实原因;Go 的 `failAuth` 调 cancel 后,downLoop 在 select 里可能直接退出,fatal 原因根本没上报。JS 在收包回调里同步处理 fatal,是正确的。 2. Go 每个状态事件起一个 goroutine,顺序不保证。 3. Go/JS 被顶号时原因写 `"0x8E"`,应为 `taken_over`。 4. Go/JS fatal 之后不失败挂起中的其他请求。 - **证据**:服务端 `session.go:316-336`、`broker.go:274-292`;Go `receive.go:38-42,75-87`、`connect.go:120,135-146`、`backoff.go:27-29`、`transport_mqtt.go:81-92`(只看 `t.stopped`,fatal 没有设置它);Python `client.py:652-655,735-745`;Java `Client.java:752,863-875`;JS `client.ts:207,304-311`。 - **文档依据**:DEVELOPMENT 6.8;9 第 6 条“收到 fatal……事件 auth_failed 带原因”;5“原因 taken_over”。 - **为何不是故意设计**:没有偏差记录;JS 已经按同步方式处理。 - **解决方案**: 1. 在收包路径识别 fatal 后立即置停止标志、失败发送队列和所有挂起请求、只上报一次原因、异步停止传输(Go 还要设 transport 的 stopped);之后到来的 CONNACK 拒绝不再覆盖原因。 2. Go 的状态事件改为有序派发。 3. Go/JS 顶号原因改为 `taken_over`。 - **改动文件**:Go `receive.go`、`connect.go`、`transport_mqtt.go`;Python `client.py`;Java `Client.java`;JS `client.ts` - **与其他模块的交互/冲突风险**:和 S-5、S-8 同文件。 - **需补测试**:处理线程阻塞时注入 fatal 加断线,断言只出现一次正确原因、没有重连尝试;真机上停用、删除、重置密码分别断言原因为 disabled、deleted、password_reset。 - **置信度**:代码阅读确定;时序竞态部分为“较高”。 #### [S-13] 断线时非发送请求不失败、JS 请求无超时;Go / JS 的 logout 不收尾(Go / JS;Python / Java 部分) - **严重级**:medium - **分类**:协议一致性 - **现象与影响**: - 断线时挂起中的 recall、群操作等请求:JS 永不结束,Go 只能等调用方 ctx,Python/Java 要等满 60 秒;Python/Java 的 down 线程还会卡在 ack 上。 - Go 的 `Logout` 不失败发送队列、不停止传输、不发 offline 事件;JS 的 `logout` 不失败队列、不发 offline 事件。 - **证据**:JS `client.ts:413-437,718-725`;Go `receive.go:322-337`、`connect.go:232-245`;Python `client.py:913`;Java `Client.java:1055`。 - **文档依据**:PRD F19“除发送以外的请求在离线时直接失败”;DEVELOPMENT 9“停止重连时队列里的发送全部以对应错误结束”。 - **为何不是故意设计**:没有偏差记录,Python/Java 的 logout 已经收尾。 - **解决方案**:断线钩子里以 not_connected 失败非发送请求;Go/JS 默认请求超时 60 秒;logout 做和 close 相同的收尾。 - **改动文件**:Go `receive.go`、`connect.go`;JS `client.ts`;Python `client.py`、Java `Client.java`(断线失败挂起请求) - **与其他模块的交互/冲突风险**:和 S-1 同一断线钩子。 - **需补测试**:recall 未得到响应时断线,断言立即报错;logout 后断言队列清空并发出 offline。 - **置信度**:代码阅读确定。 #### [S-14] 发送收到 rate_limited 后重交没有退避(Python / Java 热循环;Go / JS 固定 1 秒) - **严重级**:medium - **分类**:协议一致性 - **现象与影响**:Python/Java 清空 rid 后立即唤醒发送泵,形成按往返时间计的重试风暴,持续打满服务器令牌桶。Go/JS 固定 1 秒重试,不会随连续限流拉长。 - **证据**:Python `client.py:682-689`;Java `Client.java:797-805`;Go `send.go:186-195`;JS `client.ts:528-533`。 - **文档依据**:DEVELOPMENT 9“按退避自动重交”。 - **为何不是故意设计**:没有偏差记录,文档明确要求退避。 - **解决方案**:每条发送记录连续限流次数,按 1 秒起、翻倍、上限 30 秒、±30% 抖动退避,成功后清零。 - **改动文件**:四套 send/pump 相关文件 - **与其他模块的交互/冲突风险**:无。 - **需补测试**:连续回 3 次 rate_limited,断言重发间隔约为 1、2、4 秒。 - **置信度**:代码阅读确定。 #### [S-19] 本地整帧检查有缺口(四套) - **严重级**:low - **分类**:协议一致性 - **现象与影响**:JS 用字符串长度(UTF-16 码元)比较字节上限;四套在离线时入队的帧都不检查,握手后也不复查。超限帧会被服务器直接断开,重连后重交同一帧,形成无限循环。 - **证据**:JS `client.ts:478-482`;Go `send.go:96-102`。 - **文档依据**:DEVELOPMENT 9 本地检查一段。 - **为何不是故意设计**:文档明确要求本地拦截,正是为了避免这个循环。 - **解决方案**:JS 按 UTF-8 字节计长;四套在入队和 drain 前都按握手给出的 max_frame_bytes 检查。 - **改动文件**:四套 send 相关文件 - **与其他模块的交互/冲突风险**:无。 - **需补测试**:离线时发送超大 meta 应报 frame_too_large;用中文 meta 构造“UTF-16 未超、UTF-8 超限”的用例。 - **置信度**:代码阅读确定。 #### [S-20] Go / JS 的回执去重集合没有上限(Go / JS) - **严重级**:low - **分类**:数据(资源) - **现象与影响**:去重集合只增不删,长期运行的发送端内存持续增长。 - **证据**:Go `client.go:72`、`receive.go:201-208`;JS `client.ts:93`。 - **文档依据**:DEVELOPMENT 9 去重容量 10000 的约定(Python/Java 已按此为回执设上限)。 - **为何不是故意设计**:Python/Java 已按 10000 条设上限。 - **解决方案**:改为 10000 条的 LRU。 - **改动文件**:Go `client.go`、`receive.go`;JS `client.ts` - **与其他模块的交互/冲突风险**:无。 - **需补测试**:注入 10001 个不同回执,断言集合大小不超过 10000。 - **置信度**:代码阅读确定。 #### [S-21] JS 撤回事件可能重复、已撤回的消息仍交给应用(JS;手动模式四套都可能重复) - **严重级**:low - **分类**:协议一致性 - **现象与影响**:JS 回调还没执行时收到 revoked,会先发一次撤回事件,但回调照样执行;自动 ack 的结果又触发第二次撤回事件。手动模式下,四套在 revoked 之后应用再调 ack 时,都会再发一次撤回事件。 - **证据**:JS `client.ts:323-360,378-411`;Go `receive.go:157-185`;Python `client.py:836-848`;Java `Client.java:965-977`。 - **文档依据**:DEVELOPMENT 6.4“还没交给应用,直接丢弃”。 - **为何不是故意设计**:文档要求直接丢弃。 - **解决方案**:去重表增加 revoked 状态,撤回后跳过回调和 ack,并避免重复发事件。 - **改动文件**:JS `client.ts`;手动模式四套同改 - **与其他模块的交互/冲突风险**:无。 - **需补测试**:msg 后紧跟 revoked,断言回调 0 次、撤回事件 1 次。 - **置信度**:代码阅读确定。 #### [S-22] JS close() 后定时器没有清理,Node 进程约 60 秒才退出(JS) - **严重级**:low - **分类**:质量 - **现象与影响**:退避对象的 60 秒定时器和连接超时定时器在 close 时都没有清理。 - **证据**: - `types.ts:184-195`、`mqtt.ts:130-132`。 - 复现:close 1ms 就返回,但进程过了 60012ms 才退出。 - **文档依据**:无直接条款,属资源清理。 - **为何不是故意设计**:没有理由让进程在 close 后滞留。 - **解决方案**:close 时清理这两个定时器,并调用 `unref()`。 - **改动文件**:`types.ts`、`mqtt.ts`、`client.ts` - **与其他模块的交互/冲突风险**:无。 - **需补测试**:用子进程跑 connect+close,断言 2 秒内退出。 - **置信度**:已用测试复现。 --- <sub>复审基线:main `4059a15`(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。</sub>
nixevol added the P0-criticallane/sdkreview-2026-09-30 labels 2026-09-30 13:57:06 +08:00
Author
Owner

按第二轮 SDK 审查补充 2 条(重连退避双重翻倍、限速重交复用 rid)。第一轮把 JS 退避判为"基本符合"有误,K-00 (#57) 的退避约定已同步修订。补充条目已由总审查人对照代码核实,详见正文。

按第二轮 SDK 审查补充 2 条(重连退避双重翻倍、限速重交复用 rid)。第一轮把 JS 退避判为"基本符合"有误,K-00 (#57) 的退避约定已同步修订。补充条目已由总审查人对照代码核实,详见正文。
Author
Owner

已合入 origin/main 0c9b459。落地提交 55aa0cc fix: 按 K-00 约定修复 JS SDK 断线重交与退避 (#59)。

已合入 origin/main `0c9b459`。落地提交 `55aa0cc` fix: 按 K-00 约定修复 JS SDK 断线重交与退避 (#59)。
Sign in to join this conversation.