syncro 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ pkg
2
+ test.rb
3
+ dump.db
data/README ADDED
@@ -0,0 +1,138 @@
1
+ Syncro let's you synchronise Ruby classes and state between remote clients.
2
+ Syncro also supports offline sync.
3
+
4
+ You can record changes to a Ruby class, then Syncro will replay them on a remote client,
5
+ synchronising state between the two.
6
+
7
+ Syncro leaves the connection management up to you. You can use any networking library (EventMachine/TCPSockets etc).
8
+
9
+ Each client is represented by a GUID. Even if your architecture isn't P2P clients, the server is
10
+ considered a "client", and needs a GUID too (even if that's just the string "server").
11
+
12
+ Syncro is already setup to support ActiveModel compliant classes (such as ActiveRecord/SuperModel).
13
+ For example, to synchronise a ActiveRecord class:
14
+
15
+ class TestSync < ActiveRecord::Base
16
+ include Syncro::Model
17
+ end
18
+
19
+ If you want to use Syncro with non ActiveModel compliant classes, you need to implement the class method "sync_play",
20
+ and create Syncro::Scriber::Scribe objects whenever the class changes - checkout Syncro::Scriber::Model and Syncro::Scriber::Observer.
21
+
22
+ To synchronize a class you need to:
23
+ * Include Syncro::Model on that class
24
+ * Call the following upon connection:
25
+ @client = Syncro::Client.for(:client_uuid1)
26
+ @client.connect(connection_instance)
27
+ @client.sync
28
+ * Call the following when the connection receives data:
29
+ @client.receive_data(data)
30
+ * Call the following when the client disconnects:
31
+ @client.disconnect
32
+
33
+ That's it!
34
+
35
+ Have a look at the example test client and server.
36
+
37
+ = More information
38
+
39
+ To Syncro, everything is a client - even the server. Every client is represented by a unique identifier.
40
+ This could be a client ID, or in the case of a server a fixed string.
41
+
42
+ Each client records changes to its classes. When a class changes, a 'Scribe' is created detailing that change.
43
+ Replaying that Scribe on remote clients synchronises class state.
44
+
45
+ When a client synchronises, it asks the remote client for all Scribes since the last sync. The client then replays
46
+ the Scribes, synchronising state. The remote client then does the same. If the clients are connected, and a Scribe
47
+ is created, the remote client is immediately notified.
48
+
49
+ When clients connect for the first time, client objects are created. These record the time synchronisation happened (the last Scribe they processed).
50
+
51
+ Scribes & Clients can be stored in two ways:
52
+ * Marshaled to disk (see SuperModel::Marshal)
53
+ * In Redis
54
+
55
+ ActiveRecord support hasn't been added (but could be easily).
56
+
57
+ You should use Redis on the server for performance reasons.
58
+
59
+ = Limiting access
60
+
61
+ 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.
62
+
63
+ To limit access, you need to implement the following methods on the class.
64
+
65
+ def scribe_clients
66
+ [authed_client_uuid1, authed_client_uuid2]
67
+ end
68
+
69
+ def self.scribe_authorized?(scribe)
70
+ # Check scribe.type and scribe.from_client
71
+ # to work out if the client is authorised to
72
+ # synchronise this scribe.
73
+ end
74
+
75
+ = Quick example
76
+
77
+ The following is an example of using EM, SuperModel and Syncro.
78
+ Any changes to the class "Test" will be reflected across both clients:
79
+
80
+ require "syncro"
81
+ require "syncro/marshal"
82
+ require "eventmachine"
83
+
84
+ class Test < SuperModel::Base
85
+ include SuperModel::Marshal::Model
86
+ include Syncro::Model
87
+ end
88
+
89
+ class MyConnection < EM::Connection
90
+ def connection_complete
91
+ @client = Syncro::Client.for(:server)
92
+ @client.connect(self)
93
+ @client.sync
94
+ end
95
+
96
+ def receive_data(data)
97
+ puts "Received: #{data}"
98
+ @client.receive_data(data)
99
+ end
100
+ end
101
+
102
+ SuperModel::Marshal.path = "dump.db"
103
+ SuperModel::Marshal.load
104
+
105
+ at_exit {
106
+ SuperModel::Marshal.dump
107
+ }
108
+
109
+ EM.run {
110
+ EM.connect("0.0.0.0", 10000, MyConnection)
111
+ }
112
+
113
+ == Protocol
114
+
115
+ Syncro uses a very simple JSON protocol.
116
+ Each message is a JSON hash. The only mandatory field is "type".
117
+ {"type" => "foo", ...}
118
+
119
+ Each message is preceded by a short int, representing the message size.
120
+ For example, in Ruby:
121
+ data = {:type => "foo", :bar => 1}
122
+ message = [data.length].pack('n') + data
123
+
124
+ At the moment, there are only two types of message:
125
+ * sync (args: from)
126
+ * add_scribe (args: scribe)
127
+
128
+ Have a look at app.rb for implementation details.
129
+
130
+ If the messages are split up already, i.e. you don't need the binary length preceding
131
+ the message, you can instead use the method @client.receive_message(json_string_or_hash)
132
+ This is particular useful for WebSocket clients.
133
+
134
+ == Roadmap
135
+
136
+ * A JavaScript Syncro client for web apps that have offline capabilities, and can use WebSockets to
137
+ sync with the server. WebSockets should be on the iPhone/iPad soon.
138
+ * Easier protocol extensions, for things like authenticating clients (which are done by subclassing atm).
@@ -0,0 +1,15 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "syncro"
5
+ gemspec.summary = "Sync Ruby classes between clients."
6
+ gemspec.email = "info@eribium.org"
7
+ gemspec.homepage = "http://github.com/maccman/syncro"
8
+ gemspec.description = "Sync Ruby classes between clients."
9
+ gemspec.authors = ["Alex MacCaw"]
10
+ gemspec.add_dependency("activesupport", ">=3.0.0.beta")
11
+ gemspec.add_dependency("supermodel", ">=0.0.1")
12
+ end
13
+ rescue LoadError
14
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
15
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.3
@@ -0,0 +1,2 @@
1
+ dump_client.db
2
+ dump_server.db
@@ -0,0 +1,45 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. lib syncro])
2
+ require "eventmachine"
3
+ require "supermodel"
4
+
5
+ class Test < SuperModel::Base
6
+ include SuperModel::Marshal::Model
7
+ include Syncro::Model
8
+ end
9
+
10
+ class MyConnection < EM::Connection
11
+ def connection_complete
12
+ @client = Syncro::Client.for(:server)
13
+ @client.connect(self)
14
+ @client.sync
15
+ end
16
+
17
+ def receive_data(data)
18
+ puts "Received: #{data}"
19
+ @client.receive_data(data)
20
+ end
21
+
22
+ def send_data(data)
23
+ puts "Sending: #{data}"
24
+ super(data)
25
+ end
26
+
27
+ def unbind
28
+ @client.disconnect
29
+ end
30
+ end
31
+
32
+ require "syncro/marshal"
33
+
34
+ SuperModel::Marshal.path = "dump_client.db"
35
+ SuperModel::Marshal.load
36
+
37
+ at_exit {
38
+ SuperModel::Marshal.dump
39
+ }
40
+
41
+ unless $0 =~ /irb/
42
+ EM.run {
43
+ EM.connect("0.0.0.0", 10000, MyConnection)
44
+ }
45
+ end
@@ -0,0 +1,49 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. lib syncro])
2
+ require "eventmachine"
3
+ require "supermodel"
4
+
5
+ class Test < SuperModel::Base
6
+ include SuperModel::Marshal::Model
7
+ include Syncro::Model
8
+
9
+ def scribe_clients
10
+ [:client_uuid1, :client_uuid2]
11
+ end
12
+ end
13
+
14
+ class MyConnection < EM::Connection
15
+ def post_init
16
+ @client = Syncro::Client.for(:client_uuid1)
17
+ @client.connect(self)
18
+ @client.sync
19
+ end
20
+
21
+ def receive_data(data)
22
+ puts "Received: #{data}"
23
+ @client.receive_data(data)
24
+ end
25
+
26
+ def send_data(data)
27
+ puts "Sending: #{data}"
28
+ super(data)
29
+ end
30
+
31
+ def unbind
32
+ @client.disconnect
33
+ end
34
+ end
35
+
36
+ require "syncro/marshal"
37
+
38
+ SuperModel::Marshal.path = "dump_server.db"
39
+ SuperModel::Marshal.load
40
+
41
+ at_exit {
42
+ SuperModel::Marshal.dump
43
+ }
44
+
45
+ unless $0 =~ /irb/
46
+ EM.run {
47
+ EM.start_server("0.0.0.0", 10000, MyConnection)
48
+ }
49
+ end
@@ -0,0 +1,31 @@
1
+ gem "supermodel"
2
+ require "supermodel"
3
+
4
+ gem "activesupport"
5
+ require "active_support"
6
+ require "active_support/core_ext/class/attribute"
7
+
8
+ module Syncro
9
+ class SyncroError < StandardError; end
10
+
11
+ def klasses
12
+ @klasses ||= []
13
+ end
14
+ module_function :klasses
15
+ end
16
+
17
+ $:.unshift(File.dirname(__FILE__))
18
+
19
+ require "syncro/app"
20
+ require "syncro/client"
21
+ require "syncro/base"
22
+ require "syncro/model"
23
+ require "syncro/protocol/message"
24
+ require "syncro/protocol/message_buffer"
25
+ require "syncro/response"
26
+ require "syncro/scriber"
27
+ require "syncro/scriber/base"
28
+ require "syncro/scriber/model"
29
+ require "syncro/scriber/observer"
30
+ require "syncro/scriber/scribe"
31
+ require "syncro/scriber/scribe_observer"
@@ -0,0 +1,92 @@
1
+ module Syncro
2
+ class App
3
+ attr_reader :client, :message
4
+
5
+ def initialize(client)
6
+ @client = client
7
+ end
8
+
9
+ def call(message)
10
+ @message = message
11
+ method = "invoke_#{message.type}"
12
+ send(method) if respond_to?(method)
13
+ end
14
+
15
+ def sync
16
+ invoke(:sync, :from => client.last_scribe_id) do |resp|
17
+ scribes = resp.map {|s|
18
+ scribe = Scriber::Scribe.new(s)
19
+ scribe.from_client = client.to_s
20
+ scribe
21
+ }
22
+ allowed_scribes = scribes.select {|s|
23
+ allowed_klasses.include?(s.klass)
24
+ }
25
+ allowed_scribes.each {|s| s.play }
26
+
27
+ if scribes.any?
28
+ client.update_attribute(
29
+ :last_scribe_id,
30
+ scribes.last.id
31
+ )
32
+ end
33
+ yield if block_given?
34
+ end
35
+ end
36
+
37
+ def add_scribe(scribe, &block)
38
+ invoke(
39
+ :add_scribe,
40
+ :scribe => scribe,
41
+ &block
42
+ )
43
+ end
44
+
45
+ protected
46
+ def invoke_sync
47
+ result = begin
48
+ if message[:from]
49
+ Scriber::Scribe.since(client, message[:from])
50
+ else
51
+ Scriber::Scribe.for_client(client)
52
+ end
53
+ end
54
+ respond(result)
55
+ end
56
+
57
+ def invoke_add_scribe
58
+ scribe = Scriber::Scribe.new(message[:scribe])
59
+ scribe.from_client = client.to_s
60
+ return unless allowed_klasses.include?(scribe.klass)
61
+ scribe.play
62
+ respond(true)
63
+ client.update_attribute(
64
+ :last_scribe_id,
65
+ scribe.id
66
+ )
67
+ end
68
+
69
+ def invoke_response
70
+ Response.call(client, message[:result])
71
+ end
72
+
73
+ def allowed_klasses
74
+ Syncro.klasses.map(&:to_s)
75
+ end
76
+
77
+ def invoke(type, hash = {}, &block)
78
+ message = Protocol::Message.new
79
+ message.type = type
80
+ message.merge!(hash)
81
+ Response.expect(client, &block)
82
+ client.send_message(message)
83
+ end
84
+
85
+ def respond(res = nil)
86
+ message = Protocol::Message.new
87
+ message.type = :response
88
+ message[:result] = res
89
+ client.send_message(message)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,8 @@
1
+ module Syncro
2
+ module Base
3
+ def self.included(base)
4
+ Syncro.klasses << base
5
+ base.send :include, Scriber::Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,83 @@
1
+ module Syncro
2
+ class Client < SuperModel::Base
3
+ include SuperModel::RandomID
4
+
5
+ class << self
6
+ def for(uid)
7
+ find_or_create_by_uid(uid)
8
+ end
9
+ end
10
+
11
+ attributes :uid, :connection, :last_scribe_id
12
+
13
+ def connected?
14
+ !!self.connection
15
+ end
16
+
17
+ def connect(connection)
18
+ self.connection = connection
19
+ end
20
+
21
+ def disconnect
22
+ end
23
+
24
+ def sync(&block)
25
+ app.sync(&block)
26
+ end
27
+
28
+ def add_scribe(scribe, &block)
29
+ app.add_scribe(scribe, &block)
30
+ end
31
+
32
+ def receive_data(data)
33
+ buffer << data
34
+ buffer.messages.each do |msg|
35
+ receive_message(msg)
36
+ end
37
+ end
38
+
39
+ def receive_message(data)
40
+ message = begin
41
+ case data
42
+ when String
43
+ Protocol::Message.fromJSON(data)
44
+ when Protocol::Message
45
+ data
46
+ else
47
+ Protocol::Message.new(data)
48
+ end
49
+ end
50
+ app.call(message)
51
+ end
52
+
53
+ def send_message(message)
54
+ return unless connected?
55
+ if connection.respond_to?(:send_message)
56
+ connection.send_message(message)
57
+ elsif connection.respond_to?(:send_data)
58
+ connection.send_data(message.serialize)
59
+ else
60
+ connection.write(message.serialize)
61
+ end
62
+ end
63
+
64
+ def to_s
65
+ (uid || id).to_s
66
+ end
67
+
68
+ def serializable_hash(options = {})
69
+ options[:except] ||= []
70
+ options[:except] << :connection
71
+ super(options)
72
+ end
73
+
74
+ protected
75
+ def app
76
+ @app ||= App.new(self)
77
+ end
78
+
79
+ def buffer
80
+ @buffer ||= Protocol::MessageBuffer.new
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,11 @@
1
+ module Syncro
2
+ class Client
3
+ include SuperModel::Marshal::Model
4
+ end
5
+
6
+ module Scriber
7
+ class Scribe
8
+ include SuperModel::Marshal::Model
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ module Syncro
2
+ module Model
3
+ def self.included(base)
4
+ base.send :include, Base
5
+ base.send :include, Scriber::Model
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ require "json"
2
+
3
+ module Syncro
4
+ module Protocol
5
+ class Message < HashWithIndifferentAccess
6
+ def self.fromJSON(str)
7
+ self.new(JSON.parse(str))
8
+ end
9
+
10
+ def type
11
+ self[:type].try(:to_sym)
12
+ end
13
+
14
+ def type=(sym)
15
+ self[:type] = sym
16
+ end
17
+
18
+ def serialize
19
+ data = self.to_json
20
+ [data.length].pack('n') + data
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ module Syncro
2
+ module Protocol
3
+ class MessageBuffer < StringIO
4
+ # How much left to be read
5
+ def left
6
+ size - pos
7
+ end
8
+
9
+ def back(n)
10
+ seek(n * -1, IO::SEEK_CUR)
11
+ end
12
+
13
+ def trim
14
+ string.replace(read)
15
+ end
16
+
17
+ def messages
18
+ messages = []
19
+ rewind
20
+ while !eof?
21
+ break unless left > 2
22
+ len = read_I16
23
+ msg = read(len)
24
+ if !msg || msg.length != len
25
+ back(2 + msg.length)
26
+ break
27
+ end
28
+ messages << msg
29
+ end
30
+ trim
31
+ wind
32
+ messages
33
+ end
34
+
35
+ def wind
36
+ seek(0, IO::SEEK_END)
37
+ end
38
+
39
+ private
40
+ def read_I16
41
+ dat = read(2)
42
+ len, = dat.unpack('n')
43
+ if (len > 0x7fff)
44
+ len = 0 - ((len - 1) ^ 0xffff)
45
+ end
46
+ len
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,2 @@
1
+ require "syncro/redis/client"
2
+ require "syncro/redis/scribe"
@@ -0,0 +1,27 @@
1
+ module Syncro
2
+ module Redis
3
+ class Syncro::Client
4
+ include SuperModel::Redis::Model
5
+ indexes :uid
6
+
7
+ # We need to hold the current connections
8
+ # in memory (as they can't be serialized).
9
+
10
+ class_attribute :connections
11
+ self.connections = {}
12
+
13
+ def connection
14
+ self.class.connections[self.id]
15
+ end
16
+
17
+ def connect(connection)
18
+ return unless self.id
19
+ self.class.connections[self.id] = connection
20
+ end
21
+
22
+ def disconnect
23
+ self.class.connections.delete(self.id)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ module Syncro
2
+ module Redis
3
+ class Syncro::Scriber::Scribe
4
+ include SuperModel::Redis::Model
5
+
6
+ class << self
7
+ def since(client, id)
8
+ items = redis.zrange(redis_key(:clients, client), id, -1)
9
+ items += redis.zrange(redis_key(:clients, :all), id, -1)
10
+ items = from_ids(items)
11
+ items = items.reject {|item|
12
+ item.from_client == client.to_s
13
+ }
14
+ items
15
+ end
16
+
17
+ def for_client(client)
18
+ since(client, 0)
19
+ end
20
+ end
21
+
22
+ serialize :data, :clients
23
+
24
+ after_save :index_clients
25
+
26
+ protected
27
+ def index_clients
28
+ if clients.blank?
29
+ redis.zadd(self.class.redis_key(:clients, :all), id, id)
30
+ else
31
+ clients.each {|client|
32
+ redis.zadd(self.class.redis_key(:clients, client), id, id)
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ module Syncro
2
+ class Response
3
+ cattr_accessor :callbacks
4
+ self.callbacks = {}
5
+
6
+ def self.expect(client, prok = nil, &block)
7
+ self.callbacks[client] ||= []
8
+ self.callbacks[client] << (prok||block)
9
+ end
10
+
11
+ def self.call(client, args)
12
+ callback = self.callbacks[client].shift
13
+ callback && callback.call(args)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ module Syncro
2
+ module Scriber
3
+ def klasses
4
+ @klasses ||= []
5
+ end
6
+ module_function :klasses
7
+ end
8
+ end
@@ -0,0 +1,28 @@
1
+ module Syncro
2
+ module Scriber
3
+ module Base
4
+ def self.included(base)
5
+ Scriber.klasses << base
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ def scribe_clients
10
+ :all
11
+ end
12
+
13
+ def scribe_create?
14
+ true
15
+ end
16
+
17
+ module ClassMethods
18
+ def record(type, options = {})
19
+ options.merge!({
20
+ :klass => self,
21
+ :type => type
22
+ })
23
+ Scribe.create(options)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ module Syncro
2
+ module Scriber
3
+ module Model
4
+ def self.included(base)
5
+ base.send :include, Base
6
+ base.extend ClassMethods
7
+ Observer.instance.add_observer!(base)
8
+ end
9
+
10
+ module ClassMethods
11
+ def scribe_play(scribe) #:nodoc:
12
+ return unless scribe_authorized?(scribe)
13
+ Observer.from(scribe.from_client) do
14
+ method = "scribe_play_#{scribe.type}"
15
+ send(method, scribe) if respond_to?(method)
16
+ end
17
+ end
18
+
19
+ def scribe_play_create(scribe)
20
+ create(scribe.data)
21
+ end
22
+
23
+ def scribe_play_update(scribe)
24
+ update(scribe.data[0], scribe.data[1])
25
+ end
26
+
27
+ def scribe_play_destroy(scribe)
28
+ destroy(scribe.data[0])
29
+ end
30
+
31
+ def scribe_authorized?(scribe)
32
+ true
33
+ end
34
+
35
+ def scribe_options(value = nil)
36
+ @scribe_options = value if value
37
+ @scribe_options ||= {}
38
+ @scribe_options
39
+ end
40
+ alias_method :scribe_options=, :scribe_options
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,127 @@
1
+ module Syncro
2
+ module Scriber
3
+ class Observer
4
+ include Singleton
5
+ class_attribute :observed_methods
6
+ self.observed_methods = []
7
+
8
+ class << self
9
+ def method_added(method)
10
+ return unless defined?(ActiveRecord)
11
+ self.observed_methods += [method] if ActiveRecord::Callbacks::CALLBACKS.include?(method.to_sym)
12
+ end
13
+
14
+ def from_client
15
+ @from_client
16
+ end
17
+
18
+ def from(client, &block)
19
+ @from_client = client
20
+ result = yield
21
+ @from_client = nil
22
+ result
23
+ end
24
+ end
25
+
26
+ def after_create(rec)
27
+ rec.class.record(
28
+ :create,
29
+ :data => rec.attributes,
30
+ :clients => active_clients(rec),
31
+ :from_client => from_client
32
+ )
33
+ end
34
+
35
+ def after_update(rec)
36
+ changed_to = rec.changes.inject({}) {|hash, (key, (from, to))|
37
+ hash[key] = to
38
+ hash
39
+ }
40
+ rec.class.record(
41
+ :update,
42
+ :data => [rec.id, changed_to],
43
+ :clients => active_clients(rec),
44
+ :from_client => from_client
45
+ )
46
+ end
47
+
48
+ def after_destroy(rec)
49
+ rec.class.record(
50
+ :destroy,
51
+ :data => [rec.id],
52
+ :clients => active_clients(rec),
53
+ :from_client => from_client
54
+ )
55
+ end
56
+
57
+ def update(observed_method, object) #:nodoc:
58
+ return unless respond_to?(observed_method)
59
+ return unless allowed?(object, observed_method)
60
+ # Is sending to clients disabled, or no clients specified?
61
+ return unless scribe_clients(object)
62
+ # Clients specified, but no non-disabled clients to send too
63
+ return if scribe_clients(object).any? && active_clients(object).empty?
64
+ send(observed_method, object)
65
+ end
66
+
67
+ def observed_class_inherited(subclass) #:nodoc:
68
+ subclass.add_observer(self)
69
+ end
70
+
71
+ def add_observer!(klass)
72
+ klass.add_observer(self)
73
+
74
+ # For ActiveRecord
75
+ self.class.observed_methods.each do |method|
76
+ callback = :"_notify_observers_for_#{method}"
77
+ if (klass.instance_methods & [callback, callback.to_s]).empty?
78
+ klass.class_eval "def #{callback}; notify_observers(:#{method}); end"
79
+ klass.send(method, callback)
80
+ end
81
+ end
82
+ end
83
+
84
+ protected
85
+ def allowed?(object, observed_method)
86
+ return false unless object.scribe_create?
87
+
88
+ method = observed_method.to_s
89
+ method.gsub!(/before_|after_/, "")
90
+
91
+ options = object.class.scribe_options
92
+
93
+ options[:only] = Array.wrap(options[:only]).map { |n| n.to_s }
94
+ options[:except] = Array.wrap(options[:except]).map { |n| n.to_s }
95
+
96
+ if options[:only].any?
97
+ return false unless options[:only].include?(method)
98
+ end
99
+
100
+ if options[:except].any?
101
+ return false if options[:except].include?(method)
102
+ end
103
+ true
104
+ end
105
+
106
+ def from_client
107
+ self.class.from_client
108
+ end
109
+
110
+ def scribe_clients(object)
111
+ clients = object.scribe_clients
112
+ case clients
113
+ when :all
114
+ []
115
+ when Array
116
+ clients.empty? ? false : clients
117
+ else
118
+ false
119
+ end
120
+ end
121
+
122
+ def active_clients(object)
123
+ scribe_clients(object) - [from_client]
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,53 @@
1
+ module Syncro
2
+ module Scriber
3
+ class Scribe < SuperModel::Base
4
+ include SuperModel::RandomID
5
+
6
+ class << self
7
+ def since(client, id)
8
+ record = find(id)
9
+ index = records.index(record)
10
+ items = records.slice((index + 1)..-1)
11
+ return [] unless items
12
+ items = items.select {|item|
13
+ item.clients.blank? || item.clients.include?(client.to_s)
14
+ }
15
+ items = items.reject {|item|
16
+ item.from_client == client
17
+ }
18
+ items.dup
19
+ rescue SuperModel::UnknownRecord
20
+ []
21
+ end
22
+
23
+ def for_client(client)
24
+ items = records.values.select {|item|
25
+ item.clients.blank? || item.clients.include?(client.to_s)
26
+ }
27
+ items = items.reject {|item|
28
+ item.from_client == client.to_s
29
+ }
30
+ items.dup
31
+ end
32
+ end
33
+
34
+ attributes :klass, :type, :data, :clients, :from_client
35
+ validates_presence_of :klass, :type
36
+
37
+ def play
38
+ klass.constantize.scribe_play(self)
39
+ end
40
+
41
+ def klass=(klass)
42
+ write_attribute(:klass, klass.to_s)
43
+ end
44
+
45
+ def to_json(options = {})
46
+ options[:except] ||= []
47
+ options[:except] << :clients
48
+ options[:except] << :from_client
49
+ super(options)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ module Syncro
2
+ module Scriber
3
+ class ScribeObserver
4
+ include Singleton
5
+
6
+ def after_create(rec)
7
+ if rec.clients.blank?
8
+ clients = Client.all.reject {|client|
9
+ client.to_s == rec.from_client
10
+ }
11
+ else
12
+ clients = rec.clients.map {|r| Client.for(r) }
13
+ end
14
+
15
+ clients.each {|c| c.add_scribe(rec) }
16
+ end
17
+
18
+ def update(observed_method, object) #:nodoc:
19
+ send(observed_method, object) if respond_to?(observed_method)
20
+ end
21
+
22
+ def observed_class_inherited(subclass) #:nodoc:
23
+ subclass.add_observer(self)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ Syncro::Scriber::Scribe.add_observer(
30
+ Syncro::Scriber::ScribeObserver.instance
31
+ )
@@ -0,0 +1,72 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{syncro}
8
+ s.version = "0.0.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Alex MacCaw"]
12
+ s.date = %q{2010-03-11}
13
+ s.description = %q{Sync Ruby classes between clients.}
14
+ s.email = %q{info@eribium.org}
15
+ s.extra_rdoc_files = [
16
+ "README"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "README",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "examples/.gitignore",
24
+ "examples/test_client.rb",
25
+ "examples/test_server.rb",
26
+ "lib/syncro.rb",
27
+ "lib/syncro/app.rb",
28
+ "lib/syncro/base.rb",
29
+ "lib/syncro/client.rb",
30
+ "lib/syncro/marshal.rb",
31
+ "lib/syncro/model.rb",
32
+ "lib/syncro/protocol/message.rb",
33
+ "lib/syncro/protocol/message_buffer.rb",
34
+ "lib/syncro/redis.rb",
35
+ "lib/syncro/redis/client.rb",
36
+ "lib/syncro/redis/scribe.rb",
37
+ "lib/syncro/response.rb",
38
+ "lib/syncro/scriber.rb",
39
+ "lib/syncro/scriber/base.rb",
40
+ "lib/syncro/scriber/model.rb",
41
+ "lib/syncro/scriber/observer.rb",
42
+ "lib/syncro/scriber/scribe.rb",
43
+ "lib/syncro/scriber/scribe_observer.rb",
44
+ "syncro.gemspec"
45
+ ]
46
+ s.homepage = %q{http://github.com/maccman/syncro}
47
+ s.rdoc_options = ["--charset=UTF-8"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.3.6}
50
+ s.summary = %q{Sync Ruby classes between clients.}
51
+ s.test_files = [
52
+ "examples/test_client.rb",
53
+ "examples/test_server.rb"
54
+ ]
55
+
56
+ if s.respond_to? :specification_version then
57
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
+ s.specification_version = 3
59
+
60
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
61
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0.beta"])
62
+ s.add_runtime_dependency(%q<supermodel>, [">= 0.0.1"])
63
+ else
64
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta"])
65
+ s.add_dependency(%q<supermodel>, [">= 0.0.1"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta"])
69
+ s.add_dependency(%q<supermodel>, [">= 0.0.1"])
70
+ end
71
+ end
72
+
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: syncro
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 3
9
+ version: 0.0.3
10
+ platform: ruby
11
+ authors:
12
+ - Alex MacCaw
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-11 00:00:00 +00:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activesupport
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 3
29
+ - 0
30
+ - 0
31
+ - beta
32
+ version: 3.0.0.beta
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: supermodel
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ - 0
45
+ - 1
46
+ version: 0.0.1
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ description: Sync Ruby classes between clients.
50
+ email: info@eribium.org
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - README
57
+ files:
58
+ - .gitignore
59
+ - README
60
+ - Rakefile
61
+ - VERSION
62
+ - examples/.gitignore
63
+ - examples/test_client.rb
64
+ - examples/test_server.rb
65
+ - lib/syncro.rb
66
+ - lib/syncro/app.rb
67
+ - lib/syncro/base.rb
68
+ - lib/syncro/client.rb
69
+ - lib/syncro/marshal.rb
70
+ - lib/syncro/model.rb
71
+ - lib/syncro/protocol/message.rb
72
+ - lib/syncro/protocol/message_buffer.rb
73
+ - lib/syncro/redis.rb
74
+ - lib/syncro/redis/client.rb
75
+ - lib/syncro/redis/scribe.rb
76
+ - lib/syncro/response.rb
77
+ - lib/syncro/scriber.rb
78
+ - lib/syncro/scriber/base.rb
79
+ - lib/syncro/scriber/model.rb
80
+ - lib/syncro/scriber/observer.rb
81
+ - lib/syncro/scriber/scribe.rb
82
+ - lib/syncro/scriber/scribe_observer.rb
83
+ - syncro.gemspec
84
+ has_rdoc: true
85
+ homepage: http://github.com/maccman/syncro
86
+ licenses: []
87
+
88
+ post_install_message:
89
+ rdoc_options:
90
+ - --charset=UTF-8
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ requirements: []
108
+
109
+ rubyforge_project:
110
+ rubygems_version: 1.3.6
111
+ signing_key:
112
+ specification_version: 3
113
+ summary: Sync Ruby classes between clients.
114
+ test_files:
115
+ - examples/test_client.rb
116
+ - examples/test_server.rb