{"_id":"@ankit18193/pulse","name":"@ankit18193/pulse","dist-tags":{"latest":"0.4.0"},"versions":{"0.4.0":{"name":"@ankit18193/pulse","version":"0.4.0","description":"Pulse — Distributed Real-Time Messaging Infrastructure Server","type":"module","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./config":{"types":"./dist/config/index.d.ts","import":"./dist/config/index.js"},"./types":{"types":"./dist/types/index.d.ts","import":"./dist/types/index.js"},"./metrics":{"types":"./dist/metrics/index.d.ts","import":"./dist/metrics/index.js"},"./package.json":"./package.json"},"bin":{"pulse-server":"dist/bin/pulse-server.js"},"scripts":{"build":"tsc -p tsconfig.json","prepack":"npm run build","start":"node dist/bin/pulse-server.js","dev":"tsx watch src/bin/pulse-server.ts","test":"node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand --detectOpenHandles","test:coverage":"jest --coverage --runInBand","test:chaos":"jest --runInBand tests/chaos/","test:soak":"node --expose-gc --experimental-vm-modules ./node_modules/jest/bin/jest.js --config=jest.soak.config.js --runInBand --detectOpenHandles","test:package":"node --experimental-vm-modules ./node_modules/jest/bin/jest.js tests/package/ --runInBand --detectOpenHandles","bench":"tsx bin/pulse-bench.ts","chaos":"tsx bin/pulse-chaos.ts","dev:dashboard":"npm run dev --prefix dashboard","build:dashboard":"npm run build --prefix dashboard"},"keywords":["websocket","realtime","messaging","infrastructure","distributed-systems","redis","pubsub","presence"],"author":{"name":"Pulse Engineering"},"license":"Apache-2.0","engines":{"node":">=20.0.0"},"dependencies":{"@ankit18193/routex-gateway":"file:../../RouteX/RouteX","dotenv":"^16.4.7","ioredis":"^5.11.1","ws":"^8.18.0"},"devDependencies":{"@types/ioredis-mock":"^8.2.8","@types/jest":"^29.5.14","@types/node":"^22.13.5","@types/ws":"^8.5.14","ioredis-mock":"^8.13.1","jest":"^29.7.0","ts-jest":"^29.2.5","tsx":"^4.19.3","typescript":"^5.7.3"},"gitHead":"2d91d799841368f8ecd1f27b0f308a192aa295f7","_id":"@ankit18193/pulse@0.4.0","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-WSTLGKqZAhNp5vcm3k1pi+JcCE7T6WhnRvaD70M5/rSjKAE56XTNAL8yYVY1b8FsUmnYMo8ed3WFS4XytI93PA==","shasum":"aa0f9d57d1bb557cc5d499f0edaec45f260b2212","tarball":"https://registry.npmjs.org/@ankit18193/pulse/-/pulse-0.4.0.tgz","fileCount":153,"unpackedSize":779730,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDRwomJS4sKUVJeyE12YAZFvM790B6RoFBHrhF4q/TZwwIgOsqcYsc+XAqUm7mllWGxZOlJbvWGeWkDAYzQo5eTnrk="}]},"_npmUser":{"name":"ankit18193","email":"ankityadav18193@gmail.com"},"directories":{},"maintainers":[{"name":"ankit18193","email":"ankityadav18193@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pulse_0.4.0_1788945788569_0.7410823313305765"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-09T09:23:08.411Z","0.4.0":"2026-09-09T09:23:08.720Z","modified":"2026-09-09T09:23:08.964Z"},"maintainers":[{"name":"ankit18193","email":"ankityadav18193@gmail.com"}],"description":"Pulse — Distributed Real-Time Messaging Infrastructure Server","keywords":["websocket","realtime","messaging","infrastructure","distributed-systems","redis","pubsub","presence"],"author":{"name":"Pulse Engineering"},"license":"Apache-2.0","readme":"# @ankit18193/pulse\n\n> **Distributed Real-Time Messaging Infrastructure for Node.js & TypeScript**\n\n[![npm version](https://img.shields.io/badge/npm-v0.3.0-blue.svg)](https://www.npmjs.com/package/@ankit18193/pulse)\n[![license](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)\n[![node](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](package.json)\n\n**Pulse** is a production-grade, distributed real-time messaging engine. It provides horizontally scalable WebSocket infrastructure with room-based broadcast, cluster pub/sub, Lua-powered presence tracking, message delivery acknowledgments (ACKs), UUIDv7 idempotency, and built-in resilience hardening (origin defense, rate limiting, connection bounding, and graceful draining).\n\n---\n\n## Architecture Overview\n\n```\n┌────────────────────────────────────────────────────────────────────────┐\n│                        DEVELOPER APPLICATION                           │\n│                                                                        │\n│   import { PulseServer } from \"@ankit18193/pulse\";                     │\n│   const pulse = new PulseServer({ port: 8080 });                       │\n│   await pulse.start();                                                 │\n└───────────────────────────────────┬────────────────────────────────────┘\n                                    │\n                                    ▼\n┌────────────────────────────────────────────────────────────────────────┐\n│                   @ankit18193/pulse (Public Surface)                   │\n│                                                                        │\n│  • PulseServer          • loadConfig          • Authenticator          │\n│  • PulseServerOptions   • OriginMatcher       • generateUUIDv7         │\n│  • PulseMetricsRegistry • PrometheusSerializer• Protocol Event Types   │\n└───────────────────────────────────┬────────────────────────────────────┘\n                                    │\n               Strict Private Encapsulation (Engine Internals)\n                                    │\n                                    ▼\n┌────────────────────────────────────────────────────────────────────────┐\n│                        Pulse Engine (Private)                          │\n│                                                                        │\n│   ConnectionManager  •  MessageDispatcher  •  PresenceManager          │\n│   IdempotencyManager •  TokenBucket (Rate) •  HeartbeatManager         │\n└───────────────────────────────────┬────────────────────────────────────┘\n                                    │\n                                    ▼\n                       Redis Cluster (Pub/Sub & Leases)\n```\n\n---\n\n## 1. Installation\n\nInstall `@ankit18193/pulse` in your project via npm:\n\n```bash\nnpm install @ankit18193/pulse\n```\n\n### Requirements\n- **Node.js**: `>= 20.0.0`\n- **Module Format**: ES Modules (`\"type\": \"module\"`)\n- **Redis (Optional)**: Required only when running multiple distributed instances.\n\n---\n\n## 2. Quick Start\n\nCreate a minimal real-time WebSocket server in fewer than 10 lines of code:\n\n```typescript\n// server.ts\nimport { PulseServer } from '@ankit18193/pulse';\n\nconst server = new PulseServer({\n  port: 8080,\n  authSecret: 'pulse-dev-secret-key-32chars-min'\n});\n\nawait server.start();\nconsole.log('Pulse server listening on ws://localhost:8080');\n```\n\nRun with:\n```bash\nnode server.ts\n# or using tsx\nnpx tsx server.ts\n```\n\n### Standalone CLI Runner\nPulse can also be run directly as a dedicated daemon process without writing any code:\n\n```bash\n# Using npx\nnpx @ankit18193/pulse\n\n# Or using the installed binary\nnpx pulse-server --help\n```\n\n---\n\n## 3. Client Connection Guide\n\nPulse accepts standard RFC 6455 WebSocket connections from browser native `WebSocket`, Node.js `ws`, or mobile clients.\n\n### Connecting with Authentication\nAuthentication tokens can be passed via:\n1. **URL Query Parameter (Recommended)**: `ws://localhost:8080?token=<JWT_OR_HMAC>`\n2. **HTTP Authorization Header**: `Authorization: Bearer <TOKEN>`\n3. **WebSocket Subprotocol Header**: `Sec-WebSocket-Protocol: token.<TOKEN>`\n\n#### Browser Example\n```javascript\nconst token = \"your-auth-token-from-auth-service\";\nconst socket = new WebSocket(`ws://localhost:8080?token=${token}`);\n\nsocket.addEventListener('open', () => {\n  console.log('Connected to Pulse!');\n\n  // Join a room\n  socket.send(JSON.stringify({\n    eventId: crypto.randomUUID(),\n    type: 'ROOM_JOIN',\n    timestamp: Date.now(),\n    senderId: 'user_123',\n    target: { roomId: 'lobby' },\n    payload: { roomId: 'lobby' }\n  }));\n});\n\nsocket.addEventListener('message', (event) => {\n  const frame = JSON.parse(event.data);\n  console.log('Received event:', frame.type, frame.payload);\n});\n```\n\n#### Node.js (`ws`) Client Example\n```javascript\nimport WebSocket from 'ws';\n\nconst ws = new WebSocket(`ws://localhost:8080?token=${token}`);\n\nws.on('open', () => {\n  // Send a message to the room\n  ws.send(JSON.stringify({\n    eventId: 'msg-001',\n    type: 'ROOM_MESSAGE',\n    timestamp: Date.now(),\n    senderId: 'user_123',\n    target: { roomId: 'lobby' },\n    ackRequired: true,\n    payload: { text: 'Hello everyone!' }\n  }));\n});\n\nws.on('message', (data) => {\n  const envelope = JSON.parse(data.toString());\n  if (envelope.type === 'DELIVERY_ACK') {\n    console.log('Message delivered successfully!');\n  }\n});\n```\n\n---\n\n## 4. Configuration Reference\n\n`PulseServer` resolves configuration using the following priority:\n$$\\text{PulseServerOptions} \\longrightarrow \\text{Environment Variables} \\longrightarrow \\text{System Defaults} \\longrightarrow \\text{Production Validation}$$\n\n```typescript\nconst server = new PulseServer(options?: PulseServerOptions);\n```\n\n### Options & Environment Variables\n\n| Option | Environment Variable | Default | Description |\n| :--- | :--- | :--- | :--- |\n| `port` | `PORT` | `8080` | Port to bind HTTP and WebSocket server. |\n| `host` | `HOST` | `'0.0.0.0'` | Host address to bind. |\n| `nodeEnv` | `NODE_ENV` | `'development'` | Environment (`'development'`, `'test'`, `'production'`). |\n| `instanceId` | `INSTANCE_ID` | `'pulse-node-1'` | Unique instance node identifier in a cluster. |\n| `authSecret` | `AUTH_SECRET` | `'pulse-dev-secret-key-32chars-min'` | HMAC secret key used for authentication. |\n| `redisEnabled` | `REDIS_ENABLED` | `false` | Enable Redis cluster pub/sub and presence. |\n| `redisUrl` | `REDIS_URL` | `undefined` | Redis connection URL (e.g. `redis://127.0.0.1:6379`). |\n| `redisHost` | `REDIS_HOST` | `'127.0.0.1'` | Redis host (if not using `redisUrl`). |\n| `redisPort` | `REDIS_PORT` | `6379` | Redis port. |\n| `maxConnections` | `MAX_CONNECTIONS` | `10000` | Hard cap on concurrent active connections. |\n| `maxRoomsPerConnection` | `MAX_ROOMS_PER_CONNECTION` | `100` | Max rooms a single connection may join. |\n| `maxRoomIdLength` | `MAX_ROOM_ID_LENGTH` | `128` | Maximum room identifier character length. |\n| `allowedOrigins` | `ALLOWED_ORIGINS` | `['*']` (dev) | Comma-separated allowed Origin headers for CSWSH defense. |\n| `inboundRateLimitMax` | `INBOUND_RATE_LIMIT_MAX` | `100` | Refill tokens per second per connection. |\n| `inboundRateLimitBurst` | `INBOUND_RATE_LIMIT_BURST` | `50` | Maximum burst token bucket capacity per socket. |\n| `drainTimeoutMs` | `DRAIN_TIMEOUT_MS` | `2000` | Graceful shutdown connection draining window in ms. |\n| `heartbeatIntervalMs` | `HEARTBEAT_INTERVAL_MS`| `30000` | WebSocket ping heartbeat interval in ms. |\n| `heartbeatTimeoutMs` | `HEARTBEAT_TIMEOUT_MS` | `10000` | Unresponsive connection reaper timeout in ms. |\n| `metricsEnabled` | `METRICS_ENABLED` | `true` | Expose Prometheus metrics on `/metrics`. |\n| `metricsPath` | `METRICS_PATH` | `'/metrics'` | Path for Prometheus exposition endpoint. |\n\n---\n\n## 5. Authentication\n\nPulse includes a built-in cryptographic `Authenticator` that generates and validates HMAC-SHA256 tokens in constant time:\n\n```typescript\nimport { Authenticator } from '@ankit18193/pulse';\n\nconst authenticator = new Authenticator(process.env.AUTH_SECRET);\n\n// 1. Generate an authentication token for a user\nconst token = authenticator.generateToken({\n  userId: 'user_alice_123',\n  roles: ['member', 'moderator'],\n  expiresInSeconds: 3600 // 1 hour\n});\n\n// 2. Verify an incoming token\nconst result = authenticator.verifyToken(token);\nif (result.authenticated) {\n  console.log(`User ID: ${result.userId}, Roles: ${result.roles}`);\n}\n```\n\n### Production Security Gate\nWhen `NODE_ENV === 'production'`:\n- `AUTH_SECRET` must be explicitly configured and at least **32 characters long**.\n- Known development fallback secrets are rejected at server bootstrap.\n\n---\n\n## 6. Rooms & Subscriptions\n\nPulse supports dynamic room creation, message fan-out, and auto-cleanup.\n\n### Subscribing to Rooms\nClients join rooms by sending a `ROOM_JOIN` event:\n```json\n{\n  \"eventId\": \"01a0854b-24ac-7058-8605-a6cf1d1c2189\",\n  \"type\": \"ROOM_JOIN\",\n  \"timestamp\": 1725883200000,\n  \"senderId\": \"user_123\",\n  \"target\": { \"roomId\": \"chat-engineering\" },\n  \"payload\": { \"roomId\": \"chat-engineering\" }\n}\n```\nServer responds with `ROOM_JOIN_ACK`.\n\n### Batch Room Joins\nClients reconnecting after network drops can restore multiple rooms atomically using `ROOM_BATCH_JOIN`:\n```json\n{\n  \"eventId\": \"batch-join-01\",\n  \"type\": \"ROOM_BATCH_JOIN\",\n  \"timestamp\": 1725883200000,\n  \"senderId\": \"user_123\",\n  \"payload\": { \"rooms\": [\"chat-engineering\", \"announcements\", \"alerts\"] }\n}\n```\n\n### Room Bounding\nTo protect memory against subscription exhaustion attacks:\n- Sockets attempting to exceed `MAX_ROOMS_PER_CONNECTION` (default 100) receive `SYS_ERROR: ROOM_LIMIT_EXCEEDED`.\n- Room IDs exceeding `MAX_ROOM_ID_LENGTH` (default 128 characters) are rejected with `SYS_ERROR: INVALID_ROOM_ID`.\n\n---\n\n## 7. Messaging, Idempotency & ACKs\n\n### Event Envelope Format (`PulseEventEnvelope`)\nAll frames flowing through Pulse conform to a strict TypeScript envelope:\n\n```typescript\ninterface PulseEventEnvelope<T = unknown> {\n  eventId: string;           // RFC 9562 UUIDv7 or unique ID\n  type: EventType;           // 'ROOM_MESSAGE', 'DIRECT_MESSAGE', etc.\n  timestamp: number;         // Millisecond timestamp\n  senderId: string;          // Authenticated sender user ID\n  target?: {\n    roomId?: string;         // Target room for broadcast\n    recipientId?: string;    // Target user for direct message\n  };\n  payload: T;                // Arbitrary JSON payload\n  correlationId?: string;    // Optional correlation tracing ID\n  ackRequired?: boolean;     // Request delivery acknowledgment\n}\n```\n\n### Deduplication & Idempotency\nPulse maintains an in-memory LRU ring cache (10,000 capacity, 60s TTL) keyed by `eventId`:\n- Duplicate messages with identical `eventId` are suppressed from re-broadcasting.\n- If `ackRequired` was true, the original `DELIVERY_ACK` is replayed to the client.\n- Conflicting payload reuse of an existing `eventId` is rejected with `SYS_ERROR: EVENT_ID_CONFLICT`.\n\n### Delivery Acknowledgment (`DELIVERY_ACK`)\nWhen a client sends a message with `\"ackRequired\": true`, Pulse delivers the message to the target room and transmits a confirmation back to the sender:\n```json\n{\n  \"eventId\": \"01a0854b-666b-73f2-8a2e-fc0c10b8beac\",\n  \"type\": \"DELIVERY_ACK\",\n  \"timestamp\": 1725883201000,\n  \"senderId\": \"pulse-node-1\",\n  \"correlationId\": \"msg-alice-101\",\n  \"payload\": {\n    \"status\": \"DELIVERED\",\n    \"receivedCount\": 3\n  }\n}\n```\n\n---\n\n## 8. Distributed Presence Engine\n\nWhen Redis is enabled, Pulse runs an active presence tracking engine that aggregates multi-device user sessions across nodes using atomic Redis Lua scripts.\n\n```typescript\nimport { PulseServer } from '@ankit18193/pulse';\n\nconst server = new PulseServer({\n  redisEnabled: true,\n  redisUrl: 'redis://127.0.0.1:6379',\n  presenceTtlMs: 60000,        // Ephemeral lease TTL\n  presenceFlushIntervalMs: 15000 // Renewal heartbeat interval\n});\n```\n\n- **Online Transition**: Emitted when a user connects their first socket.\n- **Offline Transition**: Emitted when a user disconnects their last remaining device or their lease expires.\n- **Room Rosters**: Query online users within a specific room via `ROOM_ROSTER`.\n\n---\n\n## 9. Redis Multi-Node Clustering\n\nPulse scales horizontally by connecting multiple server nodes to a shared Redis instance or cluster:\n\n```bash\n# Node 1\nPORT=8081 INSTANCE_ID=pulse-1 REDIS_ENABLED=true REDIS_URL=redis://localhost:6379 npx pulse-server\n\n# Node 2\nPORT=8082 INSTANCE_ID=pulse-2 REDIS_ENABLED=true REDIS_URL=redis://localhost:6379 npx pulse-server\n```\n\n### Clustering Guarantees\n- **Reference-Counted Channels**: Redis subscriptions are created only when a local socket joins a room and freed when the last socket leaves.\n- **Loopback Suppression**: Nodes tag published frames with `originInstanceId` and drop their own echoes to prevent double delivery.\n- **Backpressure Protection**: Inbound and outbound buffers drop saturated sockets exceeding `MAX_BUFFERED_AMOUNT_BYTES` (default 1MB).\n\n---\n\n## 10. RouteX Gateway Integration\n\nPulse natively integrates with [@ankit18193/routex-gateway](https://www.npmjs.com/package/@ankit18193/routex-gateway) for edge routing, pre-101 failover, and reverse proxying:\n\n```typescript\nimport { PulseServer } from '@ankit18193/pulse';\nimport { RouteXGatewayServer } from '@ankit18193/routex-gateway';\n\nconst routex = new RouteXGatewayServer({\n  routes: [\n    { prefix: '/api', upstream: 'http://internal-api:3000' }\n  ]\n});\n\nconst server = new PulseServer(\n  { port: 8080 },\n  {},\n  { routexGateway: routex }\n);\n\nawait server.start();\n```\n\n---\n\n## 11. Production Hardening & Security\n\nEvery instance of Pulse incorporates the Phase 10 security invariants:\n\n1. **Cross-Site WebSocket Hijacking (CSWSH) Defense**:\n   Incoming handshakes are evaluated against `allowedOrigins`. In production, wildcard `*` is disallowed.\n2. **Inbound Token Bucket Rate Limiting**:\n   Sockets are metered with a token bucket (`INBOUND_RATE_LIMIT_MAX` refill rate, `INBOUND_RATE_LIMIT_BURST` capacity). Sockets exceeding limits receive `SYS_ERROR: RATE_LIMIT_EXCEEDED`; abusive sockets are closed with RFC 6455 Policy Violation (`code: 1008`).\n3. **Atomic Connection Admission**:\n   Handshake requests reserve connection slots atomically before socket upgrade. Over-capacity requests are rejected with `HTTP 503 Service Unavailable`.\n4. **Graceful Draining & Process Traps**:\n   On `SIGINT` or `SIGTERM`, Pulse enters draining mode: stops accepting new handshakes, broadcasts `SYS_SHUTDOWN` frames, and gracefully drains existing connections over `DRAIN_TIMEOUT_MS` before terminating.\n\n---\n\n## 12. Observability & Prometheus Metrics\n\nPulse exposes production metrics on `GET /metrics` formatted for Prometheus:\n\n```bash\ncurl http://localhost:8080/metrics\n```\n\n### Key Metrics\n- `pulse_connections_total`: Total connections by status (`accepted`, `rejected`).\n- `pulse_connections_active`: Current active WebSocket sockets.\n- `pulse_messages_received_total`: Inbound messages by event type.\n- `pulse_messages_delivered_total`: Outbound messages by event type.\n- `pulse_rate_limit_exceeded_total`: Messages dropped due to token bucket rate limits.\n- `pulse_event_loop_lag_seconds`: Mean, p50, and p99 Node.js event-loop lag.\n- `pulse_presence_users_online`: Distinct online users in cluster.\n\n### Health Probes\n- `GET /health`: Overall server health and uptime status.\n\n---\n\n## 13. Troubleshooting & FAQs\n\n### Q: Why do I get HTTP 401 on connect?\nEnsure an authentication token is provided in the query string (`ws://localhost:8080?token=<TOKEN>`) or `Authorization: Bearer <TOKEN>` header.\n\n### Q: Why does the server throw `AUTH_SECRET must be at least 32 characters long`?\nWhen `NODE_ENV=production`, Pulse enforces cryptographic secret length to prevent brute-force token forgery. Set a secure secret of at least 32 characters.\n\n### Q: How do I run Pulse without Redis?\nSimply leave `REDIS_ENABLED=false` (the default). Pulse will operate as a standalone, zero-dependency real-time engine.\n\n---\n\n## License\n\nApache License 2.0. See [LICENSE](LICENSE) for details.\n","readmeFilename":"README.md","_rev":"1-23f5205e045cbf16e766fccd9b45694b"}