Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/dubadub/sync_bumper


https://github.com/dubadub/sync_bumper

Last synced: 17 days ago
JSON representation

Awesome Lists containing this project

README

        

# Sync
> This started as a thought experiment that is growing into a viable option for realtime Rails apps without ditching
the standard rails stack that we love and are so productive with for a heavy client side MVC framework.

Real-time partials with Rails. Sync lets you render partials for models that, with minimal code,
update in realtime in the browser when changes occur on the server.

#### Watch a screencast to see it in action
[![See it in action](http://chrismccord.com/images/sync/video_thumb.png)](http://chrismccord.com/blog/2013/04/21/sync-realtime-rails-partials/)

In practice, one simply only needs to replace:

```erb
<%= render partial: 'user_row', locals: {user: @user} %>
```

with:

```erb
<%= sync partial: 'user_row', resource: @user %>
```

Then update views realtime automatically with the `sync` DSL or with a with a simple `sync_update(@user)` in the controller without any extra javascript or
configuration.

In addition to real-time updates, Sync also provides:

- Realtime removal of partials from the DOM when the sync'd model is destroyed in the controller via `sync_destroy(@user)`
- Realtime appending of newly created model's on scoped channels
- JavaScript/CoffeeScript hooks to override and extend element updates/appends/removes for partials
- Support for [Faye](http://faye.jcoglan.com/) and [Pusher](http://pusher.com)

## Requirements

- Ruby >= 1.9.2
- Rails 3 >= 3.1 or Rails 4
- jQuery >= 1.9

## Installation

#### 1) Add the gem to your `Gemfile`

#### Using Faye

```ruby
gem 'faye'
gem 'thin', require: false
gem 'sync'
```

#### Using Pusher

```ruby
gem 'pusher'
gem 'sync'
```

#### Install

```bash
$ bundle
$ rails g sync:install
```

#### 2) Require sync in your asset javascript manifest `app/assets/javascripts/application.js`:

```javascript
//= require sync
```

#### 3) Add the pubsub adapter's javascript to your application layout `app/views/layouts/application.html.erb`

```erb
<%= javascript_include_tag Sync.adapter_javascript_url %>
```

#### 4) Configure your pubsub server (Faye or Pusher)

#### Using [Faye](http://faye.jcoglan.com/) (self hosted)

Set your configuration in the generated `config/sync.yml` file, using the Faye adapter. Then run Faye alongside your app.

```bash
rackup sync.ru -E production
```

#### Using [Pusher](http://pusher.com) (SaaS)

Set your configuration in the generated `config/sync.yml` file, using the Pusher adapter. No extra process/setup.

## Current Caveats
The current implementation uses a DOM range query (jQuery's `nextUntil`) to match your partial's "element" in
the DOM. The way this selector works requires your sync'd partial to be wrapped in a root level html tag for that partial file.
For example, this parent view/sync partial approach would *not* work:

Given the sync partial `_todo_row.html.erb`:

```erb
Title:
<%= link_to todo.title, todo %>
```

And the parent view:

```erb



<%= sync partial: 'todo_row', resource: @todo %>

```

##### The markup *would need to change to*:

sync partial `_todo_row.html.erb`:

```erb

Title:
<%= link_to todo.title, todo %>

```

And the parent view changed to:

```erb


<%= sync partial: 'todo_row', resource: @todo %>

```

I'm currently investigating true DOM ranges via the [Range](https://developer.mozilla.org/en-US/docs/DOM/range) object.

## 'Automatic' syncing through the sync DSL

In addition to calling explicit sync actions within controller methods, a
`sync` and `enable_sync` DSL has been added to ActionController::Base and ActiveRecord::Base to automate the syncing
approach in a controlled, threadsafe way.

### Example Controller/Model
```ruby
class TodosController < ApplicationController

enable_sync only: [:create, :update, :destroy]
...
end

class Todo < ActiveRecord::Base

belongs_to :project, counter_cache: true
has_many :comments, dependent: :destroy

sync :all, scope: :project

end
```

### Syncing outside of the controller

`Sync::Actions` can be included into any object wishing to perform sync
publishes for a given resource. Instead of using the the controller as
context for rendering, a Sync::Renderer instance is used. Since the Renderer
is not part of the request/response/session, it has no knowledge of the
current session (ie. current_user), so syncing from outside the controller
context will require some care that the partial can be rendered within a
sessionless context.

### Example Syncing from a background worker or rails console
```ruby
# Inside some script/worker
Sync::Model.enable do
Todo.first.update title: "This todo will be sync'd on save"
end
Todo.first.update title: "This todo will NOT be sync'd on save"

Sync::Model.enable!
Todo.first.update title: "This todo will be sync'd on save"
Todo.first.update title: "This todo will be sync'd on save"
Todo.first.update title: "This todo will be sync'd on save"
Sync::Model.disable!
Todo.first.update title: "This todo will NOT be sync'd on save"
```

## Custom Sync Views and javascript hooks

Sync allows you to hook into and override or extend all of the actions it performs when updating partials on the client side. When a sync partial is rendered, sync will instantiate a javascript View class based on the following order of lookup:

1. The camelized version of the concatenated snake case resource
and partial names.
2. The camelized version of the snake cased partial name.

#### Examples

partial name 'list_row', resource name 'todo', order of lookup:

1. Sync.TodoListRow
2. Sync.ListRow
3. Sync.View (Default fallback)

For example, if you wanted to fade in/out a row in a sync'd todo list instead of the Sync.View default of instant insert/remove:

```coffeescript
class Sync.TodoListRow extends Sync.View

beforeInsert: ($el) ->
$el.hide()
@insert($el)

afterInsert: -> @$el.fadeIn 'slow'

beforeRemove: -> @$el.fadeOut 'slow', => @remove()

```

## Narrowing sync_new scope

Sometimes, you do not want your page to update with every new record. With the `scope` option, you can limit what is being updated on a given page.

One way of using `scope` is by supplying a String or a Symbol. This is useful for example when you want to only show new records for a given locale:

View:
```erb
<%= sync_new partial: 'todo_list_row', resource: Todo.new, scope: I18n.locale %>
```

Controller/Model:
```ruby
sync_new @todo, scope: @todo.locale
```

Another use of `scope` is with a parent resource. This way you can for example update a project page with new todos for this single project:

View:
```erb
<%= sync_new partial: 'todo_list_row', resource: Todo.new, scope: @project %>
```

Controller/Model:
```ruby
sync_new @todo, scope: @project
```

Both approaches can be combined. Just supply an Array of Strings/Symbols and/or parent resources to the `scope` option. Note that the order of elements matters. Be sure to use the same order in your view and in your controller/model.

## Refetching Partials

Refetching allows syncing partials across different users when the partial requires the session's context (ie. current_user).

Ex:
View: Add `refetch: true` to sync calls, and place partial file in a 'refetch'
subdirectory in the model's sync view folder:

The partial file would be located in `app/views/sync/todos/refetch/_list_row.html.erb`
```erb
<% @project.todos.ordered.each do |todo| %>
<%= sync partial: 'list_row', resource: todo, refetch: true %>
<% end %>
<%= sync_new partial: 'list_row', resource: Todo.new, scope: @project, refetch: true %>
```

*Notes*

While this approach works very well for the cases it's needed, syncing without refetching should be used unless refetching is absolutely necessary for performance reasons. For example,

A sync update request is triggered on the server for a 'regular' sync'd partial with 100 listening clients:
- number of http requests 1
- number of renders 1, pushed out to all 100 clients via pubsub server.

A sync update request is triggered on the server for a 'refetch' sync'd partial with 100 listening clients:
- number of http requests 100
- number of renders 100, rendering each request in clients session context.

## Using with cache_digests (Russian doll caching)

Sync has a custom `DependencyTracker::ERBTracker` that can handle `sync` render calls.
Because the full partial name is not included, it has to guess the location of
your partial based on the name of the `resource` or `collection` passed to it.
See the tests to see how it works. If it doesn't work for you, you can always
use the [explicit "Template Dependency"
markers](https://github.com/rails/cache_digests).

To enable, add to `config/initializers/cache_digests.rb`:

#### Rails 4

```ruby
require 'action_view/dependency_tracker'

ActionView::DependencyTracker.register_tracker :haml, Sync::ERBTracker
ActionView::DependencyTracker.register_tracker :erb, Sync::ERBTracker
```

#### Rails 3 with [cache_digests](https://github.com/rails/cache_digests) gem

```ruby
require 'cache_digests/dependency_tracker'

CacheDigests::DependencyTracker.register_tracker :haml, Sync::ERBTracker
CacheDigests::DependencyTracker.register_tracker :erb, Sync::ERBTracker
```

**Note:** haml support is limited, but it seems to work in most cases.

## Serving Faye over HTTPS (with Thin)

Create a thin configuration file `config/sync_thin.yml` similar to the following:

```yaml
---
port: 4443
ssl: true
ssl_key_file: /path/to/server.pem
ssl_cert_file: /path/to/certificate_chain.pem
environment: production
rackup: sync.ru
```

The `certificate_chain.pem` file should contain your signed certificate, followed by intermediate certificates (if any) and the root certificate of the CA that signed the key.

Next reconfigure the `server` and `adapter_javascript_url` in `config/sync.yml` to look like `https://your.hostname.com:4443/faye` and `https://your.hostname.com:4443/faye/faye.js` respectively.

Finally start up Thin from the project root.

```
thin -C config/sync_thin.yml start
```

## Brief Example or [checkout an example application](https://github.com/chrismccord/sync_example)

View `sync/users/_user_list_row.html.erb`

```erb

<%= link_to user.name, user %>
<%= link_to 'Edit', edit_user_path(user) %>
<%= link_to 'Destroy', user, method: :delete, remote: true, data: { confirm: 'Are you sure?' } %>

```

View `users/index.html.erb`

```erb

Some Users


<%= sync partial: 'user_list_row', collection: @users %>
<%= sync_new partial: 'user_list_row', resource: User.new, direction: :append %>

```

Controller

```ruby
def UsersController < ApplicationController

def create
@user = User.new(user_params)
if @user.save
sync_new @user
end
respond_to do |format|
format.html { redirect_to users_url }
format.json { head :no_content }
end
end

def update
@user = User.find(params[:id])
if user.save

end

# Sync updates to any partials listening for this user
sync_update @user

redirect_to users_path, notice: "Saved!"
end

def destroy
@user = User.find(params[:id])
@user.destroy

# Sync destroy, telling client to remove all dom elements containing this user
sync_destroy @user

respond_to do |format|
format.html { redirect_to users_url }
format.json { head :no_content }
end
end
end
```