Last active
October 23, 2019 19:12
-
-
Save brentjanderson/4360857 to your computer and use it in GitHub Desktop.
NOTE: Not updated since early 2013 - likely will not work with modern EmberData. Ember.JS, ember-data, and socket.io adapter. Not as primitive as the initial version and it supports object creation/deletion/etc. Does not support bulk updates like the first one just to keep it simple. Does support ember-data revision 11 and does support queries/f…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*jshint browser:true */ | |
/*global DS:true, io:true, App:true */ | |
(function() { | |
'use strict'; | |
// Initializer for Models | |
window.Models = {}; | |
console.warn("Don't pollute the global namespace with Models!"); | |
var SOCKET = '/'; // Served off the root of our app | |
var TYPES = { | |
CREATE: "CREATE", | |
CREATES: "CREATES", | |
UPDATE: "UPDATE", | |
UPDATES: "UPDATES", | |
DELETE: "DELETE", | |
DELETES: "DELETES", | |
FIND: "FIND", | |
FIND_MANY: "FIND_MANY", | |
FIND_QUERY: "FIND_QUERY", | |
FIND_ALL: "FIND_ALL" | |
}; | |
DS.SocketAdapter = DS.RESTAdapter.extend({ | |
socket: undefined, | |
/* | |
* A hashmap of individual requests. Key/value pairs of a UUID | |
* and a hashmap with the parameters passed in based on the | |
* request type. Includes "requestType" and "callback" in addition. | |
* RequestType is simply an enum value from TYPES (Defined below) | |
* and callback is a function that takes two parameters: request and response. | |
* the `ws.on('ember-data`) method receives a hashmap with two keys: UUID and data. | |
* The UUID is used to fetch the original request from this.requests, and that request | |
* is passed into the request's callback with the original request as well. | |
* Finally, the request payload is removed from the requests hashmap. | |
*/ | |
requests: undefined, | |
generateUuid: function() { | |
var S4 = function (){ | |
return Math.floor( | |
Math.random() * 0x10000 // 65536 | |
).toString(16); | |
}; | |
return ( | |
S4() + S4() + "-" + | |
S4() + "-" + | |
S4() + "-" + | |
S4() + "-" + | |
S4() + S4() + S4() | |
); | |
}, | |
send: function(request) { | |
request.uuid = this.generateUuid(); | |
request.context = this; | |
this.get('requests')[request.uuid] = request; | |
var data = { | |
uuid: request.uuid, | |
action: request.requestType, | |
type: this.rootForType(request.type) | |
}; | |
if (request.record !== undefined) { | |
data.record = this.serialize(request.record, { includeId: true}); | |
} | |
this.socket.emit('ember-data', data); | |
}, | |
find: function (store, type, id) { | |
this.send({ | |
store: store, | |
type: type, | |
id: id, | |
requestType: TYPES.FIND, | |
callback: function(req, res) { | |
Ember.run(req.context, function(){ | |
this.didFindRecord(req.store, req.type, res, req.id); | |
}); | |
} | |
}); | |
}, | |
findMany: function (store, type, ids, query) { | |
// ids = this.serializeIds(ids); | |
this.send({ | |
store: store, | |
type: type, | |
ids: ids, | |
query: query, | |
requestType: TYPES.FIND_MANY, | |
callback: function(req, res) { | |
Ember.run(req.context, function(){ | |
this.didFindMany(req.store, req.type, res); | |
}); | |
} | |
}); | |
}, | |
findQuery: function(store, type, query, recordArray) { | |
this.send({ | |
store: store, | |
type: type, | |
query: query, | |
recordArray: recordArray, | |
requestType: TYPES.FIND_QUERY, | |
callback: function(req, res) { | |
Ember.run(req.context, function(){ | |
this.didFindQuery(req.store, req.type, res, req.recordArray); | |
}); | |
} | |
}); | |
}, | |
findAll: function(store, type, since) { | |
this.send({ | |
store: store, | |
type: type, | |
since: this.sinceQuery(since), | |
requestType: TYPES.FIND_ALL, | |
callback: function(req, res) { | |
Ember.run(req.context, function(){ | |
this.didFindAll(req.store, req.type, res); | |
}); | |
} | |
}); | |
}, | |
createRecord: function(store, type, record) { | |
this.send({ | |
store: store, | |
type: type, | |
record: record, | |
requestType: TYPES.CREATE, | |
callback: function(req, res) { | |
Ember.run(req.context, function(){ | |
this.didCreateRecord(req.store, req.type, req.record, res); | |
}); | |
} | |
}); | |
}, | |
createRecords: function(store, type, records) { | |
return this._super(store, type, records); | |
}, | |
updateRecord: function(store, type, record) { | |
this.send({ | |
store: store, | |
type: type, | |
record: record, | |
requestType: TYPES.UPDATE, | |
callback: function(req, res) { | |
Ember.run(req.context, function() { | |
this.didSaveRecord(req.store, req.type, req.record, res); | |
}); | |
} | |
}); | |
}, | |
updateRecords: function(store, type, records) { | |
return this._super(store, type, records); | |
}, | |
deleteRecord: function(store, type, record) { | |
this.send({ | |
store: store, | |
type: type, | |
record: record, | |
requestType: TYPES.DELETE, | |
callback: function(req, res) { | |
Ember.run(req.context, function() { | |
this.didSaveRecord(req.store, req.type, req.record, res); | |
}); | |
} | |
}); | |
}, | |
deleteRecords: function(store, type, records) { | |
return this._super(store, type, records); | |
}, | |
init: function () { | |
this._super(); | |
var context = this; | |
this.set('requests', {}); | |
var ws = io.connect('//' + location.host); | |
// For all standard socket.io client events, see https://github.com/LearnBoost/socket.io-client | |
/* | |
* Returned payload has the following key/value pairs: | |
* { | |
* uuid: [UUID from above], | |
* data: [payload response], | |
* } | |
*/ | |
ws.on('ember-data', function(payload) { | |
var uuid = payload.uuid; | |
var request = context.get('requests')[uuid]; | |
request.callback(request, payload.data); | |
// Cleanup | |
context.get('requests')[uuid] = undefined; | |
}); | |
ws.on('disconnect',function () { | |
}); | |
this.set('socket', ws); | |
} | |
}); | |
// Create ember-data datastore and define our adapter | |
App.store = DS.Store.create({ | |
revision: 11, | |
adapter: DS.SocketAdapter.create({}) | |
}); | |
// Convenience method for handling saves of state via the model. | |
DS.Model.reopen({ | |
save:function() { | |
App.store.commit(); | |
return this; | |
} | |
}); | |
}()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
/** | |
* Module dependencies. | |
*/ | |
"use strict"; | |
var express = require('express'), | |
routes = require('./routes'), | |
http = require('http'), | |
path = require('path'), | |
hbs = require ('hbs'), | |
models = require('./server/models/models'), | |
colors = require('colors'); | |
var app = express(); | |
app.configure(function(){ | |
app.set('port', process.env.PORT || 3000); | |
app.set('views', __dirname + '/server/views'); | |
app.engine('html', hbs.__express); | |
app.set('view engine', 'html'); | |
app.use(express.favicon()); | |
app.use(express.logger('dev')); | |
app.use(express.bodyParser()); | |
app.use(express.methodOverride()); | |
app.use(app.router); | |
app.use(express.static(path.join(__dirname, 'public'))); | |
}); | |
app.configure('development', function(){ | |
app.use(express.errorHandler()); | |
}); | |
app.get('/', routes.index); | |
var server = app.listen(app.get('port'), function(){ | |
var msg = "Express server listening on port " + app.get('port'); | |
console.log(msg.bold.cyan); | |
}); | |
/** Socket.IO server implementation **/ | |
var io = require('socket.io').listen(server); | |
var TYPES = { | |
CREATE: "CREATE", | |
CREATES: "CREATES", | |
UPDATE: "UPDATE", | |
UPDATES: "UPDATES", | |
DELETE: "DELETE", | |
DELETES: "DELETES", | |
FIND: "FIND", | |
FIND_MANY: "FIND_MANY", | |
FIND_QUERY: "FIND_QUERY", | |
FIND_ALL: "FIND_ALL" | |
}; | |
var modelActions = function(data) { | |
var capitalize = function(string) { | |
return string.charAt(0).toUpperCase() + string.slice(1); | |
} | |
var results = []; | |
/** | |
* We extract the functions used in the for loop below | |
* into functionArray for optimization purposes. | |
* This also makes it pass JSHint | |
**/ | |
var functionArray = { | |
CREATE: function(callback) { | |
models[capitalize(data.type)].create(data.record, function(err, newModel) { | |
if (err) { | |
callback(err, null); | |
} else { | |
callback(null, newModel); | |
} | |
}); | |
}, | |
UPDATE: function(callback) { | |
models[capitalize(data.type)].update({_id: data.record.id}, { $set: data.record}, callback); | |
}, | |
DELETE: function(callback) { | |
models[capitalize(data.type)].remove({ _id: data.record.id}, function(err) { | |
callback(err, null); | |
}); | |
}, | |
FIND_ALL: function(callback) { | |
models[capitalize(data.type)].find({}, callback); | |
} | |
}; | |
switch(data.action) { | |
case TYPES.CREATE: | |
results.push(functionArray.CREATE); | |
break; | |
case TYPES.UPDATE: | |
results.push(functionArray.UPDATE); | |
break; | |
case TYPES.DELETE: | |
results.push(functionArray.DELETE); | |
break; | |
case TYPES.FIND_ALL: | |
results.push(functionArray.FIND_ALL); | |
break; | |
default: | |
throw "Unknown action " + data.action; | |
} | |
return results; | |
}; | |
var async = require('async'); | |
io.sockets.on('connection', function(socket) { | |
socket.on('ember-data', function(data) { | |
if (data.record !== undefined) { | |
data.data = [data.record]; | |
} | |
var actions = modelActions(data); | |
async.parallel(actions, function(err, results) { | |
if (err) { | |
console.warn(err); | |
} | |
switch (data.action) { | |
case TYPES.CREATE: | |
case TYPES.UPDATE: | |
case TYPES.DELETE: | |
var payload = {}; | |
payload[data.type] = results[0]; | |
results = payload; | |
break; | |
case TYPES.FIND_ALL: | |
var payload = {}; | |
var rows = results[0] | |
for (var i = 0; i < rows.length; i++) { | |
var row = rows[i].toObject({transform: function(doc, ret, options) { | |
delete ret.__v; | |
ret.id = ret._id; | |
delete ret._id; | |
}}); | |
console.log('ROW::',row); | |
rows[i] = row; | |
} | |
payload[data.type + 's'] = rows; | |
results = payload; | |
break; | |
default: | |
throw "Unknown action " + data.action; | |
} | |
var response = { | |
uuid: data.uuid, | |
action: data.action, | |
type: data.type, | |
data: results | |
}; | |
console.log("RESPONSE::",response); | |
socket.emit('ember-data', response); | |
}); | |
}); | |
}); | |
}()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "ember-socket.io-adapter", | |
"version": "0.0.1", | |
"private": false, | |
"scripts": { | |
"start": "node app" | |
}, | |
"dependencies": { | |
"socket.io": "*", | |
"express": "3.x", | |
"hbs": "2.0.x", | |
"handlebars": "*", | |
"mongoose": "3.5.x", | |
"async": "0.1.22", | |
"colors": "~0.6.0-1" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@brentjanderson: Thanks for this! I have some questions:
I am trying to implement a generic websockets-push, where the server can send new/updated/deleted data to the connected clients. I was under the assumption that your example implements such a strategy, but upon closer look now it seems to me that what you have implemented is the standard front-end initiated communication that REST adapter in ember-data provides, but instead of using http you are using websockets for that. Is my understanding correct?
And as a last question: if this is just a standard request-reply implementation (using ws instead of http), what is the goal of this? What does ws provide for you that http does not? Is this just to avoid re-opening the request channel for http?
In my case: ws allows me to have an open channel between the server and the client which can be used by the server to push new data (created / updated / deleted) to the connected clients without the clients actively requesting it. That means that the clients can be automatically informed that there are changes server side.