{"_id":"@arubiku/pulse-lib","_rev":"2-07366e6cf328c89ce798176a843bf991","name":"@arubiku/pulse-lib","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@arubiku/pulse-lib","version":"1.0.0","_id":"@arubiku/pulse-lib@1.0.0","maintainers":[{"name":"arubiku","email":"ggnub.studio@gmail.com"}],"dist":{"shasum":"ce7d94bcd314c3da194778be37b9a4a29a5a07fc","tarball":"https://registry.npmjs.org/@arubiku/pulse-lib/-/pulse-lib-1.0.0.tgz","fileCount":43,"integrity":"sha512-UyRzixPpfNff4Qxkdkp62B9CK8c8lZ5QjOswalE8sWpe5m/WFO0ehwruUXbrmOt3hHD0XVa+fYWyTxvDUGGyCA==","signatures":[{"sig":"MEUCIG5vB0LiBhoujM2QG3jSGSsCidsaUYDw7xWfuE7RENjxAiEAp3/g0avfafpNCxopsVAvJqUYKpr6XR0DD5S08rXjgxM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":97751},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.mjs","require":"./dist/react.js"},"./store":{"types":"./dist/store.d.ts","import":"./dist/store.mjs","require":"./dist/store.js"},"./zustand":{"types":"./dist/zustand.d.ts","import":"./dist/zustand.mjs","require":"./dist/zustand.js"},"./protocol":{"types":"./dist/protocol.d.ts","import":"./dist/protocol.mjs","require":"./dist/protocol.js"}},"private":false,"scripts":{"dev":"tsup src/index.ts src/react.ts src/zustand.ts src/protocol.ts src/store.ts --format cjs,esm --dts --watch","bump":"node bump.js","build":"tsup src/index.ts src/react.ts src/zustand.ts src/protocol.ts src/store.ts --format cjs,esm --dts","typecheck":"tsc --noEmit","release:auto":"npm version patch --no-git-tag-version && npm run deploy:publish","deploy:publish":"npm run build && npm run npm:auth:check && npm publish --access public --userconfig .npmrc","npm:auth:check":"npm whoami --userconfig .npmrc","bump:major:deploy:publish":"node bump.js major && npm run deploy:publish","bump:minor:deploy:publish":"node bump.js minor && npm run deploy:publish","bump:patch:deploy:publish":"node bump.js patch && npm run deploy:publish"},"_npmUser":{"name":"arubiku","email":"ggnub.studio@gmail.com"},"_npmVersion":"10.9.2","description":"Library for generating Pulse Auth Tickets and handling client sync","directories":{},"sideEffects":false,"_nodeVersion":"22.13.1","dependencies":{"jose":"^5.2.3"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.2","react":"^18.2.0","zustand":"^4.5.2","typescript":"^5.4.3","@types/node":"^22.13.10","@types/react":"^18.2.73"},"peerDependencies":{"react":"^18.0.0","zustand":"^4.5.2"},"peerDependenciesMeta":{"react":{"optional":true},"zustand":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/pulse-lib_1.0.0_1775188020430_0.766348428297833","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@arubiku/pulse-lib","version":"1.0.1","private":false,"type":"module","description":"Library for generating Pulse Auth Tickets and handling client sync","main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./protocol":{"types":"./dist/protocol.d.ts","import":"./dist/protocol.mjs","require":"./dist/protocol.js"},"./store":{"types":"./dist/store.d.ts","import":"./dist/store.mjs","require":"./dist/store.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.mjs","require":"./dist/react.js"},"./zustand":{"types":"./dist/zustand.d.ts","import":"./dist/zustand.mjs","require":"./dist/zustand.js"}},"scripts":{"build":"tsup src/index.ts src/react.ts src/zustand.ts src/protocol.ts src/store.ts --format cjs,esm --dts","typecheck":"tsc --noEmit","dev":"tsup src/index.ts src/react.ts src/zustand.ts src/protocol.ts src/store.ts --format cjs,esm --dts --watch","bump":"node bump.js","npm:auth:check":"npm whoami --userconfig .npmrc","deploy:publish":"npm run build && npm run npm:auth:check && npm publish --access public --userconfig .npmrc","release:auto":"npm version patch --no-git-tag-version && npm run deploy:publish","bump:patch:deploy:publish":"node bump.js patch && npm run deploy:publish","bump:minor:deploy:publish":"node bump.js minor && npm run deploy:publish","bump:major:deploy:publish":"node bump.js major && npm run deploy:publish"},"dependencies":{"jose":"^5.2.3"},"peerDependencies":{"react":"^18.0.0","zustand":"^4.5.2"},"peerDependenciesMeta":{"react":{"optional":true},"zustand":{"optional":true}},"devDependencies":{"@types/node":"^22.13.10","@types/react":"^18.2.73","react":"^18.2.0","tsup":"^8.0.2","typescript":"^5.4.3","zustand":"^4.5.2"},"sideEffects":false,"engines":{"node":">=20"},"_id":"@arubiku/pulse-lib@1.0.1","gitHead":"42bdbd0556ce7e69241527b12027de8f878ee895","_nodeVersion":"22.13.1","_npmVersion":"10.9.2","dist":{"integrity":"sha512-28c+5350Z9hkfDdTIUVRT2rt5cv7cvFQsLsn3DFtLEx/craG8MiWcv8Hzlf4BigB7Y7E+rAcnP4nT3A4IqX2vg==","shasum":"b176a7acabec83b093a86f7ff30b113a8243340c","tarball":"https://registry.npmjs.org/@arubiku/pulse-lib/-/pulse-lib-1.0.1.tgz","fileCount":48,"unpackedSize":155760,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCFeLgXUrlq0s/wJ2eeypRci60rk7OYlv5vQh2qvYPs9QIgceUMi5jiEekr4iFGoez5suElEBOcFlJDAsQmLMbnDHM="}]},"_npmUser":{"name":"arubiku","email":"ggnub.studio@gmail.com"},"directories":{},"maintainers":[{"name":"arubiku","email":"ggnub.studio@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pulse-lib_1.0.1_1775189496188_0.1704084685797156"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-03T03:47:00.270Z","modified":"2026-04-03T04:11:36.466Z","1.0.0":"2026-04-03T03:47:00.561Z","1.0.1":"2026-04-03T04:11:36.343Z"},"description":"Library for generating Pulse Auth Tickets and handling client sync","maintainers":[{"name":"arubiku","email":"ggnub.studio@gmail.com"}],"readme":"# @arubiku/pulse-lib\r\n\r\n![npm version](https://img.shields.io/npm/v/%40arubiku%2Fpulse-lib)\r\n![npm downloads](https://img.shields.io/npm/dm/%40arubiku%2Fpulse-lib)\r\n![runtime](https://img.shields.io/badge/runtime-browser%20%7C%20node%20%7C%20workers-0f172a)\r\n\r\nTyped auth helpers, a WebSocket client, framework adapters and a small state layer for apps that connect to Pulse rooms running on Cloudflare Workers.\r\n\r\nUse this package when you want to:\r\n\r\n- generate short-lived JWT tickets from your backend\r\n- connect clients to a Pulse Worker from vanilla JS\r\n- use React through `usePulse`\r\n- manage a shared connection with Zustand\r\n- build your own adapter on top of the framework-agnostic state store\r\n\r\n## Install\r\n\r\n```bash\r\nnpm install @arubiku/pulse-lib\r\n```\r\n\r\nOptional peers depending on your stack:\r\n\r\n```bash\r\nnpm install react zustand\r\n```\r\n\r\n## Entry points\r\n\r\n- `@arubiku/pulse-lib`\r\n  Auth helpers, base client and shared exports.\r\n\r\n- `@arubiku/pulse-lib/react`\r\n  React hook `usePulse`.\r\n\r\n- `@arubiku/pulse-lib/zustand`\r\n  Zustand vanilla store factory.\r\n\r\n- `@arubiku/pulse-lib/protocol`\r\n  Shared protocol types and snapshot shape.\r\n\r\n- `@arubiku/pulse-lib/store`\r\n  Framework-agnostic subscribable state store.\r\n\r\n## Quick start\r\n\r\n### 1. Generate a ticket on your backend\r\n\r\n```ts\r\nimport { generatePulseTicket } from '@arubiku/pulse-lib';\r\n\r\nconst token = await generatePulseTicket({\r\n  roomId: 'board-1',\r\n  userId: 'user-42',\r\n  secret: process.env.PULSE_SECRET!,\r\n  expiresIn: '15m',\r\n  features: {\r\n    presence: true,\r\n    presenceSync: true,\r\n    selfEcho: false,\r\n  },\r\n  metadata: {\r\n    name: 'Jane',\r\n    role: 'editor',\r\n  },\r\n});\r\n```\r\n\r\n### 2. Connect from the frontend\r\n\r\n```ts\r\nimport { PulseClient } from '@arubiku/pulse-lib';\r\n\r\nconst client = new PulseClient('https://your-worker.workers.dev', token, {\r\n  reconnectInterval: 1500,\r\n});\r\n\r\nclient.on('message', (message) => {\r\n  console.log(message);\r\n});\r\n\r\nclient.connect();\r\n```\r\n\r\n## Why Pulse instead of Ably?\r\n\r\nIf you already like Ably, the point of Pulse is not that Ably is bad. The point is control and economics.\r\n\r\nWhy teams may prefer Pulse:\r\n\r\n- your realtime layer runs in your own Cloudflare account\r\n- your auth model stays fully under your control through JWT tickets\r\n- your transport lives closer to users through Cloudflare's edge network\r\n- you can add your own rules, scopes, validation and room behavior without waiting for vendor features\r\n- your frontend and worker can stay in the same Cloudflare-centric architecture\r\n\r\nPractical difference in the free tier model:\r\n\r\n- managed realtime vendors like Ably usually gate free usage with explicit connection and message limits that can change over time by plan\r\n- with Pulse on Cloudflare Workers, the important limit is the Worker request quota and each new WebSocket handshake counts as a request\r\n- that means you are not paying or budgeting the same way as a per-message SaaS transport layer\r\n\r\nFor example, on Cloudflare Workers Free, the commonly relevant quota is on the order of `100k` requests per day, not per month. In a WebSocket setup that means up to `100k` new connection handshakes per day before you hit that specific quota. Existing sockets and message flow are a different cost model than a hosted Pub/Sub product. Always verify current Cloudflare and Ably pricing pages before quoting exact limits because plans change.\r\n\r\nLatency angle:\r\n\r\n- if your app already serves traffic through Cloudflare, Pulse can reduce extra network hops because the socket entrypoint is already on the edge\r\n- that usually gives you a better path for browser-to-edge communication than sending traffic first to a separate vendor platform and then back into your own stack\r\n\r\nChoose Pulse when you want:\r\n\r\n- a custom realtime layer inside your own infra\r\n- lower vendor dependency\r\n- Cloudflare-native deployment\r\n- control over auth and room semantics\r\n\r\nChoose Ably when you want:\r\n\r\n- a fully managed realtime product\r\n- built-in vendor features you do not want to maintain yourself\r\n- less infrastructure ownership in exchange for platform limits and pricing\r\n\r\n## API overview\r\n\r\n### `generatePulseTicket`\r\n\r\nCreates a signed JWT that `pulse-worker` can verify.\r\n\r\nSupported options:\r\n\r\n- `roomId`\r\n- `userId`\r\n- `secret`\r\n- `expiresIn`\r\n- `features`\r\n- `metadata`\r\n- `scopes`\r\n\r\n### `buildPulseWebSocketUrl`\r\n\r\nBuilds the final `wss://.../ws?token=...` URL from a base worker URL and a token.\r\n\r\n### `PulseClient`\r\n\r\nLow-level client with:\r\n\r\n- auto reconnect\r\n- reconnect backoff\r\n- reconnect jitter\r\n- pause-on-hidden and pause-on-offline behavior\r\n- conditional reconnect policies\r\n- application-level heartbeat support\r\n- offline queue\r\n- parser and serializer hooks\r\n- event listeners\r\n- subscribable snapshots\r\n- presence tracking\r\n- basic churn metrics in the connection snapshot\r\n\r\nUseful options for lowering reconnect churn:\r\n\r\n- `reconnectJitterRatio`\r\n- `pauseWhenHidden`\r\n- `pauseWhenOffline`\r\n- `heartbeatIntervalMs`\r\n- `heartbeatTimeoutMs`\r\n- `shouldReconnect`\r\n\r\n### `usePulse`\r\n\r\nReact adapter that wraps `PulseClient` and exposes:\r\n\r\n- connection `status`\r\n- `presenceMembers`\r\n- `lastMessage`\r\n- `lastPresence`\r\n- `lastSystem`\r\n- `lastError`\r\n- `send`\r\n- `sendRaw`\r\n\r\n### `createPulseStore`\r\n\r\nZustand adapter for a shared app-level connection.\r\n\r\n### `createPulseStateStore`\r\n\r\nFramework-neutral state layer useful for Astro islands, custom state managers or your own hooks.\r\n\r\n## Usage by stack\r\n\r\n### Vanilla JS\r\n\r\n```ts\r\nimport { PulseClient } from '@arubiku/pulse-lib';\r\n\r\nconst client = new PulseClient('https://your-worker.workers.dev', token);\r\nclient.connect();\r\n```\r\n\r\n### React\r\n\r\n```tsx\r\nimport { usePulse } from '@arubiku/pulse-lib/react';\r\n\r\nexport function Board({ token }: { token: string }) {\r\n  const { status, presenceMembers, send } = usePulse('https://your-worker.workers.dev', token);\r\n\r\n  return (\r\n    <button onClick={() => send({ type: 'update', entity: 'card', id: '1' })}>\r\n      {status} / {presenceMembers.length}\r\n    </button>\r\n  );\r\n}\r\n```\r\n\r\n### Zustand\r\n\r\n```ts\r\nimport { createPulseStore } from '@arubiku/pulse-lib/zustand';\r\n\r\nexport const pulseStore = createPulseStore();\r\npulseStore.getState().connect('https://your-worker.workers.dev', token);\r\n```\r\n\r\n### Custom adapter\r\n\r\n```ts\r\nimport { createPulseStateStore } from '@arubiku/pulse-lib/store';\r\n\r\nconst pulse = createPulseStateStore('https://your-worker.workers.dev', token);\r\npulse.connect();\r\n```\r\n\r\n## Event model\r\n\r\nThe client automatically recognizes three message groups coming from the worker:\r\n\r\n- `system`\r\n- `presence`\r\n- user messages\r\n\r\nPresence snapshots and incremental events update the internal `presenceMembers` list when `presenceTracking` is enabled.\r\n\r\nThe client snapshot also exposes light connection metrics such as scheduled reconnects, successful reconnects, heartbeat timeouts, queued messages, visibility state and online state.\r\n\r\n## Local development\r\n\r\nIf you are working inside this repo:\r\n\r\n```bash\r\nnpm install\r\nnpm run build\r\n```\r\n\r\n## Release flow\r\n\r\nThis package ships with a simple npm release flow similar to the CLI workflow used elsewhere in your workspace.\r\n\r\nAvailable scripts:\r\n\r\n- `npm run build`\r\n- `npm run typecheck`\r\n- `npm run npm:auth:check`\r\n- `npm run deploy:publish`\r\n- `npm run release:auto`\r\n- `npm run bump:patch:deploy:publish`\r\n- `npm run bump:minor:deploy:publish`\r\n- `npm run bump:major:deploy:publish`\r\n\r\n## Related repos\r\n\r\n- `pulse-worker`: Cloudflare Worker and Durable Object broker\r\n- `pulse-samples`: examples for native JS, server scripts, Astro, React and Zustand\r\n\r\n## Troubleshooting\r\n\r\n### `Invalid ticket`\r\n\r\nMake sure the backend signs the JWT with the same `PULSE_SECRET` configured in the worker.\r\n\r\n### No messages arrive\r\n\r\nCheck that both clients are connecting to the same `roomId` and the same deployed worker URL.\r\n\r\n### Reconnect churn is too high\r\n\r\nTry these first:\r\n\r\n- increase `reconnectInterval`\r\n- keep `reconnectJitterRatio` enabled\r\n- use `pauseWhenHidden` and `pauseWhenOffline`\r\n- enable `heartbeatIntervalMs` only when you actually need faster dead-socket detection\r\n- share one socket per app instead of one socket per component\r\n\r\n### Presence is empty\r\n\r\nEnable `features.presenceSync` in the token if you want an initial snapshot on connect.\r\n\r\n### My sender does not receive its own message\r\n\r\nThat is expected by default. Set `features.selfEcho = true` in the token if you want echo behavior.\r\n\r\n### Imports fail in React or Zustand\r\n\r\nInstall the corresponding peer dependencies in the consuming project:\r\n\r\n```bash\r\nnpm install react zustand\r\n```\r\n\r\n## Notes\r\n\r\nThis package intentionally does not own your business data. It only handles ticket generation, connection state and message transport helpers around the worker protocol.\r\n","readmeFilename":"README.md"}