syncro 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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