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

gz-shared-websocket

v0.1.1

Published

SharedWorker-first WebSocket client with React hooks and a direct fallback.

Readme

gz-shared-websocket

SharedWorker 优先的浏览器 WebSocket SDK。它把同源页面、Garfish 子应用和同源 iframe 对同一个 connectionKey 的订阅合并到一条物理 WebSocket,并为 React 18/19 提供统一 Hook。SharedWorker 不可用或初始化失败时,SDK 会降级到 react-use-websocket 页面直连。

特性

  • SharedWorker 内使用原生 WebSocket,集中处理鉴权、应用层心跳、重连与订阅恢复。
  • connectionKey 隔离用户、租户和服务,禁止冲突配置复用连接。
  • topic -> Client 精确路由,同一页面内对重复 topic 做引用计数。
  • token 只保存在内存中;更旧的 token 版本不会覆盖新版本。
  • 业务消息默认不在断线期间排队;显式排队需要幂等键。
  • 离线队列最多 100 条或 1 MiB,达到任一限制即拒绝新消息。
  • React Strict Mode 安全,支持 React 18 和 React 19。
  • ESM、CommonJS、TypeScript declarations 和独立子路径导出。
  • 模块顶层不访问浏览器全局,SSR 构建阶段可以安全导入。

安装

npm install gz-shared-websocket react react-dom

reactreact-dom 是 peer dependencies,支持范围为 >=18 <20react-use-websocket 是包内 Direct Transport 的运行时依赖,无需业务项目重复声明。

本地交互 Demo

仓库内提供了一个可直接运行的 React + SharedWorker + 模拟 WebSocket 服务端 Demo:

npm run demo

打开 http://127.0.0.1:4174。点击“打开第二个页面”,在两个页面分别选择不同 topic,即可观察服务端的“物理连接数”保持为 1,而“订阅并集”包含两个页面的 topic。Demo 还可验证 Token 刷新、服务端断开后的重连、心跳超时和直接连接降级;后者使用 http://127.0.0.1:4174/?direct=1

架构

同源页面 / Garfish 子应用 / 同源 iframe
  -> createSharedWebSocketClient().useWebSocket()
  -> 页面级 PageClient(单 MessagePort + topic 引用计数)
  -> 应用自定义 SharedWorker 入口
  -> gz-shared-websocket Worker runtime
  -> 每个 connectionKey 一条原生 WebSocket

SharedWorker 不支持或初始化失败
  -> react-use-websocket(share: true)
  -> 页面级 WebSocket

普通网络中断、服务端临时不可用、心跳超时、鉴权失败和连接配置冲突不会触发 transport 降级,避免共享连接与直连同时存在。

1. 实现业务协议适配器

Worker 不能通过 MessagePort 接收函数。页面配置只向 Worker 传递可结构化克隆的 protocolId;实际 adapter 必须在应用自己的 Worker 入口中注册。

import type {
  ProtocolMessage,
  SocketData,
  WebSocketProtocolAdapter,
} from 'gz-shared-websocket/protocol';

type IncomingMessage =
  | { type: 'auth-success' }
  | { type: 'auth-failure'; message?: string }
  | { type: 'pong'; timestamp: number }
  | { type: 'ping'; timestamp: number }
  | { type: 'topic'; topic: string; payload: unknown }
  | { type: 'system'; payload: unknown };

type OutgoingMessage =
  | { type: 'auth'; token: string }
  | { type: 'ping'; timestamp: number }
  | { type: 'pong'; timestamp: number }
  | { type: 'subscribe'; topics: string[] }
  | { type: 'unsubscribe'; topics: string[] };

export const omsProtocolAdapter: WebSocketProtocolAdapter<IncomingMessage, OutgoingMessage> = {
  createAuthMessage: (token) => ({ type: 'auth', token }),
  createPingMessage: (timestamp) => ({ type: 'ping', timestamp }),
  createPongMessage: (timestamp) => ({ type: 'pong', timestamp }),
  createSubscribeMessage: (topics) => ({ type: 'subscribe', topics }),
  createUnsubscribeMessage: (topics) => ({ type: 'unsubscribe', topics }),
  encode: (message) => JSON.stringify(message),
  decode: (data: SocketData) => {
    if (typeof data !== 'string') throw new TypeError('Expected JSON text');
    return JSON.parse(data) as IncomingMessage;
  },
  classify: (message): ProtocolMessage => {
    switch (message.type) {
      case 'auth-success':
      case 'auth-failure':
      case 'pong':
        return message;
      case 'ping':
        return { type: 'server-ping', timestamp: message.timestamp };
      case 'topic':
        return {
          type: 'topic-message',
          topic: message.topic,
          payload: message.payload,
        };
      case 'system':
        return { type: 'system-message', payload: message.payload };
    }
  },
};

完整、可处理 stringArrayBufferBlob 的示例见 examples/omsProtocolAdapter.ts

2. 创建并构建应用 Worker 入口

// src/workers/gz-shared-websocket.worker.ts
import { exposeSharedWebSocketWorker } from 'gz-shared-websocket/worker';
import { omsProtocolAdapter } from '../realtime/omsProtocolAdapter';

exposeSharedWebSocketWorker({
  protocolVersion: 1,
  protocols: {
    'oms-v1': omsProtocolAdapter,
  },
});

必须把这个入口构建为单一 ESM 文件,并部署到固定、同源、无 hash 的地址,例如:

/shared-workers/gz-shared-websocket.worker.js

所有需要共享连接的主应用、Garfish 子应用和同源 iframe 必须使用相同的 Worker URL、Worker name、Worker type 和协议版本。不要让各子应用分别产出不同 hash 的 Worker URL。

以 Vite 为例,可以为 Worker 维护一个独立构建配置:

// vite.worker.config.ts
import { resolve } from 'node:path';
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    outDir: 'dist/shared-workers',
    emptyOutDir: false,
    lib: {
      entry: resolve(__dirname, 'src/workers/gz-shared-websocket.worker.ts'),
      formats: ['es'],
      fileName: () => 'gz-shared-websocket.worker.js',
    },
  },
});

npm 包发布的是协议无关的 Worker runtime,不会发布绑定某个业务协议的可执行 Worker 文件。

3. 创建页面 Client

import { createSharedWebSocketClient } from 'gz-shared-websocket';
import { omsProtocolAdapter } from './omsProtocolAdapter';

export const omsWebSocket = createSharedWebSocketClient({
  url: '/api/realtime',
  workerUrl: '/shared-workers/gz-shared-websocket.worker.js',
  workerName: 'gz-shared-websocket-v1',
  protocolVersion: 1,
  protocolId: 'oms-v1',
  directProtocol: omsProtocolAdapter,
  heartbeat: {
    interval: 25_000,
    timeout: 10_000,
  },
  reconnect: {
    initialDelay: 1_000,
    maxDelay: 30_000,
    jitter: 0.2,
  },
});

directProtocol 只在页面直连降级通道执行;SharedWorker 主通道根据 protocolId 使用 Worker 入口中注册的 adapter。

4. 更新鉴权

鉴权不作为每个 Hook 的参数。登录或刷新 token 后,通过页面 Client 统一更新:

omsWebSocket.updateAuth({
  connectionKey: 'oms:tenant-a:user-1001',
  token,
  version: tokenIssuedAt,
});

version 必须单调递增。SDK 会忽略相同或更旧版本,避免多个页面并发刷新时 token 回退。token 不会写入 localStorage、日志、connectionKey 或诊断字段。

退出登录或切换用户:

omsWebSocket.clearAuth('oms:tenant-a:user-1001');

切换用户时必须使用新的 connectionKey

5. React Hook

function MarketPanel() {
  const {
    status,
    readyState,
    transport,
    lastMessage,
    lastJsonMessage,
    error,
    diagnostics,
    sendMessage,
    sendJsonMessage,
    reconnect,
  } = omsWebSocket.useWebSocket<{ price: number }>({
    connectionKey: 'oms:tenant-a:user-1001',
    topics: ['market.SH.600000', 'order.user-1001'],
    enabled: true,
    onMessage(message) {
      console.log(message.type, message.payload);
    },
    onError(currentError) {
      console.error(currentError.code);
    },
  });

  return (
    <section>
      <div>{status}</div>
      <div>{transport}</div>
      <div>{lastJsonMessage?.price}</div>
    </section>
  );
}

topics 变化时自动计算差异,组件卸载时自动释放引用。enabled: false 不订阅 topic,也不会维持连接。

不提供 getWebSocket():SharedWorker 模式的物理 socket 不在页面主线程,业务代码不应直接关闭或修改共享连接。

6. 安全发送与离线队列

断线时默认拒绝业务发送:

sendJsonMessage(
  { type: 'place-order', order },
  {
    queueIfDisconnected: false,
  },
);

只有服务端能够按幂等键去重的消息才允许排队:

sendJsonMessage(
  { type: 'refresh-snapshot', requestId },
  {
    queueIfDisconnected: true,
    idempotencyKey: requestId,
  },
);

规则:

  • 最多 100 条或 1 MiB。
  • 超限拒绝新消息,不静默淘汰旧消息。
  • 鉴权成功后按 FIFO 发送。
  • 页面断开时移除该页面拥有的排队消息。
  • 退出登录、清除鉴权或销毁 Client 时清空。
  • react-use-websocket 的内部预连接队列被显式关闭,Direct Transport 使用相同安全策略。

7. Transport 降级

允许降级到 direct 的错误:

  • WORKER_UNSUPPORTED
  • WORKER_CONSTRUCTION_FAILED
  • WORKER_INIT_TIMEOUT
  • WORKER_DISCONNECTED
  • PROTOCOL_VERSION_MISMATCH
  • PROTOCOL_NOT_REGISTERED

不会降级:

  • 普通 socket/network close
  • SOCKET_ERROR
  • HEARTBEAT_TIMEOUT
  • AUTH_FAILED
  • CONNECTION_CONFIG_CONFLICT
  • topic 订阅失败
  • 服务端消息解析失败

transport 返回 shared-workerdirect,可以用于诊断环境是否发生降级。

8. Garfish、iframe 和独立 WebSocket

  • 同源 Garfish 子应用可以连接同一个固定 Worker。
  • 同源 iframe 可以连接同一个固定 Worker。
  • 跨域 iframe 无法直接共享主页面的 SharedWorker,会进入页面直连。
  • 普通窗口和无痕窗口不会共享。
  • 未接入本包、自己执行 new WebSocket() 的子应用完全不受影响;它们只会继续占用独立连接和独立订阅。

SharedWorker 是资源共享机制,不是 topic 权限边界。服务端仍必须对 token、topic 和业务操作逐项鉴权。

9. 默认策略

| 能力 | 默认值 | | ------------------ | ------ | | 服务端 ping 间隔 | 25 秒 | | pong 超时 | 10 秒 | | 页面租约心跳 | 20 秒 | | 页面租约过期 | 60 秒 | | 无 Client 关闭宽限 | 5 秒 | | 初始重连延迟 | 1 秒 | | 最大重连基准延迟 | 30 秒 | | 重连抖动 | ±20% | | 稳定后重置重连次数 | 30 秒 | | 队列条数 | 100 | | 队列字节数 | 1 MiB | | Worker 初始化超时 | 5 秒 |

10. 子路径导出

import { createSharedWebSocketClient } from 'gz-shared-websocket';
import { useSharedWebSocket } from 'gz-shared-websocket/react';
import type { WebSocketProtocolAdapter } from 'gz-shared-websocket/protocol';
import { exposeSharedWebSocketWorker } from 'gz-shared-websocket/worker';

./protocol./worker 不依赖 React。根入口和 ./react 提供 React 集成。

11. 浏览器与运行环境

本包采用能力检测,不以浏览器名称猜测:

  • 存在且能够初始化 SharedWorker:使用共享通道。
  • 不存在、被 CSP 阻止、跨域加载失败或握手不兼容:使用 direct。
  • 浏览器必须支持 WebSocket、MessagePort、structured clone、ES2020 和 React 18/19 所需能力。
  • SSR 期间可以导入包,但连接只会在浏览器 Hook effect 中建立。

真实的跨页面共享行为应在目标浏览器、CSP、Garfish 和 iframe 部署形态下做验收。

开发

npm install
npm run typecheck
npm test
npm run lint
npm run format:check
npm run build
npm run check:package
npm pack --dry-run

完整设计与实施计划:

License

MIT