{"_id":"@aoctech/ws-client","_rev":"5-24729284ca335d05166f19e061486633","name":"@aoctech/ws-client","dist-tags":{"latest":"1.3.0"},"versions":{"1.0.0":{"name":"@aoctech/ws-client","version":"1.0.0","keywords":["websocket","react","hook","ctech"],"author":{"name":"Artur Oliveira"},"license":"MIT","_id":"@aoctech/ws-client@1.0.0","maintainers":[{"name":"aoctech","email":"dev@aoctech.app"}],"homepage":"https://github.com/artur-oliveira/ctech-ws-client#readme","bugs":{"url":"https://github.com/artur-oliveira/ctech-ws-client/issues"},"dist":{"shasum":"cf076fe274f6bf35fd19b8259ea0b885b2dc719a","tarball":"https://registry.npmjs.org/@aoctech/ws-client/-/ws-client-1.0.0.tgz","fileCount":7,"integrity":"sha512-hkJv4sYQBzYsahzJ5A4JZyvNaI3hckBDiMMvcG1lmRqyPRp9zywcb9PO9yjh/fD11O0IR37loPBcgEZk2WIZVQ==","signatures":[{"sig":"MEQCICQuopwsnG9IpwEnL+z2Z5gkQWMQjX2C99TtC+hf4zgvAiBE8B5tjUbTUXelEIhvS8bJBAkV3DiDh+Xu//MVABYd2A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":7594},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"e210552e71618fd8ef29576eadeb4fa6b85babb8","scripts":{"test":"npm run build && node --test","build":"tsc -p tsconfig.json"},"_npmUser":{"name":"aoctech","email":"dev@aoctech.app"},"repository":{"url":"git+https://github.com/artur-oliveira/ctech-ws-client.git","type":"git"},"_npmVersion":"11.16.0","description":"Resilient WebSocket React hook shared across CTech apps: app-level heartbeat, backoff reconnect, reconnect-on-token-refresh.","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.4","typescript":"^5.9.0","@types/react":"^19.2.0"},"peerDependencies":{"react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/ws-client_1.0.0_1784402157832_0.7129133378458512","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@aoctech/ws-client","version":"1.1.0","keywords":["websocket","react","hook","ctech"],"author":{"name":"Artur Oliveira"},"license":"MIT","_id":"@aoctech/ws-client@1.1.0","maintainers":[{"name":"aoctech","email":"dev@aoctech.app"}],"homepage":"https://github.com/artur-oliveira/ctech-ws-client#readme","bugs":{"url":"https://github.com/artur-oliveira/ctech-ws-client/issues"},"dist":{"shasum":"48ae184a8202a6a73a07897ecd09dbc743918c38","tarball":"https://registry.npmjs.org/@aoctech/ws-client/-/ws-client-1.1.0.tgz","fileCount":8,"integrity":"sha512-vUTvWmA3KOG5zxpx+ObUOzGpMOdofXLthGqvarbRKgiSeJrHV+904d6B30VWbcPmfeyGZRKeKUAsydjMeVn+0w==","signatures":[{"sig":"MEUCIH15SqYNA7S7xQsBUU5nWs8RwpJeT1j2AainRMpwDXjhAiEAwZeWjMpsnyYbsMEcoz7ZDm4deMaDFUgokHQLr+Q7M48=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aoctech%2fws-client@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":14539},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"3294f6bc9d06b5914c816416e773b49f230bbeb8","scripts":{"test":"npm run build && node --test","build":"tsc -p tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8d5675e0-4d5c-423c-8bc5-d85ec2ea3377"}},"repository":{"url":"git+https://github.com/artur-oliveira/ctech-ws-client.git","type":"git"},"_npmVersion":"11.16.0","description":"Resilient WebSocket React hook shared across CTech apps: app-level heartbeat, backoff reconnect, reconnect-on-token-refresh.","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.4","typescript":"^5.9.0","@types/react":"^19.2.0"},"peerDependencies":{"react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/ws-client_1.1.0_1784494437121_0.3929612113712173","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@aoctech/ws-client","version":"1.2.0","keywords":["websocket","react","hook","ctech"],"author":{"name":"Artur Oliveira"},"license":"MIT","_id":"@aoctech/ws-client@1.2.0","maintainers":[{"name":"aoctech","email":"dev@aoctech.app"}],"homepage":"https://github.com/artur-oliveira/ctech-ws-client#readme","bugs":{"url":"https://github.com/artur-oliveira/ctech-ws-client/issues"},"dist":{"shasum":"c27d88437648d879d8187d8e1f9c0c4d59cdff07","tarball":"https://registry.npmjs.org/@aoctech/ws-client/-/ws-client-1.2.0.tgz","fileCount":8,"integrity":"sha512-48HYHrhR93KRwVo2CS7s5NYuzLPVtPxXSYHzBZMSj0+QhjI1S/r//QrVJs8z1SSYLE/EuaWefh1KWrptapr7ag==","signatures":[{"sig":"MEYCIQD8a/a78MDkjr1/xMlIQnPNfahsP/vZPZsnvLThSNqRCQIhALM8EiroTsBMcobcxvy8iyTniZpp/hb6j+CS1f5ayQ0Q","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aoctech%2fws-client@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":16182},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"ce24728c748669755811f4aaea2e289d301e709b","scripts":{"test":"npm run build && node --test","build":"tsc -p tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8d5675e0-4d5c-423c-8bc5-d85ec2ea3377"}},"repository":{"url":"git+https://github.com/artur-oliveira/ctech-ws-client.git","type":"git"},"_npmVersion":"11.16.0","description":"Resilient WebSocket React hook shared across CTech apps: app-level heartbeat, backoff reconnect, reconnect-on-token-refresh.","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.4","typescript":"^5.9.0","@types/react":"^19.2.0"},"peerDependencies":{"react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/ws-client_1.2.0_1784720070536_0.4601427680228827","host":"s3://npm-registry-packages-npm-production"}},"1.2.1":{"name":"@aoctech/ws-client","version":"1.2.1","keywords":["websocket","react","hook","ctech"],"author":{"name":"Artur Oliveira"},"license":"MIT","_id":"@aoctech/ws-client@1.2.1","maintainers":[{"name":"aoctech","email":"dev@aoctech.app"}],"homepage":"https://github.com/artur-oliveira/ctech-ws-client#readme","bugs":{"url":"https://github.com/artur-oliveira/ctech-ws-client/issues"},"dist":{"shasum":"4f17dd31841b494705142ba24db9c670e03464f0","tarball":"https://registry.npmjs.org/@aoctech/ws-client/-/ws-client-1.2.1.tgz","fileCount":8,"integrity":"sha512-kdq09mZP91DWJORubLCJ+pSdZJBS8t+OwaTB7Q7V9bZSG8wxQggrhHV24PVf0ROViK1l6rdvEj4axg+QHojSLw==","signatures":[{"sig":"MEUCIFbs2TK4pndMvFUNt2xLiMry6dTZ7x7w+4ulodclE9/ZAiEAu8oTuf+yC2dWkfRGbWFuuNd1FPm/XNI7tx1u4Xp5A8I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aoctech%2fws-client@1.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":16182},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"ffa0b9d6a099980fe86c37289c38f4312b066a2f","scripts":{"test":"npm run build && node --test","build":"tsc -p tsconfig.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8d5675e0-4d5c-423c-8bc5-d85ec2ea3377"}},"repository":{"url":"git+https://github.com/artur-oliveira/ctech-ws-client.git","type":"git"},"_npmVersion":"11.16.0","description":"Resilient WebSocket React hook shared across CTech apps: app-level heartbeat, backoff reconnect, reconnect-on-token-refresh.","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.4","typescript":"^5.9.0","@types/react":"^19.2.0"},"peerDependencies":{"react":">=18"},"_npmOperationalInternal":{"tmp":"tmp/ws-client_1.2.1_1784727377364_0.025807015318047632","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@aoctech/ws-client","version":"1.3.0","description":"Resilient WebSocket React hook shared across CTech apps: app-level heartbeat, backoff reconnect, reconnect-on-token-refresh.","repository":{"type":"git","url":"git+https://github.com/artur-oliveira/ctech-ws-client.git"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"publishConfig":{"access":"public"},"peerDependencies":{"react":">=18"},"scripts":{"build":"tsc -p tsconfig.json","test":"npm run build && node --test"},"keywords":["websocket","react","hook","ctech"],"author":{"name":"Artur Oliveira"},"license":"MIT","devDependencies":{"@types/react":"^19.2.0","react":"^19.2.4","typescript":"^5.9.0"},"engines":{"node":">=24"},"gitHead":"7e82d915a313eb30cae844aabdc44cfdc9a09342","_id":"@aoctech/ws-client@1.3.0","bugs":{"url":"https://github.com/artur-oliveira/ctech-ws-client/issues"},"homepage":"https://github.com/artur-oliveira/ctech-ws-client#readme","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-2678ut/1WBoQiXV0gcunJsfjtJ2dbQUW3fc2HIZQAwbrsS6zi5ZkiZXOcfRbrUqaFRiUF0veS1uvUfch6Sne5A==","shasum":"521f26b8c863beafbce6a01357391be41a4491e4","tarball":"https://registry.npmjs.org/@aoctech/ws-client/-/ws-client-1.3.0.tgz","fileCount":8,"unpackedSize":17338,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@aoctech%2fws-client@1.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCiFNtJCDwlA01zo/klGeXYZVH68I1qokaILCgryj2yYAIhAP32iEJr9CIwoiljM0zlKck9YWvc1kw3QxBHQAH1j9KW"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8d5675e0-4d5c-423c-8bc5-d85ec2ea3377"}},"directories":{},"maintainers":[{"name":"aoctech","email":"dev@aoctech.app"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ws-client_1.3.0_1785080342932_0.9595696102875066"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-18T19:15:57.627Z","modified":"2026-07-26T15:39:03.354Z","1.0.0":"2026-07-18T19:15:57.982Z","1.1.0":"2026-07-19T20:53:57.247Z","1.2.0":"2026-07-22T11:34:30.669Z","1.2.1":"2026-07-22T13:36:17.504Z","1.3.0":"2026-07-26T15:39:03.067Z"},"bugs":{"url":"https://github.com/artur-oliveira/ctech-ws-client/issues"},"author":{"name":"Artur Oliveira"},"license":"MIT","homepage":"https://github.com/artur-oliveira/ctech-ws-client#readme","keywords":["websocket","react","hook","ctech"],"repository":{"type":"git","url":"git+https://github.com/artur-oliveira/ctech-ws-client.git"},"description":"Resilient WebSocket React hook shared across CTech apps: app-level heartbeat, backoff reconnect, reconnect-on-token-refresh.","maintainers":[{"name":"aoctech","email":"dev@aoctech.app"}],"readme":"# @aoctech/ws-client\n\n[![CI](https://github.com/artur-oliveira/ctech-ws-client/actions/workflows/ci.yml/badge.svg)](https://github.com/artur-oliveira/ctech-ws-client/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/@aoctech/ws-client)](https://www.npmjs.com/package/@aoctech/ws-client)\n\nResilient WebSocket React hook shared across CTech apps: app-level heartbeat, backoff reconnect,\nand immediate reconnect on a token refresh.\n\n> Repo name is `ctech-ws-client` on GitHub; published to npm as `@aoctech/ws-client`.\n> Searching by either name should land here.\n\n## Why this exists\n\n`ctech-dfe` and `ctech-wallet` each carried their own byte-identical copy of the same\n`useWebSocket` hook. Neither side verified its own ping/pong: the server sent an app-level JSON\nping every 30s and never checked for a reply, and the client replied but the server never noticed\nif it didn't — so a half-open connection (a server restart, a dropped TCP reset somewhere in the\nproxy chain) left the UI stuck showing \"connected\" indefinitely. A silent background token refresh\nalso never reconnected the socket, so it could keep sending a now-stale JWT until the connection\nhappened to drop for some other reason.\n\nThis package is the single implementation. The server side of the fix (native WS ping/pong control\nframes) lives in each app's own `ws.go` — a browser can't send those itself (see below) — but the\nclient-side heartbeat, backoff, and reconnect-on-token-change logic is here, once.\n\n## Install\n\n```bash\nnpm install @aoctech/ws-client\n```\n\n## Usage\n\n```tsx\nimport { useWebSocket } from \"@aoctech/ws-client\";\nimport { subscribeAccessToken } from \"@/lib/api/client\";\n\nconst { status } = useWebSocket({\n  url: wsUrl, // null disables the connection\n  onMessage: (data) => { /* handle a parsed JSON message */ },\n  enabled: !!wsUrl,\n  authToken: token,\n  // Reconnects immediately (no backoff) when a new token comes in — e.g. a\n  // silent OAuth refresh. Optional; omit if the app has no such notifier.\n  subscribeToken: subscribeAccessToken,\n});\n\n// status: 'disconnected' | 'connecting' | 'reconnecting' | 'connected' | 'error'\n```\n\n## How the heartbeat works\n\nA browser's WebSocket API gives JavaScript no way to send a native ping control frame — only the\nbrowser itself answers a server-sent one, transparently, per RFC 6455. So the two directions use\ndifferent mechanisms:\n\n- **Server → client:** the server sends a native WS ping periodically and enforces a read deadline\n  via `SetPongHandler`. The browser answers it automatically; this hook has no code for it at all.\n- **Client → server:** every `CLIENT_PING_INTERVAL_MS` (20s) the hook sends an app-level\n  `{\"type\":\"ping\"}` text frame and arms a `CLIENT_PONG_TIMEOUT_MS` (10s) timer. If the server's own\n  `{\"type\":\"pong\"}` reply doesn't arrive in time, the hook closes the socket — the existing\n  backoff-reconnect path takes it from there. The server must reply to this explicitly; it's not\n  automatic like the native direction.\n\n## API\n\n- `useWebSocket(options): { status, attempt, send, reconnect }` — see Usage above.\n- `status: WSStatus` — `disconnected` | `connecting` | `reconnecting` | `connected` | `error`.\n- `attempt: number` — reconnect attempts since the last successful open (capped at\n  `MAX_RECONNECT_ATTEMPTS`). Reset to `0` on open.\n- `send(value: object): boolean` — sends a JSON-encoded frame if the socket is open; returns\n  `false` (and is a no-op) when not connected. Use it for app frames like `act`, `chat`, `ready`.\n- `reconnect(): void` — forces an immediate reconnect with no backoff, the same path a token\n  refresh takes. Wire a \"Reconnect now\" button to it.\n- `onOpen?: () => void` — option fired once after the socket opens and the auth token frame is\n  sent. Put a post-auth follow-up frame here (e.g. a ping that makes the server run a reconnect\n  command) instead of racing the open event.\n- `type WSStatus = 'disconnected' | 'connecting' | 'reconnecting' | 'connected' | 'error'`\n- `nextBackoffDelay(attempt)`, `isPongMessage(data)` — the pure helpers behind the hook, exported\n  standalone for testing.\n- `BASE_DELAY_MS`, `MAX_DELAY_MS`, `MAX_RECONNECT_ATTEMPTS`, `CLIENT_PING_INTERVAL_MS`,\n  `CLIENT_PONG_TIMEOUT_MS` — the tuning constants above.\n\n## Development\n\n```bash\nnpm run build   # tsc -> dist/\nnpm test        # build + node's built-in test runner\n```\n\nThe hook itself has no test in this repo (rendering a hook needs either a DOM/RTL harness this repo\nhas no other use for, or `react-test-renderer`, which React 19 deprecates). Its behavior is tested\nin the consuming apps instead, using their existing Vitest+RTL setups — see\n`ctech-dfe/ui/src/__tests__/lib/useRealtimeUpdates.test.tsx`.\n\n## Releasing\n\n`publish.yml` only fires on a published GitHub Release — a push to `main` alone never publishes\n(it only runs `ci.yml`, which tests). Publishing uses npm's OIDC trusted publishing, so there's no\n`NPM_TOKEN` secret to manage; provenance is generated automatically.\n\n```bash\n# 1. Bump \"version\" in package.json, then commit and push as usual\ngit commit -am \"chore: release vX.Y.Z\"\ngit push\n\n# 2. Tag it and push the tag\ngit tag vX.Y.Z\ngit push --tags\n\n# 3. Cut the release — this is what actually triggers the publish workflow\ngh release create vX.Y.Z --generate-notes\n```\n\n## License\n\nMIT\n\n## Implementation reference (file:line) — audited\n\nAnchors into `src/` (`src/index.ts:1-2` re-exports `heartbeat` + `useWebSocket`):\n\n- `useWebSocket(options)` — `src/useWebSocket.ts:45`. Options `UseWebSocketOptions` `:14`\n  (`url`, `onMessage`, `enabled?`, `authToken?` first-frame JWT, `subscribeToken?` token-change →\n  immediate reconnect, `onOpen?`); result `UseWebSocketResult` `:35` (`status` `WSStatus` `:12`,\n  `attempt`, `send` `:174`, `reconnect` `:164`).\n- First-frame auth: on open, sends `{\"token\": <jwt>}` if `authToken` set — `src/useWebSocket.ts:118-124`.\n- Heartbeat: app-level `{\"type\":\"ping\"}` every 20s, 10s pong timeout → close on miss —\n  `src/useWebSocket.ts:98-104`.\n- Heartbeat helpers/constants — `src/heartbeat.ts`: `nextBackoffDelay` `:12`, `isPongMessage` `:16`,\n  `BASE_DELAY_MS` `:1`, `MAX_DELAY_MS` `:2`, `MAX_RECONNECT_ATTEMPTS` `:3`, `CLIENT_PING_INTERVAL_MS`\n  `:9`, `CLIENT_PONG_TIMEOUT_MS` `:10`.\n- No in-repo hook test (see README \"Development\"); behavior is covered in consuming apps.\n\n","readmeFilename":"README.md"}