From d9c1648e27530e115600a56a3e75553a99ebccf4 Mon Sep 17 00:00:00 2001 From: Pramod Kotipalli Date: Sun, 11 Sep 2016 03:04:25 -0400 Subject: [PATCH 1/5] Add support for data binding --- dist/channel-0.2.0.js | 220 ++++++++++++++++++++++++++++++++++++++++++ src/js/channel.js | 135 +++++++++++++++++++++----- src/ts/channel.ts | 196 +++++++++++++++++++++++++++---------- 3 files changed, 480 insertions(+), 71 deletions(-) create mode 100644 dist/channel-0.2.0.js diff --git a/dist/channel-0.2.0.js b/dist/channel-0.2.0.js new file mode 100644 index 0000000..b2c68f7 --- /dev/null +++ b/dist/channel-0.2.0.js @@ -0,0 +1,220 @@ +/** + * CHANNEL.JS - a simple Javascript front-end for Django Channels websocket applications + * + * This software is provided under the MIT License. + * + * @author PRAMOD KOTIPALLI [http://pramodk.net/, http://github.com/k-pramod] + * @version 0.2.0 + */ +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +/** + * Provides simple Javascript API for sending and receiving messages from web servers running Django Channel + */ +var Channel = (function () { + /** + * Constructs a new Channel + * + * @param webSocketPath + * The path on the server. The path should be specified **relative** to the host. + * For example, if your server is listening at http://ws.pramodk.net/chat/myRoomName/, + * you must provide the websocketPath as `'/chat/myRoomName/'` + * This approach eliminates the potential of CORS-related issues. + * @param pathType + * Tell what the type of the path is. + * Set to 'absolute' if you would like to send into the entire path of the websocket + */ + function Channel(webSocketPath, pathType) { + var _this = this; + if (pathType === void 0) { pathType = 'relative'; } + /** The client-specified functions that are called with a particular event is received */ + this._clientConsumers = { + // By default, we must specify the 'connect' consumer + 'connect': function (socket) { + console.info('Connected to Channel ' + socket._webSocketPath, socket); + }, + // And also the 'disconnect' consumer + 'disconnect': function (socket) { + console.info('Disconnected from WebSocket', socket); + } + }; + /** + * [Private method] Connects to the specified socket + * If you would like to connect to a websocket not hosted on your server + * + * @param wsPath + * The absolute path of the server socket + */ + this.connectTo = function (wsPath) { + _this._socket = new ReconnectingWebSocket(wsPath); + _this._webSocketPath = wsPath; + var _innerThis = _this; + // Hook up onopen event + _this._socket.onopen = function () { + _innerThis.callUponClient('connect', _innerThis); + }; + // Hook up onclose event + _this._socket.onclose = function () { + _innerThis.callUponClient('disconnect', _innerThis); + }; + // Hook up onmessage to the event specified in _clientConsumers + _this._socket.onmessage = function (message) { + var data = JSON.parse(message['data']); + if (data.stream) { + var payload = data.payload; + var event = BindingAgent.getBindingAgentKey(data.stream, payload.action); + data = payload.data; + data.model = payload.model; + data.pk = payload.pk; + _innerThis.callUponClient(event, data, data.stream); + } + else if (data.event) { + var event = data['event']; + delete data['event']; + _innerThis.callUponClient(event, data); + } + throw new ChannelError("Unknown action expected of client."); + }; + }; + /** + * [Private method] Calls upon the relevant action within _clientConsumers + * + * @param event The name of the event + * @param data The data to send to the consuming function + * @param eventDisplayName The name of the event to print if there is an error (used in data binding calls) + */ + this.callUponClient = function (event, data, eventDisplayName) { + if (eventDisplayName === void 0) { eventDisplayName = event; } + if (!(event in _this._clientConsumers)) { + throw new ChannelError("\"" + eventDisplayName + "\" not is a registered event." + + "Registered events include: " + + _this.getRegisteredEvents().toString() + ". " + + "Have you setup up " + + "socket_instance.on('eventName', consumer_function) ?"); + } + _this._clientConsumers[event](data); + }; + /** + * Handles messages from the server + * + * @param event The name of the event to listen to + * @param clientFunction The client-side Javascript consumer function to call + */ + this.on = function (event, clientFunction) { + _this._clientConsumers[event] = clientFunction; + }; + /** + * Sends a message to the socket server + * + * @param event The name of the event to send to the socket server + * @param data The data to send + */ + this.emit = function (event, data) { + data['event'] = event; + _this._socket.send(JSON.stringify(data)); + }; + /** + * Allows users to call .create, .update, and .destroy functions for data binding + * @param streamName The name of the stream to bind to + * @returns {BindingAgent} A new BindingAgent instance that takes care of registering the three events + */ + this.bind = function (streamName) { + return new BindingAgent(_this, streamName); + }; + var absolutePath; + if (pathType == 'relative') { + var socketScheme = window.location.protocol == "https:" ? "wss" : "ws"; + absolutePath = socketScheme + '://' + window.location.host + webSocketPath; + } + else if (pathType == 'absolute') { + absolutePath = webSocketPath; + } + else { + throw new ChannelError('Invalid pathType chosen'); + } + this.connectTo(absolutePath); + } + Channel.prototype.getRegisteredEvents = function () { + return Object.keys(this._clientConsumers); + }; + ; + return Channel; +}()); +/** + * Allows for client to register create, update, and destroy functions for data binding. + * Example: + * var bindingChannel = new Channel('/binding/'); + * bindingChannel.bind('room') + * .create(function(data) { ... }) + * .update(function(data) { ... }) + * .destroy(function(data) { ... }) + */ +var BindingAgent = (function () { + /** + * Constructor for the BindingAgent helper class. + * @param channel The Channel that this class is helping. + * @param streamName The name of the stream that this binding agent is supporting. + */ + function BindingAgent(channel, streamName) { + var _this = this; + this._streamName = null; + /** + * Registers a binding client consumer function + * @param bindingAction The name of the action to register + * @param clientFunction The function to register + */ + this.registerConsumer = function (bindingAction, clientFunction) { + if (BindingAgent.ACTIONS.indexOf(bindingAction) == -1) { + throw new ChannelError("You are trying to register an invalid action: " + + bindingAction + + ". Valid actions are: " + + BindingAgent.ACTIONS.toString()); + } + var bindingAgentKey = BindingAgent.getBindingAgentKey(_this._streamName, bindingAction); + _this._channel.on(bindingAgentKey, clientFunction); + }; + this.create = function (clientFunction) { + _this.registerConsumer('create', clientFunction); + return _this; + }; + this.update = function (clientFunction) { + _this.registerConsumer('update', clientFunction); + return _this; + }; + this.destroy = function (clientFunction) { + _this.registerConsumer('delete', clientFunction); + return _this; + }; + this._channel = channel; + this._streamName = streamName; + } + // The valid actions for users to call bind + BindingAgent.ACTIONS = ['create', 'update', 'delete']; + BindingAgent.GLUE = "559c09b44d6ff51559f14e87ad2b79ce"; // Hash of "http://pramodk.net" (^-^) + /** + * Gets the dictionary key used to call binding functions + * @param streamName The name of the stream + * @param action The name of the action + * @returns {string} A key that can be used to set and search keys in the Channel._clientConsumers dictionary + */ + BindingAgent.getBindingAgentKey = function (streamName, action) { + // Using the GLUE variable ensures that no regular event register with .on conflicts with the binding event + return streamName + " - " + BindingAgent.GLUE + " - " + action; + }; + return BindingAgent; +}()); +/** + * Errors from sockets. + */ +var ChannelError = (function (_super) { + __extends(ChannelError, _super); + function ChannelError(message) { + _super.call(this, message); + this.message = message; + this.name = 'ChannelError'; + } + return ChannelError; +}(Error)); diff --git a/src/js/channel.js b/src/js/channel.js index 84d775f..b2c68f7 100644 --- a/src/js/channel.js +++ b/src/js/channel.js @@ -1,25 +1,18 @@ +/** + * CHANNEL.JS - a simple Javascript front-end for Django Channels websocket applications + * + * This software is provided under the MIT License. + * + * @author PRAMOD KOTIPALLI [http://pramodk.net/, http://github.com/k-pramod] + * @version 0.2.0 + */ var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; -/** - * Errors from sockets. - */ -var ChannelError = (function (_super) { - __extends(ChannelError, _super); - function ChannelError(message) { - _super.call(this, message); - this.message = message; - this.name = "ChannelError"; - } - return ChannelError; -}(Error)); /** * Provides simple Javascript API for sending and receiving messages from web servers running Django Channel - * - * @author Pramod Kotipalli - * @version 0.1.1 */ var Channel = (function () { /** @@ -41,7 +34,7 @@ var Channel = (function () { this._clientConsumers = { // By default, we must specify the 'connect' consumer 'connect': function (socket) { - console.info('Connected to WebSocket', socket); + console.info('Connected to Channel ' + socket._webSocketPath, socket); }, // And also the 'disconnect' consumer 'disconnect': function (socket) { @@ -57,6 +50,7 @@ var Channel = (function () { */ this.connectTo = function (wsPath) { _this._socket = new ReconnectingWebSocket(wsPath); + _this._webSocketPath = wsPath; var _innerThis = _this; // Hook up onopen event _this._socket.onopen = function () { @@ -69,9 +63,20 @@ var Channel = (function () { // Hook up onmessage to the event specified in _clientConsumers _this._socket.onmessage = function (message) { var data = JSON.parse(message['data']); - var event = data['event']; - delete data['event']; - _innerThis.callUponClient(event, data); + if (data.stream) { + var payload = data.payload; + var event = BindingAgent.getBindingAgentKey(data.stream, payload.action); + data = payload.data; + data.model = payload.model; + data.pk = payload.pk; + _innerThis.callUponClient(event, data, data.stream); + } + else if (data.event) { + var event = data['event']; + delete data['event']; + _innerThis.callUponClient(event, data); + } + throw new ChannelError("Unknown action expected of client."); }; }; /** @@ -79,13 +84,16 @@ var Channel = (function () { * * @param event The name of the event * @param data The data to send to the consuming function + * @param eventDisplayName The name of the event to print if there is an error (used in data binding calls) */ - this.callUponClient = function (event, data) { + this.callUponClient = function (event, data, eventDisplayName) { + if (eventDisplayName === void 0) { eventDisplayName = event; } if (!(event in _this._clientConsumers)) { - throw new ChannelError("\"" + event + "\" not is a registered event." + throw new ChannelError("\"" + eventDisplayName + "\" not is a registered event." + "Registered events include: " + _this.getRegisteredEvents().toString() + ". " - + "Have you setup up socket_instance.on('event_name', consumer_function) ?"); + + "Have you setup up " + + "socket_instance.on('eventName', consumer_function) ?"); } _this._clientConsumers[event](data); }; @@ -108,6 +116,14 @@ var Channel = (function () { data['event'] = event; _this._socket.send(JSON.stringify(data)); }; + /** + * Allows users to call .create, .update, and .destroy functions for data binding + * @param streamName The name of the stream to bind to + * @returns {BindingAgent} A new BindingAgent instance that takes care of registering the three events + */ + this.bind = function (streamName) { + return new BindingAgent(_this, streamName); + }; var absolutePath; if (pathType == 'relative') { var socketScheme = window.location.protocol == "https:" ? "wss" : "ws"; @@ -127,3 +143,78 @@ var Channel = (function () { ; return Channel; }()); +/** + * Allows for client to register create, update, and destroy functions for data binding. + * Example: + * var bindingChannel = new Channel('/binding/'); + * bindingChannel.bind('room') + * .create(function(data) { ... }) + * .update(function(data) { ... }) + * .destroy(function(data) { ... }) + */ +var BindingAgent = (function () { + /** + * Constructor for the BindingAgent helper class. + * @param channel The Channel that this class is helping. + * @param streamName The name of the stream that this binding agent is supporting. + */ + function BindingAgent(channel, streamName) { + var _this = this; + this._streamName = null; + /** + * Registers a binding client consumer function + * @param bindingAction The name of the action to register + * @param clientFunction The function to register + */ + this.registerConsumer = function (bindingAction, clientFunction) { + if (BindingAgent.ACTIONS.indexOf(bindingAction) == -1) { + throw new ChannelError("You are trying to register an invalid action: " + + bindingAction + + ". Valid actions are: " + + BindingAgent.ACTIONS.toString()); + } + var bindingAgentKey = BindingAgent.getBindingAgentKey(_this._streamName, bindingAction); + _this._channel.on(bindingAgentKey, clientFunction); + }; + this.create = function (clientFunction) { + _this.registerConsumer('create', clientFunction); + return _this; + }; + this.update = function (clientFunction) { + _this.registerConsumer('update', clientFunction); + return _this; + }; + this.destroy = function (clientFunction) { + _this.registerConsumer('delete', clientFunction); + return _this; + }; + this._channel = channel; + this._streamName = streamName; + } + // The valid actions for users to call bind + BindingAgent.ACTIONS = ['create', 'update', 'delete']; + BindingAgent.GLUE = "559c09b44d6ff51559f14e87ad2b79ce"; // Hash of "http://pramodk.net" (^-^) + /** + * Gets the dictionary key used to call binding functions + * @param streamName The name of the stream + * @param action The name of the action + * @returns {string} A key that can be used to set and search keys in the Channel._clientConsumers dictionary + */ + BindingAgent.getBindingAgentKey = function (streamName, action) { + // Using the GLUE variable ensures that no regular event register with .on conflicts with the binding event + return streamName + " - " + BindingAgent.GLUE + " - " + action; + }; + return BindingAgent; +}()); +/** + * Errors from sockets. + */ +var ChannelError = (function (_super) { + __extends(ChannelError, _super); + function ChannelError(message) { + _super.call(this, message); + this.message = message; + this.name = 'ChannelError'; + } + return ChannelError; +}(Error)); diff --git a/src/ts/channel.ts b/src/ts/channel.ts index 4d333a2..397fbf3 100644 --- a/src/ts/channel.ts +++ b/src/ts/channel.ts @@ -1,52 +1,28 @@ /** - * Interface of Django Channels socket that emulates the behavior of NodeJS' socket.io - * This project has only one dependency: ReconnectingWebSocket.js - */ -interface ChannelInterface { - /** - * Setup trigger for client-side function when the Channel receives a message from the server - * @param event The 'event' received from the server - * @param clientFunction The client-side Javascript function to call when the `event` is triggered - */ - on:(event:string, clientFunction:(data:{[key:string]:string}) => void) => void; - - /** - * Sends a message to the web socket server - * @param event The string name of the task being commanded of the server - * @param data The data dictionary to send to the server - */ - emit:(event:string, data:{}) => void; -} - -/** - * Errors from sockets. + * CHANNEL.JS - a simple Javascript front-end for Django Channels websocket applications + * + * This software is provided under the MIT License. + * + * @author PRAMOD KOTIPALLI [http://pramodk.net/, http://github.com/k-pramod] + * @version 0.2.0 */ -class ChannelError extends Error { - public name = "ChannelError"; - - constructor(public message?:string) { - super(message); - } -} /** * Provides simple Javascript API for sending and receiving messages from web servers running Django Channel - * - * @author Pramod Kotipalli - * @version 0.1.1 */ class Channel implements ChannelInterface { /** The actual WebSocket connecting with the Django Channels server */ - public _socket:WebSocket; + public _socket: WebSocket; + public _webSocketPath: string; /** The client-specified functions that are called with a particular event is received */ - private _clientConsumers:{[action:string]:((data:any) => void)} = { + private _clientConsumers: {[action: string]: ((data: any) => void)} = { // By default, we must specify the 'connect' consumer - 'connect': function (socket:Channel) { - console.info('Connected to WebSocket', socket); + 'connect': function (socket: Channel) { + console.info('Connected to Channel ' + socket._webSocketPath, socket); }, // And also the 'disconnect' consumer - 'disconnect': function (socket:Channel) { + 'disconnect': function (socket: Channel) { console.info('Disconnected from WebSocket', socket); } }; @@ -63,7 +39,7 @@ class Channel implements ChannelInterface { * Tell what the type of the path is. * Set to 'absolute' if you would like to send into the entire path of the websocket */ - constructor(webSocketPath:string, pathType:string = 'relative') { + constructor(webSocketPath: string, pathType: string = 'relative') { var absolutePath; if (pathType == 'relative') { var socketScheme = window.location.protocol == "https:" ? "wss" : "ws"; @@ -83,9 +59,9 @@ class Channel implements ChannelInterface { * @param wsPath * The absolute path of the server socket */ - private connectTo = (wsPath:string) => { + private connectTo = (wsPath: string) => { this._socket = new ReconnectingWebSocket(wsPath); - + this._webSocketPath = wsPath; var _innerThis = this; // Hook up onopen event @@ -101,11 +77,21 @@ class Channel implements ChannelInterface { // Hook up onmessage to the event specified in _clientConsumers this._socket.onmessage = function (message) { var data = JSON.parse(message['data']); - var event = data['event']; - delete data['event']; + if (data.stream) { // Handle data-binding call + var payload = data.payload; + var event = BindingAgent.getBindingAgentKey(data.stream, payload.action); + data = payload.data; + data.model = payload.model; + data.pk = payload.pk; + _innerThis.callUponClient(event, data, data.stream); - _innerThis.callUponClient(event, data); + } else if (data.event) { // A websocket regular event has been triggered + var event: string = data['event']; + delete data['event']; + _innerThis.callUponClient(event, data); + } + throw new ChannelError("Unknown action expected of client."); } }; @@ -114,14 +100,16 @@ class Channel implements ChannelInterface { * * @param event The name of the event * @param data The data to send to the consuming function + * @param eventDisplayName The name of the event to print if there is an error (used in data binding calls) */ - private callUponClient = (event:string, data:any) => { + private callUponClient = (event: string, data: any, eventDisplayName:string = event) => { if (!(event in this._clientConsumers)) { throw new ChannelError( - "\"" + event + "\" not is a registered event." + "\"" + eventDisplayName + "\" not is a registered event." + "Registered events include: " + this.getRegisteredEvents().toString() + ". " - + "Have you setup up socket_instance.on('event_name', consumer_function) ?" + + "Have you setup up " + + "socket_instance.on('eventName', consumer_function) ?" ); } this._clientConsumers[event](data); @@ -137,7 +125,7 @@ class Channel implements ChannelInterface { * @param event The name of the event to listen to * @param clientFunction The client-side Javascript consumer function to call */ - on = (event:string, clientFunction:(data)=>void) => { + on = (event: string, clientFunction: (data) => void) => { this._clientConsumers[event] = clientFunction; }; @@ -147,10 +135,90 @@ class Channel implements ChannelInterface { * @param event The name of the event to send to the socket server * @param data The data to send */ - emit = (event:string, data:{}) => { + emit = (event: string, data: {}) => { data['event'] = event; this._socket.send(JSON.stringify(data)); }; + + /** + * Allows users to call .create, .update, and .destroy functions for data binding + * @param streamName The name of the stream to bind to + * @returns {BindingAgent} A new BindingAgent instance that takes care of registering the three events + */ + bind = (streamName: string) => { + return new BindingAgent(this, streamName); + } +} + +/** + * Allows for client to register create, update, and destroy functions for data binding. + * Example: + * var bindingChannel = new Channel('/binding/'); + * bindingChannel.bind('room') + * .create(function(data) { ... }) + * .update(function(data) { ... }) + * .destroy(function(data) { ... }) + */ +class BindingAgent { + private _channel: Channel; + private _streamName: string = null; + + /** + * Constructor for the BindingAgent helper class. + * @param channel The Channel that this class is helping. + * @param streamName The name of the stream that this binding agent is supporting. + */ + constructor(channel: Channel, streamName: string) { + this._channel = channel; + this._streamName = streamName; + } + + // The valid actions for users to call bind + private static ACTIONS = ['create', 'update', 'delete']; + + /** + * Registers a binding client consumer function + * @param bindingAction The name of the action to register + * @param clientFunction The function to register + */ + private registerConsumer = (bindingAction, clientFunction) => { + if (BindingAgent.ACTIONS.indexOf(bindingAction) == -1) { + throw new ChannelError( + "You are trying to register an invalid action: " + + bindingAction + + ". Valid actions are: " + + BindingAgent.ACTIONS.toString()); + } + var bindingAgentKey = BindingAgent.getBindingAgentKey(this._streamName, bindingAction); + this._channel.on(bindingAgentKey, clientFunction); + }; + + private static GLUE = "559c09b44d6ff51559f14e87ad2b79ce"; // Hash of "http://pramodk.net" (^-^) + /** + * Gets the dictionary key used to call binding functions + * @param streamName The name of the stream + * @param action The name of the action + * @returns {string} A key that can be used to set and search keys in the Channel._clientConsumers dictionary + */ + public static getBindingAgentKey = (streamName: string, action: string): string => { + // Using the GLUE variable ensures that no regular event register with .on conflicts with the binding event + return streamName + " - " + BindingAgent.GLUE + " - " + action; + }; + + public create = (clientFunction) => { + this.registerConsumer('create', clientFunction); + return this; + }; + + public update = (clientFunction) => { + this.registerConsumer('update', clientFunction); + return this; + }; + + public destroy = (clientFunction) => { + this.registerConsumer('delete', clientFunction); + return this; + } } /** @@ -161,10 +229,40 @@ interface ReconnectingSocket extends WebSocket { * Constructor for WebSocket * @param wsPath The path of the websocket on the internet */ - new(wsPath:string); + new(wsPath: string); } /** * reconnecting-websocket.js is the web socket API used behind the scenes */ -declare var ReconnectingWebSocket:ReconnectingSocket; +declare var ReconnectingWebSocket: ReconnectingSocket; + +/** + * Interface of Django Channels socket that emulates the behavior of NodeJS' socket.io + * This project has only one dependency: ReconnectingWebSocket.js + */ +interface ChannelInterface { + /** + * Setup trigger for client-side function when the Channel receives a message from the server + * @param event The 'event' received from the server + * @param clientFunction The client-side Javascript function to call when the `event` is triggered + */ + on: (event: string, clientFunction: (data: {[key: string]: string}) => void) => void; + + /** + * Sends a message to the web socket server + * @param event The string name of the task being commanded of the server + * @param data The data dictionary to send to the server + */ + emit: (event: string, data: {}) => void; +} + +/** + * Errors from sockets. + */ +class ChannelError extends Error { + public name = 'ChannelError'; + constructor(public message?: string) { + super(message); + } +} From 95f5abd9eba6018cfe5a9863962b070a46151455 Mon Sep 17 00:00:00 2001 From: Pramod Kotipalli Date: Sun, 11 Sep 2016 03:04:47 -0400 Subject: [PATCH 2/5] Update chatter example to demonstrate data binding example --- examples/chatter/chat/admin.py | 4 + examples/chatter/chat/consumers/__init__.py | 1 + examples/chatter/chat/consumers/bindings.py | 26 +++ examples/chatter/chat/routing.py | 7 +- .../chatter/chat/static/js/channel-0.1.1.js | 129 ---------- .../chatter/chat/static/js/channel-0.2.0.js | 220 ++++++++++++++++++ examples/chatter/chat/static/js/chat.js | 26 ++- examples/chatter/chat/templates/base.html | 2 +- .../chatter/chat/templates/chat/room.html | 40 ++-- examples/chatter/chat/views.py | 7 +- examples/chatter/chatter/routing.py | 1 + examples/chatter/chatter/urls.py | 4 +- 12 files changed, 314 insertions(+), 153 deletions(-) create mode 100644 examples/chatter/chat/admin.py create mode 100644 examples/chatter/chat/consumers/bindings.py delete mode 100644 examples/chatter/chat/static/js/channel-0.1.1.js create mode 100644 examples/chatter/chat/static/js/channel-0.2.0.js diff --git a/examples/chatter/chat/admin.py b/examples/chatter/chat/admin.py new file mode 100644 index 0000000..a5b452e --- /dev/null +++ b/examples/chatter/chat/admin.py @@ -0,0 +1,4 @@ +from .models import Room +from django.contrib import admin + +admin.site.register(Room) diff --git a/examples/chatter/chat/consumers/__init__.py b/examples/chatter/chat/consumers/__init__.py index 15806aa..6a93545 100644 --- a/examples/chatter/chat/consumers/__init__.py +++ b/examples/chatter/chat/consumers/__init__.py @@ -1 +1,2 @@ from .base import ChatServer +from .bindings import Demultiplexer, RoomBinding diff --git a/examples/chatter/chat/consumers/bindings.py b/examples/chatter/chat/consumers/bindings.py new file mode 100644 index 0000000..41dad40 --- /dev/null +++ b/examples/chatter/chat/consumers/bindings.py @@ -0,0 +1,26 @@ +from channels.binding.websockets import WebsocketBinding, WebsocketDemultiplexer + +from ..models import Room + + +class RoomBinding(WebsocketBinding): + model = Room + stream = 'room' + fields = ['__all__'] + + @classmethod + def group_names(cls, instance, action): + return ['room-updates'] + + def has_permission(self, user, action, pk): + # Public access + return True + + +class Demultiplexer(WebsocketDemultiplexer): + mapping = { + 'room': 'binding.room', + } + + def connection_groups(self): + return ["room-updates"] diff --git a/examples/chatter/chat/routing.py b/examples/chatter/chat/routing.py index 47c623f..bb3f22a 100644 --- a/examples/chatter/chat/routing.py +++ b/examples/chatter/chat/routing.py @@ -1,5 +1,5 @@ from channels import route, route_class -from .consumers import ChatServer, events +from .consumers import ChatServer, events, Demultiplexer, RoomBinding chat_routing = [ route_class(ChatServer, path=r'^(?P[^/]+)/stream/$'), @@ -10,3 +10,8 @@ route('chat.receive', events.user_leave, event=r'^user-leave$'), route('chat.receive', events.client_send, event=r'^message-send$'), ] + +binding_routing = [ + route_class(Demultiplexer, path=r'^/binding/'), + route('binding.room', RoomBinding.consumer), +] diff --git a/examples/chatter/chat/static/js/channel-0.1.1.js b/examples/chatter/chat/static/js/channel-0.1.1.js deleted file mode 100644 index 84d775f..0000000 --- a/examples/chatter/chat/static/js/channel-0.1.1.js +++ /dev/null @@ -1,129 +0,0 @@ -var __extends = (this && this.__extends) || function (d, b) { - for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); -}; -/** - * Errors from sockets. - */ -var ChannelError = (function (_super) { - __extends(ChannelError, _super); - function ChannelError(message) { - _super.call(this, message); - this.message = message; - this.name = "ChannelError"; - } - return ChannelError; -}(Error)); -/** - * Provides simple Javascript API for sending and receiving messages from web servers running Django Channel - * - * @author Pramod Kotipalli - * @version 0.1.1 - */ -var Channel = (function () { - /** - * Constructs a new Channel - * - * @param webSocketPath - * The path on the server. The path should be specified **relative** to the host. - * For example, if your server is listening at http://ws.pramodk.net/chat/myRoomName/, - * you must provide the websocketPath as `'/chat/myRoomName/'` - * This approach eliminates the potential of CORS-related issues. - * @param pathType - * Tell what the type of the path is. - * Set to 'absolute' if you would like to send into the entire path of the websocket - */ - function Channel(webSocketPath, pathType) { - var _this = this; - if (pathType === void 0) { pathType = 'relative'; } - /** The client-specified functions that are called with a particular event is received */ - this._clientConsumers = { - // By default, we must specify the 'connect' consumer - 'connect': function (socket) { - console.info('Connected to WebSocket', socket); - }, - // And also the 'disconnect' consumer - 'disconnect': function (socket) { - console.info('Disconnected from WebSocket', socket); - } - }; - /** - * [Private method] Connects to the specified socket - * If you would like to connect to a websocket not hosted on your server - * - * @param wsPath - * The absolute path of the server socket - */ - this.connectTo = function (wsPath) { - _this._socket = new ReconnectingWebSocket(wsPath); - var _innerThis = _this; - // Hook up onopen event - _this._socket.onopen = function () { - _innerThis.callUponClient('connect', _innerThis); - }; - // Hook up onclose event - _this._socket.onclose = function () { - _innerThis.callUponClient('disconnect', _innerThis); - }; - // Hook up onmessage to the event specified in _clientConsumers - _this._socket.onmessage = function (message) { - var data = JSON.parse(message['data']); - var event = data['event']; - delete data['event']; - _innerThis.callUponClient(event, data); - }; - }; - /** - * [Private method] Calls upon the relevant action within _clientConsumers - * - * @param event The name of the event - * @param data The data to send to the consuming function - */ - this.callUponClient = function (event, data) { - if (!(event in _this._clientConsumers)) { - throw new ChannelError("\"" + event + "\" not is a registered event." - + "Registered events include: " - + _this.getRegisteredEvents().toString() + ". " - + "Have you setup up socket_instance.on('event_name', consumer_function) ?"); - } - _this._clientConsumers[event](data); - }; - /** - * Handles messages from the server - * - * @param event The name of the event to listen to - * @param clientFunction The client-side Javascript consumer function to call - */ - this.on = function (event, clientFunction) { - _this._clientConsumers[event] = clientFunction; - }; - /** - * Sends a message to the socket server - * - * @param event The name of the event to send to the socket server - * @param data The data to send - */ - this.emit = function (event, data) { - data['event'] = event; - _this._socket.send(JSON.stringify(data)); - }; - var absolutePath; - if (pathType == 'relative') { - var socketScheme = window.location.protocol == "https:" ? "wss" : "ws"; - absolutePath = socketScheme + '://' + window.location.host + webSocketPath; - } - else if (pathType == 'absolute') { - absolutePath = webSocketPath; - } - else { - throw new ChannelError('Invalid pathType chosen'); - } - this.connectTo(absolutePath); - } - Channel.prototype.getRegisteredEvents = function () { - return Object.keys(this._clientConsumers); - }; - ; - return Channel; -}()); diff --git a/examples/chatter/chat/static/js/channel-0.2.0.js b/examples/chatter/chat/static/js/channel-0.2.0.js new file mode 100644 index 0000000..b2c68f7 --- /dev/null +++ b/examples/chatter/chat/static/js/channel-0.2.0.js @@ -0,0 +1,220 @@ +/** + * CHANNEL.JS - a simple Javascript front-end for Django Channels websocket applications + * + * This software is provided under the MIT License. + * + * @author PRAMOD KOTIPALLI [http://pramodk.net/, http://github.com/k-pramod] + * @version 0.2.0 + */ +var __extends = (this && this.__extends) || function (d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +}; +/** + * Provides simple Javascript API for sending and receiving messages from web servers running Django Channel + */ +var Channel = (function () { + /** + * Constructs a new Channel + * + * @param webSocketPath + * The path on the server. The path should be specified **relative** to the host. + * For example, if your server is listening at http://ws.pramodk.net/chat/myRoomName/, + * you must provide the websocketPath as `'/chat/myRoomName/'` + * This approach eliminates the potential of CORS-related issues. + * @param pathType + * Tell what the type of the path is. + * Set to 'absolute' if you would like to send into the entire path of the websocket + */ + function Channel(webSocketPath, pathType) { + var _this = this; + if (pathType === void 0) { pathType = 'relative'; } + /** The client-specified functions that are called with a particular event is received */ + this._clientConsumers = { + // By default, we must specify the 'connect' consumer + 'connect': function (socket) { + console.info('Connected to Channel ' + socket._webSocketPath, socket); + }, + // And also the 'disconnect' consumer + 'disconnect': function (socket) { + console.info('Disconnected from WebSocket', socket); + } + }; + /** + * [Private method] Connects to the specified socket + * If you would like to connect to a websocket not hosted on your server + * + * @param wsPath + * The absolute path of the server socket + */ + this.connectTo = function (wsPath) { + _this._socket = new ReconnectingWebSocket(wsPath); + _this._webSocketPath = wsPath; + var _innerThis = _this; + // Hook up onopen event + _this._socket.onopen = function () { + _innerThis.callUponClient('connect', _innerThis); + }; + // Hook up onclose event + _this._socket.onclose = function () { + _innerThis.callUponClient('disconnect', _innerThis); + }; + // Hook up onmessage to the event specified in _clientConsumers + _this._socket.onmessage = function (message) { + var data = JSON.parse(message['data']); + if (data.stream) { + var payload = data.payload; + var event = BindingAgent.getBindingAgentKey(data.stream, payload.action); + data = payload.data; + data.model = payload.model; + data.pk = payload.pk; + _innerThis.callUponClient(event, data, data.stream); + } + else if (data.event) { + var event = data['event']; + delete data['event']; + _innerThis.callUponClient(event, data); + } + throw new ChannelError("Unknown action expected of client."); + }; + }; + /** + * [Private method] Calls upon the relevant action within _clientConsumers + * + * @param event The name of the event + * @param data The data to send to the consuming function + * @param eventDisplayName The name of the event to print if there is an error (used in data binding calls) + */ + this.callUponClient = function (event, data, eventDisplayName) { + if (eventDisplayName === void 0) { eventDisplayName = event; } + if (!(event in _this._clientConsumers)) { + throw new ChannelError("\"" + eventDisplayName + "\" not is a registered event." + + "Registered events include: " + + _this.getRegisteredEvents().toString() + ". " + + "Have you setup up " + + "socket_instance.on('eventName', consumer_function) ?"); + } + _this._clientConsumers[event](data); + }; + /** + * Handles messages from the server + * + * @param event The name of the event to listen to + * @param clientFunction The client-side Javascript consumer function to call + */ + this.on = function (event, clientFunction) { + _this._clientConsumers[event] = clientFunction; + }; + /** + * Sends a message to the socket server + * + * @param event The name of the event to send to the socket server + * @param data The data to send + */ + this.emit = function (event, data) { + data['event'] = event; + _this._socket.send(JSON.stringify(data)); + }; + /** + * Allows users to call .create, .update, and .destroy functions for data binding + * @param streamName The name of the stream to bind to + * @returns {BindingAgent} A new BindingAgent instance that takes care of registering the three events + */ + this.bind = function (streamName) { + return new BindingAgent(_this, streamName); + }; + var absolutePath; + if (pathType == 'relative') { + var socketScheme = window.location.protocol == "https:" ? "wss" : "ws"; + absolutePath = socketScheme + '://' + window.location.host + webSocketPath; + } + else if (pathType == 'absolute') { + absolutePath = webSocketPath; + } + else { + throw new ChannelError('Invalid pathType chosen'); + } + this.connectTo(absolutePath); + } + Channel.prototype.getRegisteredEvents = function () { + return Object.keys(this._clientConsumers); + }; + ; + return Channel; +}()); +/** + * Allows for client to register create, update, and destroy functions for data binding. + * Example: + * var bindingChannel = new Channel('/binding/'); + * bindingChannel.bind('room') + * .create(function(data) { ... }) + * .update(function(data) { ... }) + * .destroy(function(data) { ... }) + */ +var BindingAgent = (function () { + /** + * Constructor for the BindingAgent helper class. + * @param channel The Channel that this class is helping. + * @param streamName The name of the stream that this binding agent is supporting. + */ + function BindingAgent(channel, streamName) { + var _this = this; + this._streamName = null; + /** + * Registers a binding client consumer function + * @param bindingAction The name of the action to register + * @param clientFunction The function to register + */ + this.registerConsumer = function (bindingAction, clientFunction) { + if (BindingAgent.ACTIONS.indexOf(bindingAction) == -1) { + throw new ChannelError("You are trying to register an invalid action: " + + bindingAction + + ". Valid actions are: " + + BindingAgent.ACTIONS.toString()); + } + var bindingAgentKey = BindingAgent.getBindingAgentKey(_this._streamName, bindingAction); + _this._channel.on(bindingAgentKey, clientFunction); + }; + this.create = function (clientFunction) { + _this.registerConsumer('create', clientFunction); + return _this; + }; + this.update = function (clientFunction) { + _this.registerConsumer('update', clientFunction); + return _this; + }; + this.destroy = function (clientFunction) { + _this.registerConsumer('delete', clientFunction); + return _this; + }; + this._channel = channel; + this._streamName = streamName; + } + // The valid actions for users to call bind + BindingAgent.ACTIONS = ['create', 'update', 'delete']; + BindingAgent.GLUE = "559c09b44d6ff51559f14e87ad2b79ce"; // Hash of "http://pramodk.net" (^-^) + /** + * Gets the dictionary key used to call binding functions + * @param streamName The name of the stream + * @param action The name of the action + * @returns {string} A key that can be used to set and search keys in the Channel._clientConsumers dictionary + */ + BindingAgent.getBindingAgentKey = function (streamName, action) { + // Using the GLUE variable ensures that no regular event register with .on conflicts with the binding event + return streamName + " - " + BindingAgent.GLUE + " - " + action; + }; + return BindingAgent; +}()); +/** + * Errors from sockets. + */ +var ChannelError = (function (_super) { + __extends(ChannelError, _super); + function ChannelError(message) { + _super.call(this, message); + this.message = message; + this.name = 'ChannelError'; + } + return ChannelError; +}(Error)); diff --git a/examples/chatter/chat/static/js/chat.js b/examples/chatter/chat/static/js/chat.js index 6920de9..79bce04 100644 --- a/examples/chatter/chat/static/js/chat.js +++ b/examples/chatter/chat/static/js/chat.js @@ -7,11 +7,11 @@ $(document).ready(function () { channel.on('connect', function (channel) { var username = prompt('What is your username?'); - + var username_element = $('#chat-username'); username_element.val(username); username_element.attr('disabled', true); - + channel.emit('user-join', { 'username': username }) @@ -25,7 +25,6 @@ $(document).ready(function () { var members = data['members']; var html = ''; $.each(members, function (idx, member) { - console.log(member); html += '
  • '; html += member['username']; html += '
  • '; @@ -69,5 +68,26 @@ $(document).ready(function () { // Send the message across the channel channel.emit('message-send', data); + + // Prevent page reloading + return false; + }); + + var binder = new Channel('/binding/'); + var bindingAgent = binder.bind('room'); + bindingAgent.create(function (data) { + var roomItem = + '
  • ' + + data.slug + + '
  • '; + $("#chat-rooms").append(roomItem); + }); + bindingAgent.update(function (data) { + $("[data-room_id=" + data.pk + "]").html(data.slug); + }); + bindingAgent.destroy(function (data) { + $("[data-room_id=" + data.pk + "]").remove(); }); }); diff --git a/examples/chatter/chat/templates/base.html b/examples/chatter/chat/templates/base.html index 105a2f5..c53c649 100644 --- a/examples/chatter/chat/templates/base.html +++ b/examples/chatter/chat/templates/base.html @@ -15,7 +15,7 @@ - + diff --git a/examples/chatter/chat/templates/chat/room.html b/examples/chatter/chat/templates/chat/room.html index 22f3c4f..4f1d617 100644 --- a/examples/chatter/chat/templates/chat/room.html +++ b/examples/chatter/chat/templates/chat/room.html @@ -8,33 +8,41 @@

    Welcome to {{ room.slug }}

    -
    + +
    +
      + {% for rm in rooms %} +
    • {{ rm.slug }}
    • + {% endfor %} +
    +
    + +

    Messages

    -
    +

    Send message

    - -
    - - -
    -
    - - -
    - -
    - -
    +
    + +
    + + +
    +
    + + +
    + +

    Members ({{ room.member_count }})

    -
      +
    diff --git a/examples/chatter/chat/views.py b/examples/chatter/chat/views.py index eb6cbbd..145f92e 100644 --- a/examples/chatter/chat/views.py +++ b/examples/chatter/chat/views.py @@ -11,7 +11,10 @@ def chatroom(request, slug): # type: (HttpRequest, str) -> HttpResponse :return: The metronome room with the given name """ room, created = Room.objects.get_or_create(slug=slug) - + rooms = Room.objects.all() return render(request=request, template_name='chat/room.html', - context={'room': room}) + context={ + 'room': room, + 'rooms': rooms, + }) diff --git a/examples/chatter/chatter/routing.py b/examples/chatter/chatter/routing.py index 2ad6d52..b2a8360 100644 --- a/examples/chatter/chatter/routing.py +++ b/examples/chatter/chatter/routing.py @@ -3,4 +3,5 @@ channel_routing = [ include('chat.routing.chat_routing', path=r'^/chat/'), include('chat.routing.event_routing'), + include('chat.routing.binding_routing'), ] diff --git a/examples/chatter/chatter/urls.py b/examples/chatter/chatter/urls.py index 7ef5075..014e2b9 100644 --- a/examples/chatter/chatter/urls.py +++ b/examples/chatter/chatter/urls.py @@ -14,7 +14,9 @@ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url, include +from django.contrib import admin urlpatterns = [ - url(r'^chat/', include('chat.urls')) + url(r'^chat/', include('chat.urls')), + url(r'^admin/', admin.site.urls) ] From 84f4240cf0fbae4bc625b6824b56ad76f74391e0 Mon Sep 17 00:00:00 2001 From: Pramod Kotipalli Date: Sun, 11 Sep 2016 04:06:21 -0400 Subject: [PATCH 3/5] Updates --- examples/chatter/chat/models.py | 7 +++++-- examples/chatter/chat/static/js/chat.js | 5 ++++- examples/chatter/chat/templates/chat/room.html | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/examples/chatter/chat/models.py b/examples/chatter/chat/models.py index 47bd96b..4338727 100644 --- a/examples/chatter/chat/models.py +++ b/examples/chatter/chat/models.py @@ -42,7 +42,7 @@ def members(self): # type: () -> [dict] """ Returns an array of member information """ - return [member.serialized for member in self.member_set.all()] + return [member.as_dict for member in self.member_set.all()] @property def member_count(self): # type: () -> int @@ -52,6 +52,9 @@ def member_count(self): # type: () -> int def group(self): # type: () -> Group return Group(self.slug) + def __str__(self): + return "'{}' room ({} members)".format(self.slug, self.member_count) + class Member(models.Model): """ @@ -63,7 +66,7 @@ class Member(models.Model): reply_channel_name = models.CharField(max_length=128, null=False) @property - def serialized(self): # type: () -> dict + def as_dict(self): # type: () -> dict """ Provides a serialized version of this member :return: diff --git a/examples/chatter/chat/static/js/chat.js b/examples/chatter/chat/static/js/chat.js index 79bce04..589801d 100644 --- a/examples/chatter/chat/static/js/chat.js +++ b/examples/chatter/chat/static/js/chat.js @@ -6,7 +6,10 @@ $(document).ready(function () { var channel = new Channel(ws_path); channel.on('connect', function (channel) { - var username = prompt('What is your username?'); + var username = null; + while (!username) { + username = prompt('What is your username? (Required)'); + } var username_element = $('#chat-username'); username_element.val(username); diff --git a/examples/chatter/chat/templates/chat/room.html b/examples/chatter/chat/templates/chat/room.html index 4f1d617..9e478e6 100644 --- a/examples/chatter/chat/templates/chat/room.html +++ b/examples/chatter/chat/templates/chat/room.html @@ -9,7 +9,7 @@

    -
    +
      {% for rm in rooms %}
    • {{ rm.slug }}
    • @@ -17,13 +17,13 @@

    -
    +

    Messages

    -
    +

    Send message

    From 00eeb3c4578c17ad0306be59f17369afead0bff0 Mon Sep 17 00:00:00 2001 From: Pramod Kotipalli Date: Sun, 11 Sep 2016 04:30:33 -0400 Subject: [PATCH 4/5] Update channel.md API docs --- docs/channel.md | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/channel.md b/docs/channel.md index 8cac3a9..a5aa7c9 100644 --- a/docs/channel.md +++ b/docs/channel.md @@ -6,34 +6,34 @@ The aim of this project is provide a front-end Javascript API and set of backend With `channel.js`, clients receive `event`s from the server and send `event`s to the server. When an event is received from the server, `channel.js` calls upon a registered client-side function which performs the needed actions. To send a message, the client will `emit` an event and data to the server through a `event` string. **This project is under active development so this API may change over time.** -### API +### API (v0.2.0) * `Channel` - the Javascript 'class' wrapping a web socket connection - * **constructor**: `new Channel(ws_path, path_type)`: + * **constructor**: `new Channel(webSocketPath, pathType)`: - * `ws_path` (type: `string`): The path of the websocket + * `webSocketPath` (type: `string`): The path of the websocket - * `path_type` (type: `string`) (options: `relative`, `absolute`): The type of URI passed as `ws_path`. `relative` indicates that the `ws_path` provided is found on this host (i.e. `window.location.host`). `absolute` indicates that the `ws_path` is an absolute websocket path + * `pathType` (type: `string`) (options: `relative`, `absolute`): The type of URI passed as `webSocketPath`. `relative` indicates that the `ws_path` provided is found on this host (i.e. `window.location.host`). `absolute` indicates that the `webSocketPath` is an absolute websocket path _Example_: ```javascript // Connect to a websocket at `ws://your-host/chat/room-name/stream/` var relative_path = '/chat/room-name/stream/'; var channel = new Channel(relative_path, 'relative'); - // In this case, the `path_type` is optional. So the following is equivalent: + // In this case, the `pathType` is optional. So the following is equivalent: var channel = new Channel(relative_path); // Connect to a websocket on another server at `ws://other-host/chat/room-name/stream/` - var absolute_path = 'ws://other-host/chat/room-name/stream/'; + var absolute_path = 'ws://example.com/chat/room-name/stream/'; var someones_channel = new Channel(absolute_path, 'absolute'); ``` - * `.on(event_name, func)`: + * `.on(eventName, clientFunction)`: - * `event_name` (type: `string`): the event received from a server that should trigger a client-side event + * `eventName` (type: `string`): the event received from a server that should trigger a client-side event - * `func` (type: `function`): a function that takes in a `data` dictionary parameter + * `clientFunction` (type: `function`): a function that takes in a `data` dictionary parameter _Example_: ```javascript @@ -45,9 +45,9 @@ With `channel.js`, clients receive `event`s from the server and send `event`s to }); ``` - * `.emit(event_name, data)`: + * `.emit(eventName, data)`: - * `event_name` (type: `string`): The task to notify the server of (e.g. 'user-join' or 'message-send') + * `eventName` (type: `string`): The task to notify the server of (e.g. 'user-join' or 'message-send') * `data` (type: dictionary): The data to be sent to the websocket server @@ -69,5 +69,28 @@ With `channel.js`, clients receive `event`s from the server and send `event`s to channel.emit('message-send', data); }); ``` + + * `.bind('streamName')`: + + * `streamName` (type: `string`): The name of the data-binding stream to listen to + + * **returns** `BindingAgent` object that allows you to easily register `create`, `update`, and `destroy` (delete) functions + + _Example_: You can create a new channel just for handling data bindings + ```javascript + var channel = new Channel('/binding/'); + channel.bind('room') + .create(function(data) { ... }) + .update(function(data) { ... }) + .destroy(function(data) { ... }); + + channel.bind('message') + .create(function(data) { ... }); + ``` + In each of these consumer functions, the `data` parameter contains all the fields of the Django database model. The `...` portion of the functions can take care of updating the HTML with the new or updated data. Just like with socket.io, `.on` is used to take client-side actions and `.emit` is used to send messages to the server. + +### Change log + +See the GitHub repo's [Releases page](https://github.com/k-pramod/channel.js/releases) for a list of changes with each release. From a76e5569c4fc5df5c9655db7254c1cb8161ae4c3 Mon Sep 17 00:00:00 2001 From: Pramod Kotipalli Date: Sun, 11 Sep 2016 04:33:34 -0400 Subject: [PATCH 5/5] Update public README with new version --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7c2ff92..99920c1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Simply add the following references to your client-side HTML markup: +https://raw.githubusercontent.com/k-pramod/channel.js/master/dist/channel-0.2.0.js"> ``` Or clone this repo and use the latest files from the `dist` directory. @@ -30,7 +30,6 @@ This project features a fully-worked, front-to-back example that illustrates how Features to be implemented in the near future include: -* Support for Django Channel's data binding * More diverse examples with ['Deploy to Heroku'](https://devcenter.heroku.com/articles/heroku-button) buttons ### Contributing @@ -39,4 +38,3 @@ If you would like to propose new features, please use this repo's [GitHub Issue --- [Pramod Kotipalli](http://pramodk.net/) -