Skip to content

Instantly share code, notes, and snippets.

@brentjanderson
Last active October 23, 2019 19:12
Show Gist options
  • Save brentjanderson/4360857 to your computer and use it in GitHub Desktop.
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…
/*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;
}
});
}());
(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);
});
});
});
}());
{
"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"
}
}
@gonvaled
Copy link

@brentjanderson: Thanks for this! I have some questions:

  1. adapter.js is client side (ember + ember-data application, using a specially configured REST adapter to handle websockets data)
  2. app.js is node.js server-side?
  3. Who is initiating the data transfer? I was expecting to use this for server initiated communication, to push records to the front-end store, but looking more closely at your code the ember application seems to be requesting from the backend, and the callbacks in the frontend are just called when the reply from the backend arrives. So this is no push implementation?
  4. In the server side some request types are not implemented (only implemented CREATE, UPDATE, DELETE, FIND_ALL)
  5. In the client side not all parameters are evaluated: (ids, query, recordArray)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment