npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kookapp/im-socket

v0.0.3

Published

Browser-first, resilient IM WebSocket state machine

Readme

@kookapp/im-socket

面向浏览器真实用户会话的 IM WebSocket 状态机。只负责连接、心跳、resume、严格 SN 顺序和生命周期,不包含 Bot、REST、鉴权请求或 KOOK 业务事件模型。

当前版本:0.1.0。已通过 PC 兼容门面接入 apps/webapp 的 Web Adapter;真实网关互通、浏览器矩阵、灰度和生产观测仍是上线门槛。

核心契约(先读这一段)

内核永不因为「失败次数够多」而放弃传输。

只有三种情况会停下来:

| 终态 | 触发 | 恢复方式 | | ---------- | --------------------------------------------------------------------------------------------- | ------------------------ | | stopped | 宿主 stop() | 宿主 start() | | fatal | 服务端明确判定身份无效(provider 抛 fatal: true 的错误,或 classifyClose 返回 'fatal') | 宿主重新登录后 start() | | disposed | 宿主 dispose() | 不可恢复 |

默认 recoveryStrategy: 'resume' 下,网络失败、网关失败、握手失败、心跳假死一律无限重试(带抖动的有上限指数退避)。Webapp Adapter 显式使用该策略:断连优先续会话,s:5、Resume 降级或无法修复的 SN 缺口会清理旧会话并自动重建,不会主动刷新页面。recoveryStrategy: 'reload' 仅作为宿主明确选择的可选能力保留。

会话语义失效(s:5 / resume 降级 / SN 缺口不可修复)走 hard_reset,它是迁移态不是终点:内核清掉 session 后自行重建,同时发 hardReset 事件让宿主决定业务侧要不要清 store / 重拉。需要先做完清理再放行的场景用 hardResetPolicy: 'manual'

运行时边界

  • 只依赖浏览器标准能力:WebSocketAbortControllerURLTextDecoderperformance.now、Promise 和 timer。
  • 不 import Node 内置模块,不依赖 ws,不读环境变量或文件系统。
  • 唯一运行时 npm 依赖是 pako(DEFLATE 解压)。
  • IMSocket 内核的 gateway 获取由宿主通过 gatewayProvider 注入;PC 兼容门面根据原生 start(config)get_gateway_api_url/http_headers/token 提供浏览器 fetch 适配。
  • 产物 target 为 ES2020。若宿主 browserslist 覆盖更老的浏览器,需要把本包纳入转译范围。

快速使用

import IMSocket from '@kookapp/im-socket'

const socket = new IMSocket({
  compression: true,
  gatewayProvider: async ({ signal, compression, resume, sessionId, lastSn }) => {
    const response = await getGatewayFromHost({ signal, compression, resume, sessionId, lastSn })
    return response.url
  },
  queryProvider: () => ({
    'x-client-machine-id': getMachineId(),
    'x-client-utm': getClientUtm(),
  }),
  // 只有服务端明确拒绝身份时才停止重试
  classifyClose: (event) => (event.code === 4001 ? 'fatal' : 'retry'),
})

socket.on('message', (frame) => {
  // 完整协议帧:{ s, sn, d },已按 SN 严格连续排序
  consumeOrderedFrame(frame)
})

socket.start() // 同步返回 void,不会抛网络错误,也不产生 unhandled rejection
await socket.waitUntilConnected(20_000) // 只表示「本次等待」超时,内核仍在后台重试

start() 返回 void。「开始运行」和「何时就绪」是两件事:后者用 waitUntilConnected()connected 事件观测。这样宿主写 socket.start() 不可能产生 unhandled rejection —— 旧设计里 s:5 这条正常协议路径会 reject 一个没人 catch 的 promise。

start() / stop() / dispose() 幂等。

PC SDK 兼容门面

import { ImSessionObject, kookImInitSdk } from '@kookapp/im-socket'

kookImInitSdk({})
const session = new ImSessionObject((eventName, eventData) => {
  // on_im_data_event / on_im_state_event / on_im_statistics_event /
  // on_im_resume_event / on_im_heartbeat_event
})

session.start({
  token,
  get_gateway_api_url: `${baseUrl}/api/v3/gateway/index`,
  http_headers: JSON.stringify(httpHeaders),
  ws_headers: JSON.stringify(wsHeaders),
  compress_type: 0,
  heartbeat_interval: 30,
})
session.setIp({ ip: '' }) // Web no-op,保留原生方法形态
session.stop()

门面状态与原生 KOOK_IM_STATE 一致:Init(0) / Started(1) / GetGatewayOK(2) / GatewayConnected(3) / Connected(4) / Timeout(5) / StopPending(6) / Stopped(7) / Failed(8) / Ended(9)。多个内部细状态映射到同一原生状态时不会重复回调。

宿主集成要求

1. 接 nudge()

window.addEventListener('online', () => socket.nudge('online'))
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') socket.nudge('visible')
})

nudge() 的语义是「时间可能已经流逝了,请按当前时刻重新评估 deadline」,并且有一条硬不变量:

只能让 deadline 提前,永远不能推迟、延长或新建义务。

探活路径天然自限流(发完即进入 awaiting_pong,此后调用无效);抢跑重连路径额外受 nudgeMinIntervalMs(默认 1 秒)限流 —— 抢跑本身就是在绕过退避,没有下限时,接在抖动事件上的 nudge() 可以形成不受退避约束的重连风暴。网络恢复后的第一次 nudge() 立即生效。

在这两条约束下它可以安全地接到任意高频事件上。它不修改 session 或 SN。

2. 用 outageStarted / outageEnded 驱动 UI,不要自己映射状态

socket.on('outageStarted', () => showReconnectOverlay())
socket.on('outageEnded', () => hideReconnectOverlay())

宿主真正需要的是「要不要动 UI」,不是原始状态迁移。让每个接入方自己去推导「哪些状态该清消息时间线」,早晚有人推错,而错的代价是每次网络抖动都清空全部消息 + 全量重拉

outageStarted 只在两种情况下触发:断连持续超过 outageThresholdMs(默认 3s),或需要刷新网关(快路径已失败)。首连期不算 outage —— 那是「连接中」,宿主自有首连看门狗。这个粒度对齐现网:WS 层带 resume 静默重连两次都失败、回退去重取 gateway 时才切 UI。

3. 其他

  • gatewayProvider 自己的请求设置并透传鉴权;尊重传入的 AbortSignal
  • 身份失效(401 / 封禁 / 异地登录)时,让 gatewayProvidernew IMSocketError(code, msg, { fatal: true }),或用 classifyClose 把对应 close code 判成 'fatal'内核不会靠失败次数推断身份失效
  • 监听 hardReset 做业务侧清理。默认策略下内核会自己重建连接,宿主不需要调用任何东西。
  • 不在 reconnecting 时清历史数据 —— 软 resume 只补 lastSn 之后的增量。
  • 上报 error 事件前用 error.toSafeJSON()originalError 可能含 gateway URL / token。

事件

| 事件 | 语义 | | ------------------------------- | ------------------------------------------------------------- | | connected | 首次 Hello 成功 | | recovered | ResumeAck 成功并恢复有序交付 | | reconnecting | 已排程一次重试,附 { reason, attempt, delayMs, willResume } | | hardReset | session 不可继续,附 { reason, code, autoRecovering } | | fatal | 身份被判定无效,内核已停止一切尝试 | | outageStarted / outageEnded | 面向宿主 UI 的派生断服信号 | | message | 已按 SN 严格连续排序的 s:0 完整帧 { s, sn, d } | | receive | 所有已解码帧,排序前发出(与 message 语义不同) | | unresponsive | 心跳尝试耗尽,即将进入恢复 | | error | 带稳定错误码的 IMSocketError | | statechange | 状态字段已更新后的通知 | | listenerError | 消费者监听器抛错或异步 reject | | unknownFrame | 结构合法但当前不处理的 signal(含入站 s:2 / s:4) |

消费者监听器的同步异常和异步 reject 均被隔离,不会中断 FSM。

同步字段与发送

  • sessionId:Hello 处理完成后同步可读;软重连全程保持(宿主的 x-client-sessionid 不会中断),hard reset / stop 后清空。
  • lastSn:最后一个已连续交付的业务 SN。
  • sendData() / sendFrame():成功入 WebSocket 返回 true,否则 false
  • 包不提供业务发送队列。KOOK 的这条 WS 事实上只收不发(除心跳)。

稳定性策略

  • 单调时钟:所有 deadline 走 performance.now()。用墙钟会在系统时钟回拨(NTP 修正 / 用户改时间 / 虚拟机恢复)时让心跳、探活、缺口检测全部停摆,而状态仍显示 connected。自定义 Clocknow() 必须单调
  • operation epoch:所有 provider continuation 和 socket 回调都要匹配当前代际;旧 Promise / 旧 socket / 旧 timer 不能推进新状态。close/error 双回调只触发一次恢复。
  • 退避重置需要健康证据:连接稳定存活 stabilityWindowMs(默认 30s)才重置退避与网关复用计数。握手成功不算 —— 否则「接受连接后立刻断开」的坏节点会让每个客户端以固定频率重连。
  • URL 保真:服务端下发的 gateway URL 逐字节透传,不做 URL/URLSearchParams 往返(会把 %20+~%7E 等重编码)。
  • SNsn <= lastSn 与重复直接丢弃;缺口不跨越交付;队列有硬上限;缺口超时通过 resume 重放。连续 maxGapRecoveryAttempts 轮 resume 仍未推进 lastSn 则升级为 hard reset,避免服务端真缺号时形成无限循环。队列溢出走同一条升级阶梯 —— 它和缺口是同一类问题(resume 没能把序列接上)。
  • 队列保留策略:只有队列溢出触发的恢复才清空队列;其他恢复保留缓存帧(服务端重放完整时它们会被去重丢弃,零成本;重放不完整时它们是唯一补救来源)。
  • 重入安全:每一处 transition() / emit() 之后还要继续改状态的地方都会重新校验状态。宿主在事件回调里同步 stop() / start() / nudge() / dispose() 不会留下孤儿 timer 或跨会话交付。
  • timer:每类关注点最多一个命名 timer,数量与连接时长无关。
  • 解码:binary/text 帧按实际内容自适应尝试压缩与非压缩解析;Blob-like 异步解码严格串行,不会反转到达顺序。

完整状态、时序和设计取舍见 architecture.md

开发验证

pnpm test        # 137 个确定性测试,含兼容门面、reload、属性测试与七天心跳耐久
pnpm typecheck   # src 与 tests 均纳入 strict 检查
pnpm build

测试分层:

  • tests/lifecycle.test.ts / protocol.test.ts / heartbeat-stability.test.ts —— 行为契约
  • tests/regression.test.ts —— 每条锁死一个曾被证实的缺陷(相对现网的倒退,或独立评审发现的问题)
  • tests/reentrancy.test.ts —— 宿主在事件回调里同步调用 start/stop/nudge/hardReset/dispose 的各条路径
  • tests/invariants.test.ts —— 确定性 PRNG 驱动的随机操作序列 + 混沌时钟,断言路径无关的不变量(活跃 socket ≤ 1、timer 有上界、SN 严格递增不重复、不存在「running 但既没连上也没有在途工作」的静默掉线