├── .gitignore ├── README.md ├── index.js ├── lib ├── browser │ └── object-sync-client.coffee └── object-sync.coffee ├── package.json └── test ├── server.coffee └── static ├── coffee ├── demo.coffee └── object-sync-client.coffee ├── index.html ├── slkscr.ttf ├── slkscrb.ttf ├── slkscre.ttf ├── slkscreb.ttf └── sprites.png /.gitignore: -------------------------------------------------------------------------------- 1 | test/static/coffee/*.js 2 | .DS_Store -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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') -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/coffee/object-sync-client.coffee: -------------------------------------------------------------------------------- 1 | ../../../lib/browser/object-sync-client.coffee -------------------------------------------------------------------------------- /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 |
95 |
Say
96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /test/static/slkscr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonastemplestein/node-object-sync/e498b8236e12c5aebf8a0775386b3cfd18b5186e/test/static/slkscr.ttf -------------------------------------------------------------------------------- /test/static/slkscrb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonastemplestein/node-object-sync/e498b8236e12c5aebf8a0775386b3cfd18b5186e/test/static/slkscrb.ttf -------------------------------------------------------------------------------- /test/static/slkscre.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonastemplestein/node-object-sync/e498b8236e12c5aebf8a0775386b3cfd18b5186e/test/static/slkscre.ttf -------------------------------------------------------------------------------- /test/static/slkscreb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonastemplestein/node-object-sync/e498b8236e12c5aebf8a0775386b3cfd18b5186e/test/static/slkscreb.ttf -------------------------------------------------------------------------------- /test/static/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonastemplestein/node-object-sync/e498b8236e12c5aebf8a0775386b3cfd18b5186e/test/static/sprites.png --------------------------------------------------------------------------------