Syncro let’s you synchronise Ruby classes and state between remote clients. Syncro also supports offline sync.

You can record changes to a Ruby class, then Syncro will replay them on a remote client, synchronising state between the two.

Syncro leaves the connection management up to you. You can use any networking library (EventMachine/TCPSockets etc).

Each client is represented by a GUID. Even if your architecture isn’t P2P clients, the server is considered a “client”, and needs a GUID too (even if that’s just the string “server”).

Syncro is already setup to support ActiveModel compliant classes (such as ActiveRecord/SuperModel). For example, to synchronise a ActiveRecord class:

class TestSync < ActiveRecord::Base

include Syncro::Model

end

If you want to use Syncro with non ActiveModel compliant classes, you need to implement the class method “sync_play”, and create Syncro::Scriber::Scribe objects whenever the class changes - checkout Syncro::Scriber::Model and Syncro::Scriber::Observer.

To synchronize a class you need to:

* Include Syncro::Model on that class
* Call the following upon connection:
    @client = Syncro::Client.for(:client_uuid1)
    @client.connect(connection_instance)
    @client.sync
* Call the following when the connection receives data:
    @client.receive_data(data)
* Call the following when the client disconnects:
    @client.disconnect

That’s it!

Have a look at the example test client and server.

More information

To Syncro, everything is a client - even the server. Every client is represented by a unique identifier. This could be a client ID, or in the case of a server a fixed string.

Each client records changes to its classes. When a class changes, a ‘Scribe’ is created detailing that change. Replaying that Scribe on remote clients synchronises class state.

When a client synchronises, it asks the remote client for all Scribes since the last sync. The client then replays the Scribes, synchronising state. The remote client then does the same. If the clients are connected, and a Scribe is created, the remote client is immediately notified.

When clients connect for the first time, client objects are created. These record the time synchronisation happened (the last Scribe they processed).

Scribes & Clients can be stored in two ways:

* Marshaled to disk (see SuperModel::Marshal)
* In Redis

ActiveRecord support hasn’t been added (but could be easily).

You should use Redis on the server for performance reasons.

Limiting access

By default, all model changes are synced with everyone. This isn’t ideal for a lot of use cases - for example if a user had many pages, those pages are specific to the user and shouldn’t be synced with anyone else.

To limit access, you need to implement the following methods on the class.

def scribe_clients

[authed_client_uuid1, authed_client_uuid2]

end

def self.scribe_authorized?(scribe)

# Check scribe.type and scribe.from_client
# to work out if the client is authorised to
# synchronise this scribe.

end

Quick example

The following is an example of using EM, SuperModel and Syncro. Any changes to the class “Test” will be reflected across both clients:

require "syncro"
require "syncro/marshal"
require "eventmachine"

class Test < SuperModel::Base
  include SuperModel::Marshal::Model
  include Syncro::Model
end

class MyConnection < EM::Connection
  def connection_complete
    @client = Syncro::Client.for(:server)
    @client.connect(self)
    @client.sync
  end

  def receive_data(data)
    puts "Received: #{data}"
    @client.receive_data(data)
  end
end

SuperModel::Marshal.path = "dump.db"
SuperModel::Marshal.load

at_exit {
  SuperModel::Marshal.dump
}

EM.run {
  EM.connect("0.0.0.0", 10000, MyConnection)
}

Protocol

Syncro uses a very simple JSON protocol. Each message is a JSON hash. The only mandatory field is “type”. => “foo”, …

Each message is preceded by a short int, representing the message size. For example, in Ruby:

data = {:type => "foo", :bar => 1}
message = [data.length].pack('n') + data

At the moment, there are only two types of message:

* sync (args: from)
* add_scribe (args: scribe)

Have a look at app.rb for implementation details.

If the messages are split up already, i.e. you don’t need the binary length preceding the message, you can instead use the method @client.receive_message(json_string_or_hash) This is particular useful for WebSocket clients.

Roadmap

  • A JavaScript Syncro client for web apps that have offline capabilities, and can use WebSockets to sync with the server. WebSockets should be on the iPhone/iPad soon.

  • Easier protocol extensions, for things like authenticating clients (which are done by subclassing atm).