Socket
Book a DemoInstallSign in
Socket

pulse_zero

Package Overview
Dependencies
Maintainers
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pulse_zero

0.3.2
bundlerRubygems
Version published
Maintainers
1
Created
Source

Pulse Zero

Real-time broadcasting generator for Rails + Inertia.js applications. Generate a complete WebSocket-based real-time system with zero runtime dependencies.

What is Pulse Zero?

Pulse Zero generates a complete real-time broadcasting system directly into your Rails application. Unlike traditional gems, all code is copied into your project, giving you full ownership and the ability to customize everything.

Inspired by Turbo Rails, Pulse Zero brings familiar broadcasting patterns to Inertia.js applications. If you've used broadcasts_to in Turbo Rails, you'll feel right at home with Pulse Zero's API.

Features:

  • 🚀 WebSocket broadcasting via ActionCable
  • 🔒 Secure signed streams
  • 📱 Browser tab suspension handling
  • 🔄 Automatic reconnection with exponential backoff
  • 📦 TypeScript support for Inertia + React
  • 🎯 Zero runtime dependencies
  • 🏗️ Turbo Rails-inspired API design

Installation

Add this gem to your application's Gemfile:

group :development do
  gem 'pulse_zero'
end

Then run:

bundle install
rails generate pulse_zero:install

What Gets Generated?

Backend (Ruby)

  • lib/pulse/ - Core broadcasting system
  • app/models/concerns/pulse/broadcastable.rb - Model broadcasting DSL
  • app/controllers/concerns/pulse/request_id_tracking.rb - Request tracking
  • app/channels/pulse/channel.rb - WebSocket channel
  • app/jobs/pulse/broadcast_job.rb - Async broadcasting
  • config/initializers/pulse.rb - Configuration

Frontend (TypeScript)

  • app/frontend/lib/pulse.ts - Subscription manager
  • app/frontend/lib/pulse-connection.ts - Connection monitoring
  • app/frontend/lib/pulse-recovery-strategy.ts - Recovery logic
  • app/frontend/lib/pulse-visibility-manager.ts - Tab visibility handling
  • app/frontend/hooks/use-pulse.ts - React subscription hook
  • app/frontend/hooks/use-visibility-refresh.ts - Tab refresh hook

Quick Start

1. Enable Broadcasting on a Model

class Post < ApplicationRecord
  include Pulse::Broadcastable
  
  # Broadcast to a simple channel
  broadcasts_to ->(post) { "posts" }
  
  # Or broadcast to account-scoped channel
  # broadcasts_to ->(post) { [post.account, "posts"] }
end

2. Pass Stream Token to Frontend

class PostsController < ApplicationController
  def index
    @posts = Post.all

    render inertia: "Post/Index", props: {
      posts: @posts.map do |post|
        serialize_post(post)
      end,
      pulseStream: Pulse::Streams::StreamName.signed_stream_name("posts")
    }
  end
  
  private
  
  def serialize_post(post)
    {
      id: post.id,
      title: post.title,
      content: post.content,
      created_at: post.created_at
    }
  end
end

Why pulseStream? Following Turbo Rails' security model, Pulse uses signed stream names to prevent unauthorized access to WebSocket channels. Since Inertia.js doesn't have a built-in way to access streams like Turbo does, we pass the signed stream name as a prop. This approach:

  • Maintains security through cryptographically signed tokens
  • Works naturally with Inertia's prop system
  • Keeps the API simple and explicit

3. Subscribe in React Component

import { useState } from 'react'
import { usePulse } from '@/hooks/use-pulse'
import { useVisibilityRefresh } from '@/hooks/use-visibility-refresh'
import { router } from '@inertiajs/react'

interface IndexProps {
  posts: Array<{
    id: number
    title: string
    content: string
    created_at: string
  }>
  pulseStream: string
  flash: {
    success?: string
    error?: string
  }
}

export default function Index({ posts: initialPosts, flash, pulseStream }: IndexProps) {
  // Use local state for posts to enable real-time updates
  const [posts, setPosts] = useState(initialPosts)
  
  // Automatically refresh data when returning to the tab after 30+ seconds
  // This ensures users see fresh data after being away, handling cases where
  // WebSocket messages might have been missed during browser suspension
  useVisibilityRefresh(30, () => {
    router.reload({ only: ['posts'] })
  })

  // Subscribe to Pulse updates for real-time changes
  usePulse(pulseStream, (message) => {
    switch (message.event) {
      case 'created':
        // Add the new post to the beginning of the list
        setPosts(prev => [message.payload, ...prev])
        break
      case 'updated':
        // Replace the updated post in the list
        setPosts(prev => 
          prev.map(post => post.id === message.payload.id ? message.payload : post)
        )
        break
      case 'deleted':
        // Remove the deleted post from the list
        setPosts(prev => 
          prev.filter(post => post.id !== message.payload.id)
        )
        break
      case 'refresh':
        // Full reload for refresh events
        router.reload()
        break
    }
  })
  
  return (
    <>
      {flash.success && <div className="alert-success">{flash.success}</div>}
      <PostsList posts={posts} />
    </>
  )
}

Note: This example shows optimistic UI updates using local state. Alternatively, you can use router.reload({ only: ['posts'] }) for all events to fetch fresh data from the server, which ensures consistency but may feel less responsive.

Why useVisibilityRefresh? When users switch tabs or minimize their browser, WebSocket connections can be suspended and messages may be lost. The useVisibilityRefresh hook detects when users return to your app and automatically refreshes the data if they've been away for more than the specified threshold (30 seconds in this example). This ensures users always see up-to-date information without manual refreshing.

Broadcasting Events

Pulse broadcasts four types of events:

created - When a record is created

{
  "event": "created",
  "payload": { "id": 123, "content": "New post" },
  "requestId": "uuid-123",
  "at": 1234567890.123
}

updated - When a record is updated

{
  "event": "updated",
  "payload": { "id": 123, "content": "Updated post" },
  "requestId": "uuid-456",
  "at": 1234567891.456
}

deleted - When a record is destroyed

{
  "event": "deleted",
  "payload": { "id": 123 },
  "requestId": "uuid-789",
  "at": 1234567892.789
}

refresh - Force a full refresh

{
  "event": "refresh",
  "payload": {},
  "requestId": "uuid-012",
  "at": 1234567893.012
}

Advanced Usage

Manual Broadcasting

# Broadcast with custom payload
post.broadcast_updated_to(
  [Current.account, "posts"],
  payload: { id: post.id, featured: true }
)

# Async broadcasting
post.broadcast_updated_later_to([Current.account, "posts"])

Suppress Broadcasts During Bulk Operations

Post.suppressing_pulse_broadcasts do
  Post.where(account: account).update_all(featured: true)
end

# Then send one refresh broadcast
Post.new.broadcast_refresh_to([account, "posts"])

Custom Serialization

# config/initializers/pulse.rb
Rails.application.configure do
  config.pulse.serializer = ->(record) {
    case record
    when Post
      record.as_json(only: [:id, :title, :state])
    else
      record.as_json
    end
  }
end

Configuration

# config/initializers/pulse.rb
Rails.application.configure do
  # Debounce window in milliseconds (default: 300)
  config.pulse.debounce_ms = 300
  
  # Background job queue (default: :default)
  config.pulse.queue_name = :low
  
  # Custom serializer
  config.pulse.serializer = ->(record) { record.as_json }
end

Browser Tab Handling

Pulse includes sophisticated handling for browser tab suspension:

  • Quick switches (<30s): Just ensures connection is alive
  • Medium absence (30s-5min): Reconnects and syncs data
  • Long absence (>5min): Full page refresh for consistency

Platform-aware thresholds:

  • Desktop Chrome/Firefox: 30 seconds
  • Safari/Mobile: 15 seconds (more aggressive)

Testing

# In your test files
test "broadcasts on update" do
  post = posts(:one)
  
  assert_broadcast_on([post.account, "posts"]) do
    post.update!(title: "New Title")
  end
end

# Suppress broadcasts in tests
Post.suppressing_pulse_broadcasts do
  # Your test code
end

Debugging

Enable debug logging:

// In browser console
localStorage.setItem('PULSE_DEBUG', 'true')

Check connection health:

import { getPulseMonitorStats } from '@/lib/pulse-connection'

const stats = getPulseMonitorStats()
console.log(stats)

Requirements

  • Rails 7.0+
  • ActionCable
  • Inertia.js
  • React (Vue/Svelte support coming soon)
  • TypeScript

Philosophy

Pulse Zero follows the same philosophy as authentication-zero, and is heavily inspired by Turbo Rails. The API design closely mirrors Turbo Rails patterns, making it intuitive for developers already familiar with the Hotwire ecosystem.

  • Own your code: All code is generated into your project
  • No runtime dependencies: The gem is only needed during generation
  • Customizable: Modify any generated code to fit your needs
  • Production-ready: Includes battle-tested patterns from real applications
  • Familiar API: Inspired by Turbo Rails, uses similar broadcasting patterns and conventions

Contributing

Bug reports and pull requests are welcome on GitHub.

License

The gem is available as open source under the terms of the MIT License.

FAQs

Package last updated on 26 Jun 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

About

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.

  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc

U.S. Patent No. 12,346,443 & 12,314,394. Other pending.