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

dexiecable

v3.0.0

Published

Client-side companion for the DexieCable Ruby gem. Receives Dexie.js mutations from ActionCable and replays them against a local IndexedDB database.

Readme

DexieCable

[!NOTE] By itself, DexieCable is NOT a local-first solution. It has no automatic capability to push client-side changes back to the server.

An addon providing full synchronization based on event streams is currently in development. But for now, if you need full synchronization, you'll have to roll your own.

Who's this for?

DexieCable is made for Ruby on Rails apps that manage their client-side state in Dexie and use Dexie live queries for reactive UI updates. It's an alternative to Turbo Streams for apps that opt to use component frameworks (React, Vue, Svelte, etc.) instead of Turbo.

How does it work?

DexieCable gives your ActionCable channel a query DSL that mirrors the Dexie.js API, letting you push database mutations from the server to the client in real time. It also gives you a syncs_to_dexie ActiveRecord macro for automatic change syncing.

Push Dexie table updates to a client from anywhere on the server:

class NotificationsController < ApplicationController
  def create
    notification = current_user.notifications.create!(notification_params)
    DexieChannel[current_user].table("notifications").add(notification)
  end
end

Or sync model changes automatically with the syncs_to_dexie macro (more info below)

class Notification < ApplicationRecord
  syncs_to_dexie via: :user
end

What's new in 2.0

Custom Channels

DexieCable 2.0 is a mixin. Create your own DexieChannel and include DexieCable:

class DexieChannel < ApplicationCable::Channel
  include DexieCable
end

Reuse the same subscription for multiple streams

On the client, subscribe to that channel and add streams as needed. addStream accepts a signed token or a plain string, and returns a function that removes the stream:

const subscription = subscribe(db);
const unsubscribe = subscription.addStream(userStreamToken);

What's new in 3.0

subscribed_to moved to the model

The subscribed_to hook now lives on the record rather than on the channel. Private streams declare their initial snapshot on the record the token was issued for, and receive the channel plus any params sent from the client:

class User < ApplicationRecord
  subscribed_to do |channel, params|
    channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
  end
end

Public streams register a handler by name in your channel. The block runs in the channel's context, so table and transmit are available:

class DexieChannel < ApplicationCable::Channel
  include DexieCable

  subscribed_to "feed" do |params|
    table("announcements").bulkAdd(Announcement.for_stream("feed").map(&:as_json_for_dexie))
  end
end

Installation

Ruby gem

Add to your Gemfile:

gem "dexiecable"

Then bundle install. The Railtie automatically extends ActiveRecord::Base with syncs_to_dexie.

npm package

npm install dexiecable
# or
yarn add dexiecable

Pass your Dexie database as the first argument to subscribe():

import { subscribe } from "dexiecable";
import { db } from "./db";

const subscription = subscribe(db);
// Stream tokens come from the server: DexieChannel.stream_token_for(target)
subscription.addStream(streamToken);

A consumer is lazily created on the first subscribe() call. If you need to access or set the consumer explicitly, use getConsumer() and setConsumer():

import { getConsumer, setConsumer, createConsumer } from "dexiecable";

// Get the consumer (creates one lazily if needed)
const consumer = getConsumer();

// Or set a custom one
setConsumer(createConsumer("wss://example.com/cable"));

Usage

DexieChannel

Create a DexieChannel in your app and include DexieCable in it. Every Dexie broadcast goes through this channel:

# app/channels/dexie_channel.rb
class DexieChannel < ApplicationCable::Channel
  include DexieCable
end

Stream tokens are signed with the application secret, so a client can only subscribe to streams the server has issued for it:

DexieChannel.stream_token_for(target)

For an ActiveRecord model it returns a signed GlobalID (Rails' signed_id):

DexieChannel.stream_token_for(current_user)
# => "signed global id"

Tokens never expire by default. Pass expires_in: or expires_at: to limit a token's lifetime:

DexieChannel.stream_token_for(current_user, expires_in: 1.day)

An expired token, or one whose record no longer exists, is rejected. Only plain strings are treated as public stream names.

Send that token to the client (render it in a view, return it from an endpoint, etc.) and add it to the subscription:

const subscription = subscribe(db);
const stopStreaming = subscription.addStream(userStream);

addStream returns a function that removes the stream, so you can clean up later:

stopStreaming(); // equivalent to subscription.removeStream(userStream)

addStream/removeStream perform add_stream/remove_stream on DexieChannel, which verifies the token and then stream_from/stop_stream_from the decoded identifier. removeAllStreams() performs remove_all_streams, stopping every current stream. Handy on logout:

subscription.removeAllStreams();

Customizing DexieChannel

Add custom actions or push initial data directly on your channel:

# app/channels/dexie_channel.rb
class DexieChannel < ApplicationCable::Channel
  include DexieCable

  # Push a snapshot when a public stream is added. The block receives the
  # params sent from the client and runs in the channel's context, so
  # `table(...)` and `transmit` are available.
  subscribed_to "feed" do |params|
    table("feed").bulkAdd(Announcement.for_stream("feed").map(&:as_json_for_dexie))
  end

  # Any public method is a custom action the client can perform. Always
  # resolve records through the connection's actor, never from the payload:
  # `Message.find(data["id"])` would let any client mark anyone's message
  # read.
  def mark_as_read(data)
    current_user.messages.find(data["id"]).update!(read: true)
  end
end

Private streams declare their snapshot on the record itself, which is where the stream was opened for. The block runs in the record's context and receives the channel — so channel.table(...) transmits to just this subscriber — plus the params sent from the client:

class User < ApplicationRecord
  subscribed_to do |channel, params|
    channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
  end
end

class Conversation < ApplicationRecord
  subscribed_to do |channel, params|
    channel.table("messages").bulkAdd(messages.where("seq_id > ?", params[:last_seq_id]).map(&:as_json_for_dexie))
  end
end

Declare subscribed_to more than once to push several snapshots — the blocks run in declaration order. To share a snapshot across models, or take full control, override the instance method instead and call super to keep any registered blocks running:

module HasNotifications
  def subscribed_to(channel, params)
    channel.table("notifications").bulkAdd(notifications.map(&:as_json_for_dexie))
    super
  end
end

The client subscribes to DexieChannel by default:

const subscription = subscribe(db);

Pass params from the client when adding a stream:

subscription.addStream(userStream, { last_seq_id: 100 });

subscribed_to runs after the stream is opened, and the snapshot it pushes (via table(...) on the channel, or channel.table(...) on the record) transmits to just this subscriber, so it arrives before any live mutation. Custom actions are triggered like any ActionCable action: subscription.perform("mark_as_read", { id: 42 }). The payload is client-controlled, so pass it through the authenticated actor — current_user.messages.find(data["id"]), not Message.find(data["id"]).

Public streams

For data that's public (a global feed, announcements, etc.), skip the signature. Use a string target. It's namespaced under public: automatically:

DexieChannel["feed"].table("announcements").add(announcement)

# or, on a model:
class Announcement < ApplicationRecord
  syncs_to_dexie via: "feed"
end

Then subscribe by name. No token required:

const stopPublicStream = subscription.addStream("feed");
stopPublicStream(); // equivalent to subscription.removeStream("feed")

Public streams are namespaced under public:, so this path can never reach a signed (private) stream.

DexieChannel[target] returns a scoped channel for broadcasting to one recipient:

DexieChannel[current_user].table("notifications").add(notification)

Chaining Dexie operations

Any Dexie.js write operation triggers an immediate broadcast:

# Single insert
DexieChannel[current_user].table("messages").add(id: 1, text: "hello")

# Bulk insert
DexieChannel[current_user].table("messages").bulkAdd(messages)

# Update (using modify)
DexieChannel[current_user]
  .table("messages")
  .where(:id).equals(msg.id)
  .modify(read: true)

# Update (using update)
DexieChannel[current_user]
  .table("messages")
  .update(msg.id, text: "updated text")

# Delete
DexieChannel[current_user]
  .table("messages")
  .where(:room_id).equals(room.id)
  .delete()

The full query chain is serialized as JSON and sent over ActionCable. The JS client replays every method call against the local Dexie database in order.

syncs_to_dexie: automatic model streaming

Add to any ActiveRecord model. Optionally provide the broadcast target.

class Message < ApplicationRecord
  belongs_to :conversation
  belongs_to :receiver
  
  # Calls send(:receiver), then broadcasts: DexieChannel.broadcast_to(receiver, ...)
  syncs_to_dexie via: :receiver

  # String = public stream (subscribe via addStream("public"))
  syncs_to_dexie via: "public"

  # Procs are also supported. If an array is returned, multiple broadcasts are made
  # conversation.users.each { |u| DexieChannel.broadcast_to(u, ...) }
  syncs_to_dexie via: -> { conversation.users }
end

Broadcasts go out over DexieChannel, the channel DexieCable provides. On the client, subscribe to it and add the stream token returned by DexieChannel.stream_token_for(target):

const subscription = subscribe(db);
subscription.addStream(streamIdentifier);

Internally, syncs_to_dexie sets up the following ActiveRecord callbacks:

| Event | Action | |---|---| | after_commit on: :create | channel.table(table).add(as_json_for_dexie) | | after_commit on: :update | channel.table(table).update(id, as_json_for_dexie.slice(*saved_changes.keys)) | | after_commit on: :destroy | channel.table(table).delete(id) |

Options

| Option | Default | Description | |---|---|---| | via: | the record itself | The stream target. Symbol → calls send (a record, signed). String → public stream name. Proc → evaluated in record context. Returns a single recipient or collection. | | table: | model's table_name | Override the Dexie table name. A Proc is evaluated in the record's context. | | only: | [:create, :update, :destroy] | Limit which events trigger a sync | | with: | :as_json_for_dexie | Method name (Symbol) or Proc for serializing records | | if: | (none) | Symbol (method name) or Proc. Only sync when it returns truthy | | unless: | (none) | Symbol (method name) or Proc. Skip sync when it returns truthy |

You can combine multiple syncs_to_dexie declarations, each with different conditions:

class Message < ApplicationRecord
  syncs_to_dexie via: -> { sender },
                 if: :published?

  syncs_to_dexie unless: -> { draft? }
end

Customizing the synced payload

Override as_json_for_dexie in your model, or use the with option to specify a different method or Proc:

class Message < ApplicationRecord
  # Using the default as_json_for_dexie override:
  syncs_to_dexie via: :sender

  def as_json_for_dexie
    super.merge(room_name: room.name)
  end

  # Or use a custom serializer method:
  syncs_to_dexie via: :admin,
                 with: :admin_payload

  def admin_payload
    attributes.slice("id", "body", "flagged")
  end

  # Or a Proc:
  syncs_to_dexie with: -> { { id: id, summary: body.truncate(100) } }
end

How it works

sequenceDiagram
    participant Model as ActiveRecord Model
    participant Channel as DexieCable Channel
    participant WS as ActionCable WebSocket
    participant JS as dexiecable.js
    participant DB as Dexie.js (IndexedDB)

    Model->>Channel: after_commit
    Channel->>Channel: build Query DSL
    Channel->>WS: broadcast JSON { table, ops }
    WS->>JS: received(data)
    JS->>DB: replay ops chain
    DB-->>JS: result

The Ruby side builds a JSON payload like:

{
  "table": "messages",
  "ops": [
    { "method": "where", "params": ["room_id"] },
    { "method": "equals", "params": [5] },
    { "method": "add", "params": [{ "id": 1, "text": "hello" }] }
  ]
}

The JS side replays it as:

dexie.messages.where("room_id").equals(5).add({ id: 1, text: "hello" })

License

MIT