DAN ZEN EXPO - CODE EXHIBIT -
ZIM SOCKET
// NODEJS SETUP
var express = require("express");
var app = express();
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Headers", "Content-Type");
res.header("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS");
next();
});
var http = require("http").Server(app);
var io = require("socket.io")(http);
// ZIM SERVER (for ZIM Socket Module - http://zimjs.com)
// ZIM Server handles this on the server side with NodeJS and SocketIO
// based on RobinFlash Server in PHP by Dan Zen 2006
// updated to NodeJS by Andrew Blackbourn 2013
// programmed fresh with SocketIO by Dan Zen 2015 - free to use and change
// SocketIO handles apps, sockets and rooms
// the apps below are not really apps in the SocketIO sense
// but rather an appName prefix followed by the roomRoot and a number suffix
// appName_roomRoot0, appName_roomRoot1, appName_roomRoot2, etc.
// if no roomRoot is provided, then "default" is used
// the term roomRoot is used to avoid confusion with a room
// for example, test is the roomRoot of the room test2
// ZIM Server handles the maximum number of people in the room (0 is unlimited)
// and also handles how empty spots from people leaving get filled by the next to join
// these are the parameters sent in as a data object when a person joins
// it also handles changing rooms by changing the roomRoot
// it does not let you change rooms within a roomRoot
// JOIN AND LEAVE
// server sends out events to others when people join and leave
// DATA
// ZIM Server also handles receiving data and distributing data to people in the room
// the socket.id is used to key all this data
// the sender sends an object to the server and the server relays the object to the clients
// the clients then merge the object with a master object per client
// the server serves each new client with a current master object
// it is then up to the client to update the master object locally
// a sync command can be sent to request the master object from the server if desired
// HISTORY
// There is a history property for each room that can be appended to or cleared by people
// the history file is only sent when a new person joins
// this allows things like past chat histories to be sent to new clients
// LAST DATA
// Initially, we need to send who updated the properties last
// and who was the last person overall to update a property
// we send this to a new person in a room
// this is used in a variety of cases such as initially positioning a shared ball
// for regular data, the client is responsible for keeping track of the "last" data
// MASTER TIME
// there is a masterTime stamp based on the start of the server
// and a socket's joinTime for each time the socket enters a room
// and a currentTime is available by sending a time request
// PERSISTENT VARIABLES
// these will run as long as the server runs
// while the server runs, the apps are never cleared
// but the rooms inside an app may be cleared if all people have left the room for that app
var masterTime = Math.floor(Date.now() / 1000);
var apps = {}; // apps["appName"] = {roomRoot:{maxPeople:3, fill:true, rooms:["roomName", "roomName"]}, roomRoot2:{ etc }}
var rooms = {};
// rooms["roomName"] = {
// people:2, // current number
// gone:1, // how many have left the room
// history:"",
// sockets:{id:socket, id2:socket},
// current:{id:{x:10, y:20}, id2:{x:33, y:42}},
// last:{id:id, properties:{text:[id,"hi"], y:[id,10]}
// }
var clients = {}; // clients[socketID] = {app:appName, roomRoot:roomRoot, room:roomName}
// SOCKET IO CONNECTION
io.on("connection", function(socket) {
// socket gives us a reference to the socket that connects us to the client
// this variable is available for us while the client is connected
// socket.id gives us the unique id of the socket - like "fhSsasdfa4ULksjd5f"
// we create a client object inside of the clients object for each socket based on the id
// this will hold the app, the roomroot and the roomname
// this data gets added when the client joins and then afterwards,
// any events that come in like the message, history, disconnect, etc.
// can get access to the socket's room and app objects like so:
// rooms[clients[socket.id].room]
// apps[clients[socket.id].app]
// HANDLE NEW CLIENTS
socket.on("join", function(data) {
// if socket already exists - it is room change
// remove them from the last room
// add them to a new room - else add them to a new room (same way)
if (clients[socket.id]) removeFromRoom();
socket.emit("join", addToRoom(data));
sendOutData(data.initObj, "join");
function addToRoom(data) {
// setting app and room names to lowercase no spaces
var appName = data.appName; // name of app
if (zot(appName)) appName = "default";
appName = appName.toLowerCase();
appName = appName.replace(/\n/, "");
var roomRoot = data.roomName; // name of room without index on end
if (zot(roomRoot)) roomRoot = "default";
roomRoot = roomRoot.toLowerCase();
roomRoot = roomRoot.replace(/\n/, "");
var app; // object
var room; // object
var roomName; // name of room with index on end
if (!apps[appName]) {
app = {maxPeople:data.maxPeople, fill:data.fill, rooms:[]};
apps[appName] = {};
apps[appName][roomRoot] = app;
} else {
if (!apps[appName][roomRoot]) {
app = {maxPeople:data.maxPeople, fill:data.fill, rooms:[]};
apps[appName][roomRoot] = app;
} else {
app = apps[appName][roomRoot];
// if these have changed, use current values
if (!zot(data.maxPeople)) app.maxPeople = data.maxPeople;
if (!zot(data.fill)) app.fill = data.fill;
}
}
var filled = false;
if (app.fill) {
for (var i=0; i<app.rooms.length; i++) {
roomName = app.rooms[i];
room = rooms[roomName];
if (!room) continue; // room may have been emptied
if (room.people < app.maxPeople) {
filled = true;
break;
}
}
}
// if the app is set to fill - might still not be filled if no empty spots
// or filled might be false because fill is false
if (!filled) { // add to latest room
if (app.rooms.length > 0) {
roomName = app.rooms[app.rooms.length-1];
room = rooms[roomName]; // this room may have been emptied
if (room && (app.maxPeople == 0 || room.people < app.maxPeople)) {
if (!app.fill && room.people + room.gone >= app.maxPeople) {
makeRoom();
}
// this is the room
} else {
makeRoom();
}
} else {
makeRoom();
}
}
function makeRoom() {
clientIndex = 0;
roomName = appName + "_" + roomRoot + app.rooms.length;
room = {people:0, gone:0, history:"", sockets:{}, current:{}, last:{}};
app.rooms.push(roomName);
rooms[roomName] = room;
}
function adjustLast(id, obj) {
// last:{id:id, properties:{text:[id,"hi"], y:[id,10]}
room.last.id = id;
if (!room.last.properties) room.last.properties = {};
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
room.last.properties[i] = [id,obj[i]];
}
}
room.last.properties.id = [id,id];
}
if (!data.initObj) data.initObj = {};
data.initObj.id = socket.id;
room.people++;
room.sockets[socket.id] = socket;
// will add initObj to current in sendOutData function
adjustLast(socket.id, data.initObj);
socket.join(roomName);
clients[socket.id] = {app:appName, roomRoot:roomRoot, room:roomName};
var joinTime = Math.floor(Date.now() / 1000);
return {id:socket.id, masterTime:masterTime, joinTime:joinTime, history:room.history, current:room.current, last:room.last};
}
});
// HANDLE RECEIVING AND DISTRIBUTING PROPERTIES
socket.on("message", function(data){
data.id = socket.id;
sendOutData(data, "message");
});
socket.on("time", function(){
var currentTime = Math.floor(Date.now() / 1000);
socket.emit("time", {masterTime:masterTime, currentTime:currentTime});
});
socket.on("sync", function(){
if (!clients[socket.id]) return;
var roomName = clients[socket.id].room;
var room = rooms[roomName];
if (!room) return;
var currentTime = Math.floor(Date.now() / 1000);
function filter(obj) { // copy current object and remove current client
var out = copy(obj);
delete out[socket.id];
return out;
}
socket.emit("sync", {id:socket.id, masterTime:masterTime, currentTime:currentTime, history:room.history, current:filter(room.current), last:room.last});
});
function sendOutData(data, type) {
// handles both a message send (for dispatching a data event)
// and a join send (for dispatching an otherjoin event)
// io.sockets.in(data.room).emit("receive", data); // to all
if (!clients[socket.id]) return;
var roomName = clients[socket.id].room;
socket.broadcast.to(roomName).emit("receive", data, type); // not to self
var room = rooms[roomName];
if (!room) return;
room.current[socket.id] = merge(room.current[socket.id], data);
if (type == "message") { // add to current and latest (already did this for join)
var some = false;
for (var property in data) {
some = true;
room.last.properties[property] = [socket.id, data[property]];
}
if (some) room.last.id = socket.id;
}
}
// HANDLE HISTORY
socket.on("history", function(data){
var client = clients[socket.id];
var room = rooms[client.room];
room.history += data;
});
socket.on("clearhistory", function(){
var client = clients[socket.id];
var room = rooms[client.room];
room.history = "";
});
// HANDLE DISCONNECTION
socket.on("disconnect", function() {
removeFromRoom();
});
function removeFromRoom() { // used in disconnect and changeroom
// need to distribute sender properties to all receivers in room
// these would only distribute the same message to receivers so do not use them
// io.sockets.in(data.room).emit("receive", data.message); // to all
var clientLeaving = clients[socket.id];
if (!clientLeaving) return;
var roomName = clientLeaving.room;
socket.leave(roomName);
socket.broadcast.to(roomName).emit("otherleave", socket.id); // not to self
var room = rooms[roomName];
if (room) {
delete room.sockets[socket.id];
room.people--;
room.gone++;
var emptyCheck = true;
for (var i in room.sockets) {
emptyCheck = false;
break;
}
if (emptyCheck) { // everyone is gone from room
rooms[roomName] = null;
var allEmptyCheck = true;
var appRooms = apps[clientLeaving.app][clientLeaving.roomRoot].rooms;
for (i=0; i<appRooms.length; i++) {
if (rooms[appRooms[i]]) { // the room has not been emptied
allEmptyCheck = false;
break;
}
}
if (allEmptyCheck) { // all rooms empty, clear rooms for app
apps[clientLeaving.app][clientLeaving.roomRoot].rooms = [];
}
clients[socket.id] = null;
return;
}
delete room.current[socket.id];
// leave last potentially with old id in data
}
clients[socket.id] = null;
}
/*
socket.on("error", function(e) {
zog(e);
});
*/
});
// HELPER FUNCTIONS
function zog(t) {
console.log(t);
}
function zot(v) {
if (v === null) return true;
return typeof v === "undefined";
}
function copy(o) {
if (typeof o === "string" || typeof o === "number") return o;
var out, v, key;
out = Array.isArray(o) ? [] : {};
for (key in o) {
if (o.hasOwnProperty(key)) {
v = o[key];
out[key] = (typeof v === "object") ? copy(v) : v;
}
}
return out;
}
function merge() {
var obj = {}; var i; var j;
for (i=0; i<arguments.length; i++) {
for (j in arguments[i]) {
if (arguments[i].hasOwnProperty(j)) {
obj[j] = arguments[i][j];
}
}
}
return obj;
}
// START THE SERVER
http.listen(3000, function() {
zog("ZIM Server listening on port 3000");
});