├── .gitignore
├── test
├── static
│ ├── coffee
│ │ ├── object-sync-client.coffee
│ │ └── demo.coffee
│ ├── slkscr.ttf
│ ├── slkscrb.ttf
│ ├── slkscre.ttf
│ ├── slkscreb.ttf
│ ├── sprites.png
│ └── index.html
└── server.coffee
├── index.js
├── package.json
├── lib
├── object-sync.coffee
└── browser
│ └── object-sync-client.coffee
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | test/static/coffee/*.js
2 | .DS_Store
--------------------------------------------------------------------------------
/test/static/coffee/object-sync-client.coffee:
--------------------------------------------------------------------------------
1 | ../../../lib/browser/object-sync-client.coffee
--------------------------------------------------------------------------------
/test/static/slkscr.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonastemplestein/node-object-sync/HEAD/test/static/slkscr.ttf
--------------------------------------------------------------------------------
/test/static/slkscrb.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonastemplestein/node-object-sync/HEAD/test/static/slkscrb.ttf
--------------------------------------------------------------------------------
/test/static/slkscre.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonastemplestein/node-object-sync/HEAD/test/static/slkscre.ttf
--------------------------------------------------------------------------------
/test/static/slkscreb.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonastemplestein/node-object-sync/HEAD/test/static/slkscreb.ttf
--------------------------------------------------------------------------------
/test/static/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonastemplestein/node-object-sync/HEAD/test/static/sprites.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // This is how it works:
2 | // You put the may down, and then you jump to conclusions
3 | require('coffee-script')
4 | module.exports = require('./lib/object-sync')
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "object-sync",
3 | "version": "0.1.1",
4 | "description": "Transparently synchronize objects accross many connected clients.",
5 | "author": {
6 | "name": "Jonas Huckestein",
7 | "email": "jonas.huckestein@gmail.com",
8 | "url": "http://thezukunft.com"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "http://github.com/jonashuckestein/node-object-sync.git"
13 | },
14 | "main": "./index.js",
15 | "dependencies": {
16 | "coffee-script": ">= 1.0.0",
17 | "socket.io": ">=0.6.8"
18 | }
19 | }
--------------------------------------------------------------------------------
/test/server.coffee:
--------------------------------------------------------------------------------
1 | # Don't get too excited, this is not a real test!
2 |
3 | connect = require 'connect'
4 |
5 | ObjectSync = require '../lib/object-sync'
6 |
7 | server = connect.createServer()
8 |
9 | id_counter = 1
10 |
11 | # local object database
12 | objects = {}
13 | object_owners = {}
14 |
15 |
16 | sync = ObjectSync.listen server,
17 | destroy: (id, sid, callback) ->
18 | if not objects[id]
19 | return callback
20 | code: 'invalid_id'
21 | delete objects[id]
22 | if object_owners[sid]
23 | idx = object_owners[sid].indexOf id
24 | object_owners[sid].splice idx, 1 if idx isnt -1
25 | delete object_owners[sid] if not object_owners[sid].length
26 | callback null
27 |
28 | update: (obj, sid, callback) ->
29 | if objects[obj.id]
30 | same = true
31 | for prop, val of obj when objects[obj.id][prop] isnt val
32 | same = false
33 | objects[obj.id][prop] = val
34 | callback null, objects[obj.id], not same
35 | else callback
36 | code: 'invalid_id'
37 |
38 | create: (obj, sid, callback) ->
39 | obj.id = id_counter++
40 | objects[obj.id] = obj
41 |
42 | object_owners[sid] or= []
43 | object_owners[sid].push obj.id
44 |
45 | callback null, obj
46 |
47 | fetch: (ids, sid, callback) ->
48 | results = []
49 | for id in ids
50 | results.push (objects[id] or null)
51 | callback null, results
52 |
53 |
54 | # remove all objects the disconnecting player created
55 | sync.on 'disconnect', (sid) ->
56 | if object_owners[sid]
57 | ids = [].concat(object_owners[sid])
58 | sync.destroy id for id in ids
59 |
60 | # serve list of of game objects to new clients
61 | server.use '/init', (req, res, next) ->
62 | keys = Object.keys objects
63 | response = JSON.stringify keys
64 | headers =
65 | 'Content-Type': 'application/json; charset=utf-8'
66 | 'Content-Length': Buffer.byteLength response
67 | res.writeHead 200, headers
68 | res.end response
69 |
70 |
71 | server.use '/coffee', connect.compiler
72 | src: './static/coffee'
73 | enable: ['coffeescript']
74 |
75 | server.use '/', connect.staticProvider './static'
76 | server.listen 80
77 | module.exports = server
--------------------------------------------------------------------------------
/test/static/coffee/demo.coffee:
--------------------------------------------------------------------------------
1 | sync = new ObjectSync()
2 |
3 | me = null
4 | $talk = $('#talk')
5 | $input = $talk.find('input')
6 | mePlayer = -> $(".player_#{me}").data('player')
7 |
8 | showTalk = ->
9 | if not $talk.is(':visible')
10 | $talk.show()
11 | $input.focus()
12 |
13 | submitTalk = ->
14 | return unless $talk.is(':visible')
15 | new_say = $('#talk input').val()
16 | return if new_say is ''
17 | hideTalk()
18 | player = mePlayer()
19 | player.says = new_say
20 | sync.save player, (err) ->
21 | console.error err if err
22 |
23 | hideTalk = ->
24 | $talk.fadeOut('fast')
25 | $input.val('').blur()
26 |
27 | installHandlers = ->
28 | arrow = {left: 37, up: 38, right: 39, down: 40}
29 | $(window).keydown (e) ->
30 | key = e.keyCode or e.which
31 | old_player = mePlayer()
32 | player =
33 | id: old_player.id
34 | switch key
35 | when arrow.up
36 | player.y = old_player.y - 10
37 | sync.save player
38 | when arrow.right
39 | player.x = old_player.x + 10
40 | sync.save player
41 | when arrow.down
42 | player.y = old_player.y + 10
43 | sync.save player
44 | when arrow.left
45 | player.x = old_player.x - 10
46 | sync.save player
47 | else
48 | showTalk()
49 |
50 | $(window).keyup (e) ->
51 | key = e.keyCode or e.which
52 | switch key
53 | when arrow.left, arrow.right, arrow.up, arrow.down
54 | return
55 | when 13 # return
56 | submitTalk()
57 | when 27 # esc
58 | hideTalk()
59 |
60 |
61 | clear = ->
62 | $('.player').remove()
63 | me = null
64 |
65 |
66 | drawPlayer = (player) ->
67 | # is that player already on the screen?
68 | $player = $(".player_#{player.id}")
69 | if not $player.length
70 | $player = $('
')
71 | .addClass('player')
72 | .addClass("player_#{player.id}")
73 | .append($('
'))
74 | .append($('
'))
75 | .appendTo('body')
76 | if player.id is me
77 | $player.addClass('me')
78 | $player.data 'player', player
79 | $player.css
80 | top: player.y
81 | left: player.x
82 | $says = $player.find('.says')
83 | old_says = $says.text()
84 | if player.says isnt old_says
85 | $says.hide().fadeIn().text player.says
86 |
87 | removePlayer = (id) ->
88 | $(".player_#{id}").fadeOut -> $(@).remove()
89 |
90 |
91 | initialize = ->
92 |
93 | $.getJSON '/init', (result) ->
94 | sync.fetch result, (err, objects) ->
95 | if err
96 | console.error err
97 | return alert('boo, that didnt work')
98 | for player in objects
99 | drawPlayer player
100 |
101 | createPlayer = ->
102 |
103 | player =
104 | x: Math.floor(Math.random()*400) + 11
105 | y: Math.floor(Math.random()*300) + 71
106 | says: "I'm new here, talk to me"
107 | avatar_type: Math.floor(Math.random()*5) + 1
108 |
109 | sync.save player, (err, player) ->
110 | if err
111 | console.error err
112 | return alert 'Awwwww. no. *dies*'
113 | me = player.id
114 |
115 | $ ->
116 |
117 | sync.connect()
118 |
119 | sync.on 'create', (obj) -> drawPlayer obj
120 | sync.on 'update', (obj) -> drawPlayer obj
121 | sync.on 'destroy', (id) -> removePlayer id
122 |
123 | sync.on 'connect', ->
124 | clear()
125 | initialize()
126 | installHandlers()
127 | createPlayer()
128 |
129 | window.sync = sync
130 |
131 |
--------------------------------------------------------------------------------
/test/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
90 |
91 |
92 |
Walk and talk arrow keys to move, any other key to talk
93 |
94 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/lib/object-sync.coffee:
--------------------------------------------------------------------------------
1 | socket_io = require 'socket.io'
2 | EventEmitter = require('events').EventEmitter
3 |
4 |
5 | class ObjectSync extends EventEmitter
6 |
7 |
8 | # Wraps a server and returnes ObjectSync object.
9 | @listen: (http_server, options={}) ->
10 | options.server = http_server
11 | sync = new ObjectSync options
12 | sync.listen()
13 | return sync
14 |
15 | constructor: (options) ->
16 | super()
17 | @options =
18 | server: null
19 | update: => @log 'missing update handler', arguments[0]
20 | create: => @log 'missing create handler', arguments[0]
21 | destroy: => @log 'missing destroy handler', arguments[0]
22 | fetch: => @log 'missing fetch handler', arguments[0]
23 |
24 | for key, val of options
25 | @options[key] = val
26 |
27 | for action in ['update', 'create', 'destroy', 'fetch']
28 | @setHandler action, @options[action]
29 |
30 | # Starts listening on the wrapped server. If no server was passed
31 | # a new socket.io server is created
32 | #
33 | # TODO: remove dependency on HTTP server
34 | listen: ->
35 | throw new Error 'No server in options!' unless @options.server
36 |
37 | @log 'hooking up to server. booyah.'
38 | @_socket = socket_io.listen @options.server
39 |
40 | @_socket.on 'clientConnect', @_onConnect
41 |
42 | @_socket.on 'clientMessage', @_onMessage
43 |
44 | @_socket.on 'clientDisconnect', @_onDisconnect
45 |
46 | save: (obj, cb) ->
47 | if obj.id then @_update obj, 0, (cb or ->)
48 | else @_create obj, 0, (cb or ->)
49 | destroy: (id, cb) -> @_destroy id, 0, (cb or ->)
50 | fetch: (ids, cb) -> @_fetch ids, 0, (cb or ->)
51 |
52 | # Message =
53 | # type: 'save' or 'destroy' or 'fetch'
54 | # obj: the object to save
55 | # id: the ids to destroy/fetch
56 | # client_req_id: id the client uses to reference this request for cb
57 | # If the object has no id, it will be created, otherwise updated
58 | _onMessage: (msg, client) =>
59 | if not (typeof client is 'object' and msg.type and msg.client_req_id)
60 | return @log new Error('invalid message received'), arguments
61 |
62 | # construct cb function that will respond directly to the client
63 | # TODO obfuscate stack trace
64 | client_cb = (err, result) =>
65 | response =
66 | code: 'ok'
67 | result: result
68 | type: 'response'
69 | client_req_id: msg.client_req_id
70 | if err
71 | response.code = 'error'
72 | response.error = err
73 | client.send response
74 |
75 | switch msg.type
76 | when 'save'
77 | if typeof msg.obj.id is 'undefined'
78 | @_create msg.obj, client, client_cb
79 | else @_update msg.obj, client, client_cb
80 | when 'destroy'
81 | @_destroy msg.id, client, client_cb
82 | when 'fetch'
83 | @_fetch msg.id, client, client_cb
84 |
85 | _onDisconnect: (client) =>
86 | @emit 'disconnect', client.sessionId
87 |
88 | _onConnect: (client) =>
89 | @emit 'connect', client.sessionId
90 |
91 | _broadcast: (payload) ->
92 | response =
93 | code: 'ok'
94 | for key, val of payload
95 | response[key] = val
96 |
97 | @_socket.broadcast response
98 |
99 |
100 | _fetch: (ids, client, client_cb) =>
101 | @_handle 'fetch', [ids, client.sessionId], client_cb
102 |
103 | _destroy: (id, client, client_cb) =>
104 | @_handle 'destroy', [id, client.sessionId], (err, fire_event=true) =>
105 | client_cb arguments...
106 | if fire_event and not err
107 | @_broadcast
108 | type: 'destroy'
109 | id: id
110 |
111 | _create: (obj, client, client_cb) =>
112 | @_handle 'create', [obj, client.sessionId], (err, obj, fire_event=true) =>
113 | client_cb arguments...
114 | if fire_event and not err
115 | @_broadcast
116 | type: 'create'
117 | obj: obj
118 |
119 | _update: (obj, client, client_cb) =>
120 | @_handle 'update', [obj, client.sessionId], (err, obj, fire_event=true) =>
121 | client_cb arguments...
122 | if fire_event and not err
123 | @_broadcast
124 | type: 'update'
125 | obj: obj
126 |
127 |
128 |
129 | # Sets the function to handle event of type ev. Possible event types
130 | # are fetch, create, update, destroy.
131 | #
132 | # Parameters for handlers: create and update take an object and destroy
133 | # and fetch take an id. All handlers take a callback as last param.
134 | setHandler: (ev, handler) ->
135 | @_handlers or= {}
136 | @_handlers[ev] = handler
137 |
138 | _handle: (ev, args, cb) ->
139 | #try
140 | @_handlers[ev](args..., cb)
141 | #catch e
142 | # cb e
143 | log: ->
144 | console.log arguments...
145 |
146 |
147 |
148 | module.exports = ObjectSync
--------------------------------------------------------------------------------
/lib/browser/object-sync-client.coffee:
--------------------------------------------------------------------------------
1 | window.console or= {}
2 | console.log or= ->
3 | console.error or= ->
4 | console.trace or= ->
5 | console.dir or= ->
6 |
7 | if not Array.indexOf
8 | Array.prototype.indexOf = (obj) ->
9 | for i in [0..@length]
10 | if this[i] is obj
11 | return i
12 | return -1
13 |
14 | isArray = Array.isArray or (obj) ->
15 | obj.constructor.toString().indexOf("Array") isnt -1
16 |
17 | default_max_listeners = 10
18 | class EventEmitter
19 |
20 | setMaxListeners: (n) ->
21 | @_events.maxListeners = n
22 |
23 | emit: (type) ->
24 | if type is 'error'
25 | if not isArray(@_events?.error?) or not @_events?.error.length
26 | if arguments[1] instanceof Error
27 | throw arguments[1]
28 | else throw new Error arguments[1].code
29 | return false
30 |
31 | handler = @_events?[type]
32 | return false unless @_events?[type]
33 |
34 | if typeof handler is 'function'
35 | switch arguments.length
36 | # fast cases
37 | when 1 then handler.call @
38 | when 2 then handler.call @, arguments[1]
39 | when 3 then handler.call @, arguments[2]
40 | else
41 | args = Array.prototype.slice.call arguments, 1
42 | handler.apply @, args
43 | return true
44 | else if isArrayhandler
45 | args = Array.prototype.slice.call arguments, 1
46 | listeners = handler.slice()
47 | for listener in listeners
48 | listener.apply this, args
49 | else
50 | return false
51 |
52 |
53 | addListener: (type, listener) ->
54 | if typeof listener isnt 'function'
55 | throw new Error 'addListener only takes instances of Function'
56 |
57 | @_events or= {}
58 |
59 | @emit 'newListener', type, listener
60 |
61 | if not @_events[type]
62 | @_events[type] = listener
63 | else if isArray(@_events[type])
64 | if not @_events[type].warned
65 | m = 0
66 | if @_events.maxListeners isnt undefined
67 | m = @_events.maxListeners
68 | else m = default_max_listeners
69 | if m and m > 0 and @_events[type].length > m
70 | @_events[type].warned = true
71 | console.error "warning: possible EventEmitter memory" + \
72 | "leak detected. #{@_events[type].length} listeners"
73 | console.trace()
74 | @_events[type].push listener
75 | else
76 | @_events[type] = [@_events[type], listener]
77 |
78 | return @
79 |
80 |
81 | on: EventEmitter.prototype.addListener
82 |
83 | once: (type, listener) ->
84 | g = =>
85 | @removeListener type, g
86 | listener.apply @, arguments
87 | @on type, g
88 | return @
89 |
90 | removeListener: (type, listener) ->
91 | if typeof listener isnt 'function'
92 | throw new Error 'removeListener only takes instances of Function'
93 |
94 | list = @_events?[type]
95 | return @ unless list
96 |
97 | if isArray list
98 | i = list.indexOf listener
99 | return @ if i < 0
100 | list.splice i, 1
101 |
102 | if list.length is 0
103 | delete @_events[type]
104 | else if @_events[type] is listener
105 | delete @_events[type]
106 | return @
107 |
108 | removeAllListeners: (type) ->
109 | if type and @_events?[type]
110 | @_events[type] = null
111 | return this
112 |
113 | listeners: (type) ->
114 | @_events or= {}
115 | @_events[type] or= []
116 | if not isArray @_events[type]
117 | @_events[type] = [@_events[type]]
118 | return @_events[type]
119 |
120 |
121 | # Static functions use a singleton. Instantiate more instances if you want.
122 | class ObjectSync extends EventEmitter
123 |
124 | # @getSingleton: (options={}) ->
125 | # @_singleton or= new this(options)
126 | #
127 | # @connect: (options) -> @getSingleton().connect arguments...
128 | # @fetch: (id, cb) -> @getSingleton().fetch arguments...
129 | # @save: (obj, cb) -> @getSingleton().save arguments...
130 | # @destroy: (id, cb) -> @getSingleton().destroy arguments...
131 |
132 |
133 | constructor: (options={}) ->
134 | @options =
135 | auto_reconnect: true
136 | verbose: false
137 | reconnect_timeout: 1000
138 |
139 | for key, val of options
140 | @options[key] = val
141 |
142 | @_socket = new io.Socket
143 | @_socket.on 'connect', @_onConnect
144 | @_socket.on 'message', @_onMessage
145 | @_socket.on 'disconnect', @_onDisconnect
146 |
147 | @_reconnect_timer = null
148 | @_reconnect_attempts = 0
149 | @_request_counter = 1
150 | @_objects = {}
151 |
152 | allObjects: -> return @_objects
153 |
154 | fetch: (id, cb) ->
155 | id = [id] if not isArray id
156 | @_doRequest 'fetch', id, cb
157 |
158 | save: (obj, cb) ->
159 | @_doRequest 'save', obj, cb
160 |
161 | destroy: (id, cb) ->
162 | @_doRequest 'destroy', id, cb
163 |
164 |
165 | # Tries to connect to server until it succeeds
166 | #
167 | # TODO implement exponential backoff instead of linear
168 | connect: ->
169 | @_reconnect_timer = setTimeout (=>
170 | if not @_socket.connecting and not @_socket.connected
171 | @log 'attempting to connect' if @options.verbose
172 | @_socket.connect() # onConnect will invalidate the timeout
173 | @connect() if @options.auto_reconnect
174 | ), @_reconnect_attempts*1000
175 | @_reconnect_attempts += 1
176 |
177 | log: -> console.log arguments... if @options.verbose
178 |
179 | isConnected: -> @_socket.connected
180 |
181 | _onConnect: =>
182 | @log 'Connected', arguments if @options.verbose
183 | # reset some stuff
184 | @_reconnect_attempts = 0
185 | clearTimeout @_reconnect_timer
186 | @_reconnect_timer = null
187 | @emit 'connect'
188 |
189 | _onDisconnect: =>
190 | @log 'Disconnected', arguments if @options.verbose
191 | @connect() if @options.auto_reconnect
192 | @emit 'disconnect'
193 |
194 | _onMessage: (payload) =>
195 | @log 'Message', arguments if @options.verbose
196 | type = payload.type
197 |
198 | error = null
199 | if payload.code isnt 'ok' then error = payload.error
200 | result = payload.result
201 | @emit 'error', error if error
202 |
203 | # execute callback in case on is waiting
204 | @_callReqCallback payload.client_req_id, [error, result]
205 |
206 | # fire local events
207 | ev_param = payload.obj
208 |
209 | switch type
210 | when 'destroy'
211 | ev_param = payload.id
212 | delete @_objects[payload.id]
213 | when 'fetch', 'update', 'create'
214 | @_objects[payload.obj.id] = payload.obj
215 | @emit type, ev_param unless type is 'response'
216 |
217 |
218 | # TODO buffer requests if disconnected
219 | _doRequest: (type, obj_or_ids, cb) ->
220 | payload =
221 | type: type
222 | client_req_id: @_request_counter
223 | if type is 'fetch' or type is 'destroy'
224 | payload.id = obj_or_ids
225 | else payload.obj = obj_or_ids
226 | if typeof cb is 'function'
227 | @_registerReqCallback @_request_counter, cb
228 | @_request_counter++
229 | @_socket.send payload
230 |
231 | # TODO count pending cbs to detect leaks
232 | _registerReqCallback: (req_id, cb) ->
233 | @_req_callbacks or= {}
234 | @_req_callbacks[req_id] = cb
235 |
236 | _callReqCallback: (req_id, args) ->
237 | fn = @_req_callbacks[req_id]
238 | if typeof fn is 'function'
239 | fn.apply @, args
240 | delete @_req_callbacks[req_id]
241 |
242 | window.ObjectSync = ObjectSync
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | node-object-sync
2 | ================
3 |
4 | [Check out a running demo of the test/ directory here](http://98.158.186.218:8087/)
5 |
6 | node-object-sync transparently synchronizes objects across multiple connected clients. It's a low-level library that aims to take the pain out of synchronizing state across clients.
7 |
8 | Many webapps and games offer a 'multiplayer' components (for webapps that would be called collaboration or social feature). Many such apps use socket.io for realtime communication and some other framework on top of that to manage models (views, controllers and whatever else). Oftentimes the glue between model-land and socket.io on the client and socket.io and the database on the server is a bunch of complicated custom code. node-object-sync is an attempt to replace that code with something more generic.
9 |
10 | This is the first cut implementation. While there's still a lot to do, I thing this is already pretty useful. Check out the test directory for a little game that that has surprisingly little multiplayer networking code :)
11 |
12 | Note: All examples and the implementation are in [coffee-script](http://jashkenas.github.com/coffee-script/). CoffeeScript is great and you should probably use it. If you don't want to use CoffeeScript, you can easily convert it using the 'Try CoffeeScript' button on the aforementioned website.
13 |
14 | ***Any contributions are more than welcome***
15 |
16 | Design Notes
17 | ============
18 |
19 | node-object-sync allows you to maintain a consistent world state across a number of connected clients. If you modify an object locally, that change will transparently propagate to all connected clients.
20 |
21 | node-object-sync is designed to be a low-level library that belongs *underneath* your model layer. It doesn't (yet) deal with model types, atomic edits and other complicated stuff. The intention is that you can slip this in underneath frameworks like [backbone.js](http://github.com/documentcloud/backbone) or [javascriptmvc](http://github.com/jupiterjs/javascriptmvc).
22 |
23 | As far as node-object-sync is concerned, an entity has a unique id and arbitrary properties. There's nothing more to it. There is currently no support for collection types (lists, sets, maps). If you want to have different model classes, you can namespace your ids.
24 |
25 | Here's a list of bullet points:
26 |
27 | * Models destroy, update and create events appear transparently across all clients
28 | * Objects consist of an id and arbitrary primitive properties
29 | * Create events are only ever fired on the client if that client is connected when the object was created
30 | * socket.io is used for all communication
31 | * When changing objects, the client can pass a callback that gets executed after a response for that request has come through. In addition to that update, create and destroy events will fire.
32 | * Is database agnostic on the server side. You can even not use any database at all (use it to sync in-session objects among multiple clients)
33 |
34 | TL;DR show me some code
35 | ======================
36 |
37 | ***On the server***
38 |
39 | ObjectSync = require 'object-sync'
40 | server = http.createServer()
41 |
42 | # Hook an ObjectSync instance up to our server.
43 | # Define a bunch of handlers for CRUD events. The handlers are
44 | # async because they'll likely interact with some kind of database
45 | sync = ObjectSync.listen server,
46 |
47 | # a client wants to delete object with id id
48 | destroy: (id, client_sid, callback) ->
49 | console.log "client #{client_sid} has destroyed object #{id}"
50 | callback null # sends events to clients
51 |
52 | # a client wants to update an object
53 | update: (obj, client_sid, callback) ->
54 | # sends an error to the client requesting the update and no
55 | # message to all other clients
56 | callback
57 | code: 'invalid_id'
58 |
59 | # a client wants to create an object
60 | create: (obj, client_sid, callback) ->
61 | callback null, obj
62 |
63 | # a client requests a list of objects
64 | fetch: (ids, client_sid, callback) ->
65 | results = [] # ...
66 | callback null, results
67 |
68 | # The following functions let the server pro-actively change things
69 | sync.save obj, (err, obj) -> # ...
70 | sync.destroy obj, (err, obj) -> # ...
71 | sync.fetch ids, (err, objs) -> # ...
72 | sync.update obj, (err, obj) -> # ...
73 |
74 | ***On the client***
75 |
76 | The client will automatically reconnect if the server connection dies. For a more detailed, runnable example check out the test/ directory.
77 |
78 |
79 |
80 |
121 |
122 |
123 | TODO:
124 | =====
125 |
126 | Some take 5 minutes and some take 5 days.
127 |
128 | * Make changes about properties instead of resending the entire object. (this will fix the jerkiness in the demo if you go in two directions at once)
129 | * Consider moving a little bit of model layer magic into the client lib. For instance, objects should only be updated if they have actually changed.
130 | * Don't expose the entire world to every client. Let client's subscribe to objects.
131 | * Don't use new objects liberally. Use exactly one reference to an object with a certain id and modify it if it changes. This should prevent a few bugs and make an eventual GC routine easier to implement.
132 | * Let client have different state from global state with eventual consistency and rollbacks (local storage and offline mode FTW)
133 | * Handle collection properties (lists, sets)
134 | * Pass around diffs instead of full objectsto save bandwidth and make my life more complicated
135 | * Let servers talk to one another (aka make the server a full-size client)
136 | * Handle timeouts, crashes and other crazies
137 | * Perhaps support rigid JSON schemas (diffs will be flimsy to do otherwise)
138 | * Figure out how to deal with atomic counters. What if two clients change a value at the same time?
139 | * What if not all objects are visible in the same way to all clients?
140 | * Deal with custom socket.io endpoints (trivial)
141 | * Buffer communication for N minutes and resend all communication if clients drop for a little bit.
142 | * Buffer and resend requests on client if disconnected
143 | * Don't use /socket.io endpoint by default
144 | * Serve client files from server
145 | * Scale the whole shebang to MMORPG levels using RabbitMQ as routing backend
146 |
147 |
148 | License
149 | =======
150 |
151 | I haven't really made up my mind yet. Any reason not to go with MIT?
152 |
153 | (c) 2011 Jonas Huckestein
--------------------------------------------------------------------------------