Merge pull request #1262 from rmehta/realtime
[realtime] cleanup & auto-refresh doc
This commit is contained in:
commit
86322e7ce2
21 changed files with 1353 additions and 523 deletions
|
|
@ -994,3 +994,16 @@ def get_logger(module=None):
|
|||
logging_setup_complete = True
|
||||
|
||||
return logging.getLogger(module or "frappe")
|
||||
|
||||
def publish_realtime(*args, **kwargs):
|
||||
"""Publish real-time updates
|
||||
|
||||
:param event: Event name, like `task_progress` etc.
|
||||
:param message: JSON message object. For async must contain `task_id`
|
||||
:param room: Room in which to publish update (default entire site)
|
||||
:param user: Transmit to user
|
||||
:param doctype: Transmit to doctype, docname
|
||||
:param docname: Transmit to doctype, docname"""
|
||||
import frappe.async
|
||||
|
||||
return frappe.async.publish_realtime(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ def set_task_status(task_id, status, response=None):
|
|||
"status": status,
|
||||
"task_id": task_id
|
||||
})
|
||||
emit_via_redis("task_status_change", response, room="task:" + task_id)
|
||||
publish_realtime("task_status_change", response, room="task:" + task_id)
|
||||
|
||||
|
||||
def remove_old_task_logs():
|
||||
|
|
@ -120,21 +120,50 @@ def is_file_old(file_path):
|
|||
return ((time.time() - os.stat(file_path).st_mtime) > TASK_LOG_MAX_AGE)
|
||||
|
||||
|
||||
def emit_via_redis(event, message, room=None):
|
||||
def publish_realtime(event, message=None, room=None, user=None, doctype=None, docname=None):
|
||||
"""Publish real-time updates
|
||||
|
||||
:param event: Event name, like `task_progress` etc.
|
||||
:param message: JSON message object. For async must contain `task_id`
|
||||
:param room: Room in which to publish update (default entire site)
|
||||
:param user: Transmit to user
|
||||
:param doctype: Transmit to doctype, docname
|
||||
:param docname: Transmit to doctype, docname"""
|
||||
if message is None:
|
||||
message = {}
|
||||
|
||||
if not room:
|
||||
if user:
|
||||
room = get_user_room(user)
|
||||
elif doctype and docname:
|
||||
room = get_doc_room(doctype, docname)
|
||||
message["doctype"] = doctype
|
||||
message["name"] = docname
|
||||
else:
|
||||
room = get_site_room()
|
||||
|
||||
emit_via_redis(event, message, room)
|
||||
|
||||
def emit_via_redis(event, message, room):
|
||||
"""Publish real-time updates via redis
|
||||
|
||||
:param event: Event name, like `task_progress` etc.
|
||||
:param message: JSON message object. For async must contain `task_id`
|
||||
:param room: name of the room"""
|
||||
r = get_redis_server()
|
||||
|
||||
try:
|
||||
r.publish('events', json.dumps({'event': event, 'message': message, 'room': room}))
|
||||
except redis.exceptions.ConnectionError:
|
||||
pass
|
||||
|
||||
|
||||
def put_log(line_no, line, task_id=None):
|
||||
r = get_redis_server()
|
||||
if not task_id:
|
||||
task_id = frappe.local.task_id
|
||||
task_progress_room = "task_progress:" + frappe.local.task_id
|
||||
task_log_key = "task_log:" + task_id
|
||||
emit_via_redis('task_progress', {
|
||||
publish_realtime('task_progress', {
|
||||
"message": {
|
||||
"lines": {line_no: line}
|
||||
},
|
||||
|
|
@ -195,19 +224,6 @@ def get_user_info(sid):
|
|||
'user': session.user,
|
||||
}
|
||||
|
||||
def new_comment(doc, event):
|
||||
if not doc.comment_doctype:
|
||||
return
|
||||
if doc.comment_doctype == 'Message':
|
||||
if doc.comment_docname == frappe.session.user:
|
||||
message = doc.as_dict()
|
||||
message['broadcast'] = True
|
||||
emit_via_redis('new_message', message, room=get_site_room())
|
||||
else:
|
||||
emit_via_redis('new_message', doc.as_dict(), room=get_user_room(doc.comment_docname))
|
||||
else:
|
||||
emit_via_redis('new_comment', doc.as_dict(), room=get_doc_room(doc.comment_doctype, doc.comment_docname))
|
||||
|
||||
def get_doc_room(doctype, docname):
|
||||
return ''.join([frappe.local.site, ':doc:', doctype, '/', docname])
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,21 @@ class Comment(Document):
|
|||
"feed_type": comment_type
|
||||
}
|
||||
|
||||
def after_insert(self):
|
||||
"""Send realtime updates"""
|
||||
if not self.comment_doctype:
|
||||
return
|
||||
if self.comment_doctype == 'Message':
|
||||
if self.comment_docname == frappe.session.user:
|
||||
message = self.as_dict()
|
||||
message['broadcast'] = True
|
||||
frappe.publish_realtime('new_message', message)
|
||||
else:
|
||||
frappe.publish_realtime('new_message', self.as_dict(), user=frappe.session.user)
|
||||
else:
|
||||
frappe.publish_realtime('new_comment', self.as_dict(), doctype= self.comment_doctype,
|
||||
docname = self.comment_docname)
|
||||
|
||||
def validate(self):
|
||||
"""Raise exception for more than 50 comments."""
|
||||
if frappe.db.sql("""select count(*) from tabComment where comment_doctype=%s
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -33,12 +33,31 @@ frappe.desk.pages.Messages = Class.extend({
|
|||
this.page.wrapper.find(".layout-main-section-wrapper").addClass("col-sm-9");
|
||||
this.page.wrapper.find(".page-title").removeClass("col-xs-6").addClass("col-xs-12");
|
||||
this.page.wrapper.find(".page-actions").removeClass("col-xs-6").addClass("hidden-xs");
|
||||
this.setup_realtime();
|
||||
},
|
||||
|
||||
make: function() {
|
||||
this.make_sidebar();
|
||||
},
|
||||
|
||||
setup_realtime: function() {
|
||||
frappe.realtime.on('new_message', function(comment) {
|
||||
if(comment.modified_by !== user) {
|
||||
frappe.utils.notify(__("Message from {0}", [comment.comment_by_fullname]), comment.comment);
|
||||
}
|
||||
if (frappe.get_route()[0] === 'messages') {
|
||||
var current_contact = $(cur_page.page).find('[data-contact]').data('contact');
|
||||
var on_broadcast_page = current_contact === user;
|
||||
if (current_contact == comment.owner || (on_broadcast_page && comment.broadcast)) {
|
||||
var $row = $('<div class="list-row"/>');
|
||||
frappe.desk.pages.messages.list.data.unshift(comment);
|
||||
frappe.desk.pages.messages.list.render_row($row, comment);
|
||||
frappe.desk.pages.messages.list.parent.prepend($row);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
make_sidebar: function() {
|
||||
var me = this;
|
||||
return frappe.call({
|
||||
|
|
|
|||
|
|
@ -8,11 +8,15 @@
|
|||
class="form-control messages-textarea"></textarea>
|
||||
</div>
|
||||
<div style="padding-top: 15px;">
|
||||
<button class="pull-right btn btn-default btn-sm btn-post" data-contact="{%= contact %}">
|
||||
<button class="pull-right btn btn-primary btn-sm btn-post" data-contact="{%= contact %}">
|
||||
{%= __("Post") %}
|
||||
</button>
|
||||
{% if (contact === user) { %}
|
||||
<span class="pull-right indicator orange">{%= __("Public") %}</span>
|
||||
<span class="pull-right"
|
||||
style="margin-top: 4px; margin-right: 10px;">
|
||||
<i class="octicon octicon-rss"></i>
|
||||
<span class="text-muted small">{%= __("Public") %}</span>
|
||||
</span>
|
||||
{% } %}
|
||||
<div class="pull-right checkbox text-muted small"
|
||||
style="margin-right: 15px; margin-top: 7px;">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<div class="row message-row small">
|
||||
{% if (data.owner==data.comment_docname && data.parenttype!="Assignment") { %}
|
||||
<span class="pull-left indicator orange" title="{%= __("Public") %}"></span>
|
||||
{% } %}
|
||||
<div class="col-sm-9">
|
||||
<div class="media">
|
||||
{% if (data.owner==data.comment_docname
|
||||
&& data.parenttype!="Assignment") { %}
|
||||
<span class="pull-left" title="{{ __("Public") }}"><i class="octicon octicon-rss text-muted" style="margin-top: 3px;"></i></span>
|
||||
{% } else { %}
|
||||
<span class="pull-left" title="{{ __("Public") }}" style="width: 20px; height: 16px; display: inline-block;"></span>
|
||||
{% } %}
|
||||
<div class="pull-left hidden-xs">
|
||||
<span class="avatar avatar-small" title="{%= frappe.user.full_name(data.owner) %} ">
|
||||
<img class="media-object {{ data.is_system_message ? "grayscale" : "" }}"
|
||||
|
|
|
|||
|
|
@ -131,9 +131,6 @@ doc_events = {
|
|||
"frappe.email.doctype.email_alert.email_alert.trigger_email_alerts"
|
||||
],
|
||||
"on_trash": "frappe.desk.notifications.clear_doctype_notifications"
|
||||
},
|
||||
"Comment": {
|
||||
"after_insert": "frappe.async.new_comment"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -573,9 +573,19 @@ class Document(BaseDocument):
|
|||
self.run_method("on_update_after_submit")
|
||||
|
||||
frappe.cache().hdel("last_modified", self.doctype)
|
||||
self.notify_modified()
|
||||
|
||||
self.latest = None
|
||||
|
||||
def notify_modified(self):
|
||||
"""Publish realtime that the current document is modified"""
|
||||
frappe.publish_realtime("doc_update", {"modified_by": frappe.session.user},
|
||||
doctype=self.doctype, docname=self.name)
|
||||
|
||||
if not self.meta.get("read_only") and not self.meta.get("issingle"):
|
||||
frappe.publish_realtime("list_update", {"doctype": self.doctype})
|
||||
|
||||
|
||||
def check_no_back_links_exist(self):
|
||||
"""Check if document links to any active document before Cancel."""
|
||||
from frappe.model.delete_doc import check_if_doc_is_linked, check_if_doc_is_dynamically_linked
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ select.form-control {
|
|||
}
|
||||
.form-headline .alert {
|
||||
font-size: 12px;
|
||||
border-color: #d1d8dd;
|
||||
background-color: #fffce7;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.delivery-status-indicator {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ frappe.Application = Class.extend({
|
|||
this.startup();
|
||||
},
|
||||
startup: function() {
|
||||
frappe.model.init();
|
||||
this.load_bootinfo();
|
||||
this.make_nav_bar();
|
||||
this.set_favicon();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ frappe.ui.form.Dashboard = Class.extend({
|
|||
this.wrapper.toggle(true);
|
||||
},
|
||||
set_headline_alert: function(text, alert_class, icon) {
|
||||
if(!alert_class) alert_class = "alert-warning";
|
||||
this.set_headline(repl('<div class="alert %(alert_class)s">%(icon)s%(text)s</div>', {
|
||||
"alert_class": alert_class || "",
|
||||
"icon": icon ? '<i class="'+icon+'" /> ' : "",
|
||||
|
|
|
|||
|
|
@ -294,4 +294,4 @@ frappe.ui.form.Comments = Class.extend({
|
|||
|
||||
return last_email;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,12 +38,22 @@ frappe.views.ListFactory = frappe.views.Factory.extend({
|
|||
});
|
||||
|
||||
$(document).on("save", function(event, doc) {
|
||||
var list_page = "List/" + doc.doctype;
|
||||
frappe.views.set_list_as_dirty(doc.doctype);
|
||||
});
|
||||
|
||||
frappe.views.set_list_as_dirty = function(doctype) {
|
||||
var list_page = "List/" + doctype;
|
||||
if(frappe.pages[list_page]) {
|
||||
if(frappe.pages[list_page].doclistview)
|
||||
frappe.pages[list_page].doclistview.dirty = true;
|
||||
}
|
||||
})
|
||||
var route = frappe.get_route();
|
||||
if(route[0]==="List" && route[1]===doctype) {
|
||||
setTimeout(function() {
|
||||
frappe.pages[list_page].doclistview.run();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
frappe.views.DocListView = frappe.ui.Listing.extend({
|
||||
init: function(opts) {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,37 @@ $.extend(frappe.model, {
|
|||
new_names: {},
|
||||
events: {},
|
||||
|
||||
init: function() {
|
||||
// setup refresh if the document is updated somewhere else
|
||||
frappe.realtime.on("doc_update", function(data) {
|
||||
// set list dirty
|
||||
frappe.views.set_list_as_dirty(data.doctype);
|
||||
var doc = locals[data.doctype] && locals[data.doctype][data.name];
|
||||
if(doc) {
|
||||
// current document is dirty, show message if its not me
|
||||
if(cur_frm.doc.doctype===doc.doctype && cur_frm.doc.name===doc.name) {
|
||||
if(data.modified_by!==user) {
|
||||
doc.__needs_refresh = true;
|
||||
cur_frm.show_if_needs_refresh();
|
||||
}
|
||||
} else {
|
||||
if(!doc.__unsaved) {
|
||||
// no local changes, remove from locals
|
||||
frappe.model.remove_from_locals(doc.doctype, doc.name);
|
||||
} else {
|
||||
// show message when user navigates back
|
||||
doc.__needs_refresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.realtime.on("list_update", function(data) {
|
||||
frappe.views.set_list_as_dirty(data.doctype);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
is_value_type: function(fieldtype) {
|
||||
// not in no-value type
|
||||
return frappe.model.no_value_type.indexOf(fieldtype)===-1;
|
||||
|
|
@ -122,6 +153,21 @@ $.extend(frappe.model, {
|
|||
return frappe.model.docinfo[doctype] && frappe.model.docinfo[doctype][name] || null;
|
||||
},
|
||||
|
||||
new_comment: function(comment) {
|
||||
if (frappe.model.docinfo[comment.comment_doctype]
|
||||
&& frappe.model.docinfo[comment.comment_doctype][comment.comment_docname]) {
|
||||
var comments = frappe.model.docinfo[comment.comment_doctype][comment.comment_docname].comments;
|
||||
var comment_exists = !!$.map(comments,
|
||||
function(x) { return x.name == comment.name? true : undefined}).length
|
||||
if (!comment_exists) {
|
||||
frappe.model.docinfo[comment.comment_doctype][comment.comment_docname].comments = comments.concat([comment]);
|
||||
}
|
||||
}
|
||||
if (cur_frm.doctype === comment.comment_doctype && cur_frm.docname === comment.comment_docname) {
|
||||
cur_frm.comments.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
get_shared: function(doctype, name) {
|
||||
return frappe.model.get_docinfo(doctype, name).shared;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
frappe.socket = {
|
||||
open_tasks: {},
|
||||
open_docs: [],
|
||||
init: function() {
|
||||
if (frappe.boot.disable_async) {
|
||||
return;
|
||||
|
|
@ -16,9 +17,9 @@ frappe.socket = {
|
|||
frappe.socket.doc_subscribe(frm.doctype, frm.docname);
|
||||
});
|
||||
|
||||
$(document).on('form-unload', function(e, frm) {
|
||||
frappe.socket.doc_unsubscribe(frm.doctype, frm.docname);
|
||||
});
|
||||
// $(document).on('form-unload', function(e, frm) {
|
||||
// frappe.socket.doc_unsubscribe(frm.doctype, frm.docname);
|
||||
// });
|
||||
},
|
||||
subscribe: function(task_id, opts) {
|
||||
frappe.socket.socket.emit('task_subscribe', task_id);
|
||||
|
|
@ -28,11 +29,17 @@ frappe.socket = {
|
|||
},
|
||||
doc_subscribe: function(doctype, docname) {
|
||||
frappe.socket.socket.emit('doc_subscribe', doctype, docname);
|
||||
frappe.socket.open_doc = {doctype: doctype, docname: docname};
|
||||
frappe.socket.open_docs.push({doctype: doctype, docname: docname});
|
||||
},
|
||||
doc_unsubscribe: function(doctype, docname) {
|
||||
frappe.socket.socket.emit('doc_unsubscribe', doctype, docname);
|
||||
frappe.socket.open_doc = null;
|
||||
frappe.socket.open_docs = $.filter(frappe.socket.open_docs, function(d) {
|
||||
if(d.doctype===doctype && d.name===docname) {
|
||||
return null;
|
||||
} else {
|
||||
return d;
|
||||
}
|
||||
})
|
||||
},
|
||||
setup_listeners: function() {
|
||||
frappe.socket.socket.on('task_status_change', function(data) {
|
||||
|
|
@ -47,33 +54,6 @@ frappe.socket = {
|
|||
frappe.socket.socket.on('task_progress', function(data) {
|
||||
frappe.socket.process_response(data, "progress");
|
||||
});
|
||||
frappe.socket.socket.on('new_comment', function(comment) {
|
||||
if (frappe.model.docinfo[comment.comment_doctype] && frappe.model.docinfo[comment.comment_doctype][comment.comment_docname]) {
|
||||
var comments = frappe.model.docinfo[comment.comment_doctype][comment.comment_docname].comments
|
||||
var comment_exists = !!$.map(comments, function(x) { return x.name == comment.name? true : undefined}).length
|
||||
if (!comment_exists) {
|
||||
frappe.model.docinfo[comment.comment_doctype][comment.comment_docname].comments = comments.concat([comment]);
|
||||
}
|
||||
}
|
||||
if (cur_frm.doctype === comment.comment_doctype && cur_frm.docname === comment.comment_docname) {
|
||||
cur_frm.comments.refresh();
|
||||
}
|
||||
});
|
||||
frappe.socket.socket.on('new_message', function(comment) {
|
||||
frappe.utils.notify(__("Message from {0}", [comment.comment_by_fullname]), comment.comment);
|
||||
if ($(cur_page.page).data('page-route') === 'messages') {
|
||||
var current_contact = $(cur_page.page).find('[data-contact]').data('contact');
|
||||
var on_broadcast_page = current_contact === user;
|
||||
if (current_contact == comment.owner || (on_broadcast_page && comment.broadcast)) {
|
||||
var $row = $('<div class="list-row"/>');
|
||||
frappe.desk.pages.messages.list.data.unshift(comment);
|
||||
frappe.desk.pages.messages.list.render_row($row, comment);
|
||||
frappe.desk.pages.messages.list.parent.prepend($row);
|
||||
}
|
||||
}
|
||||
else {
|
||||
}
|
||||
});
|
||||
},
|
||||
setup_reconnect: function() {
|
||||
// subscribe again to open_tasks
|
||||
|
|
@ -81,11 +61,15 @@ frappe.socket = {
|
|||
$.each(frappe.socket.open_tasks, function(task_id, opts) {
|
||||
frappe.socket.subscribe(task_id, opts);
|
||||
});
|
||||
|
||||
// re-connect open docs
|
||||
$.each(frappe.socket.open_docs, function(d) {
|
||||
if(locals[d.doctype] && locals[d.doctype][d.name]) {
|
||||
frappe.socket.doc_subscribe(d.doctype, d.name);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if(frappe.socket.open_doc) {
|
||||
frappe.socket.doc_subscribe(frappe.socket.open_doc.doctype, frappe.socket.open_doc.docname);
|
||||
}
|
||||
},
|
||||
process_response: function(data, method) {
|
||||
if(!data) {
|
||||
|
|
@ -112,3 +96,8 @@ frappe.socket = {
|
|||
}
|
||||
|
||||
$(frappe.socket.init);
|
||||
|
||||
frappe.provide("frappe.realtime");
|
||||
frappe.realtime.on = function(event, callback) {
|
||||
frappe.socket.socket.on(event, callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({
|
|||
if(f.get_parsed_value) {
|
||||
var v = f.get_parsed_value();
|
||||
|
||||
if(f.df.reqd && !v)
|
||||
if(f.df.reqd && is_null(v))
|
||||
errors.push(__(f.df.label));
|
||||
|
||||
if(v) ret[f.df.fieldname] = v;
|
||||
|
|
|
|||
|
|
@ -19,9 +19,20 @@ frappe.views.FormFactory = frappe.views.Factory.extend({
|
|||
me.show_doc(route);
|
||||
}
|
||||
|
||||
$(document).on("page-change", function() {
|
||||
frappe.ui.form.close_grid_form();
|
||||
});
|
||||
|
||||
if(!this.initialized) {
|
||||
$(document).on("page-change", function() {
|
||||
frappe.ui.form.close_grid_form();
|
||||
});
|
||||
|
||||
frappe.realtime.on("new_comment", function(data) {
|
||||
frappe.model.new_comment(data);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
this.initialized = true;
|
||||
},
|
||||
show_doc: function(route) {
|
||||
var dt = route[1],
|
||||
|
|
|
|||
|
|
@ -406,6 +406,16 @@ _f.Frm.prototype.refresh = function(docname) {
|
|||
if(this.print_preview.wrapper.is(":visible")) {
|
||||
this.print_preview.preview();
|
||||
}
|
||||
|
||||
this.show_if_needs_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
_f.Frm.prototype.show_if_needs_refresh = function() {
|
||||
if(this.doc.__needs_refresh) {
|
||||
this.dashboard.set_headline_alert(__("This form has been modified after you have loaded it")
|
||||
+ '<a class="btn btn-xs btn-primary pull-right" onclick="cur_frm.reload_doc()">'
|
||||
+ __("Refresh") + '</a>', "alert-warning");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -212,8 +212,7 @@ select.form-control {
|
|||
|
||||
.form-headline .alert {
|
||||
font-size: @text-medium;
|
||||
border-color: @border-color;
|
||||
// background-color: @light-bg;
|
||||
background-color: @light-yellow;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ io.on('connection', function(socket){
|
|||
var room = get_user_room(socket, res.body.message.user);
|
||||
// console.log('joining', room);
|
||||
socket.join(room);
|
||||
socket.join(get_site_room(socket));
|
||||
}
|
||||
})
|
||||
socket.on('task_subscribe', function(task_id) {
|
||||
|
|
@ -64,7 +65,7 @@ io.on('connection', function(socket){
|
|||
docname: docname
|
||||
})
|
||||
.end(function(err, res) {
|
||||
console.log(err)
|
||||
if(err) console.log(err);
|
||||
if(res.status == 200) {
|
||||
var room = get_doc_room(socket, doctype, docname);
|
||||
// console.log('joining', room)
|
||||
|
|
@ -89,7 +90,7 @@ function send_existing_lines(task_id, socket) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
subscriber.on("message", function(channel, message) {
|
||||
message = JSON.parse(message);
|
||||
io.to(message.room).emit(message.event, message.message);
|
||||
|
|
@ -97,7 +98,7 @@ subscriber.on("message", function(channel, message) {
|
|||
});
|
||||
|
||||
subscriber.subscribe("events");
|
||||
|
||||
|
||||
http.listen(3000, function(){
|
||||
console.log('listening on *:3000');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue