diff --git a/docs/examples/feed.md b/docs/examples/feed.md index 9c5a6bc..e2a238a 100644 --- a/docs/examples/feed.md +++ b/docs/examples/feed.md @@ -29,3 +29,23 @@ Subscribe } }) ``` + +Reload +------ +``` +curl -u john:hello 127.0.0.1:10100/v1/application/30/feed/89/reload --digest +{ + "result": "Feed reloaded", + "status": "ok" +} +``` + +Empty +----- +``` +curl -u john:hello 127.0.0.1:10100/v1/application/30/feed/86/empty --digest +{ + "result": "Feed empty done.", + "status": "ok" +} +``` diff --git a/docs/examples/javascript.md b/docs/examples/javascript.md new file mode 100644 index 0000000..9055496 --- /dev/null +++ b/docs/examples/javascript.md @@ -0,0 +1,39 @@ +### JS client + +#### Usage +``` + window.onload = function() { + elasticfeed.init({ + channel: { + url: 'ws://localhost:80', + transport: 'ws' + } + }); + + feed = elasticfeed.initFeed('000001', { + outputContainerId: 'my-elastic-feed-1', + stylerFunction: function(data) { + return '
' + data + '
'; + } + }); + + feed.channel.on('join', function(chid, ts) { + feed1.addEntry(chid + " joined the chat room"); + }); + + feed.channel.on('leave', function(chid, ts) { + feed1.addEntry(chid + " left the chat room"); + }); + + window['socket'] = feed1.socket; + } +``` + +#### Test data broadcast +``` + // single feed + socket.send({Type:1, Timestamp:1111111, Content: {Type:3, Timestamp:22222, Id: "000001", Content: "data-examples"}}) + + // all feeds in the view on the chanel + socket.send({Type:1, Timestamp:1111111, Content: {Type:3, Timestamp:22222, Id: "*", Content: "data-examples"}}) +``` diff --git a/resource/entry.go b/resource/entry.go index 9a77700..fdc2f48 100644 --- a/resource/entry.go +++ b/resource/entry.go @@ -8,60 +8,8 @@ import ( "github.com/feedlabs/feedify/graph" "github.com/feedlabs/elasticfeed/service/stream/controller/room" - "github.com/feedlabs/elasticfeed/service/stream/model" ) -// user_feed_token = channel_id + feed_id => e.g aabbccddee + aabbcc -// for private feeds there will be 1 websocket connection -// for public company feeds will be 1 websocket connection -// basically for each channel is 1 websocket connection -// private and public channel will stream through multiple feed-pages events -// -// channel => channel_id -// event => 'feed:' + feed_id -// data => [{ // action object -// id => string // entryId -// tags => strings... // array of strings -// action => string // add/delete/update -// data => string // entry data as content; string e.g. json.stringify -// }, {}, {}] - -const BODY_HEADER = `{ - "channel": "iO5wshd5fFE5YXxJ/hfyKQ==:17", - "event": "CM_Action_Abstract:SEND:31", - "data": { - "action": { - "actor": { - "_type": 33, - "_id": { - "id": "1" - }, - "id": 1, - "displayName": "user1", - "visible": true, - "_class": "Feed_Model_User" - }, - "verb": 13, - "type": 31, - "_class": "Feed_Action_Feed" - }, - "model": { - "_type": 33, - "_id": { - "id": "1" - }, - "id": 1, - "displayName": "user1", - "visible": true, - "_class": "Feed_Model_User" - }, - "data": {` - -const BODY_BOTTOM = ` - } - } -}` - func GetEntryList(FeedId string, ApplicationId string, OrgId string) (feedEntries []*Entry, err error) { feed, err := GetFeed(FeedId, ApplicationId, OrgId) @@ -136,14 +84,11 @@ func AddEntry(feedEntry Entry, FeedId string, ApplicationId string, OrgId string return "0", err } -// _data := BODY_HEADER + `"Id": "` + feedEntry.Id + `", "Action": "add", "Tag": {}, "Data": ` + strconv.Quote(feedEntry.Data) + BODY_BOTTOM -// message.Publish(_data) - feedEntry.Id = strconv.Itoa(entry.Id) // notify - data, _ := json.Marshal(entry) - room.Publish <- room.NewEntryEvent(model.EVENT_MESSAGE, "system", string(data)) + d, _ := json.Marshal(feedEntry) + room.Publish <- room.NewFeedEvent(room.FEED_ENTRY_NEW, feed.Id, string(d)) return feedEntry.Id, nil } @@ -155,8 +100,12 @@ func UpdateEntry(id string, FeedId string, ApplicationId string, OrgId string, d return err } - _data := BODY_HEADER + `"Id": "` + entry.Id + `", "Action": "update", "Tag": {}, "Data": ` + strconv.Quote(data) + BODY_BOTTOM - message.Publish(_data) + // update entry + entry.Data = data + + // notify + d, _ := json.Marshal(entry) + room.Publish <- room.NewEntryEvent(room.ENTRY_UPDATE, entry.Id, string(d)) _id, _ := strconv.Atoi(entry.Id) return storage.SetPropertyNode(_id, "data", data) @@ -176,8 +125,9 @@ func DeleteEntry(id string, FeedId string, ApplicationId string, OrgId string) ( storage.DeleteRelation(rel.Id) } - _data := BODY_HEADER + `"Id": "` + entry.Id + `", "Action": "remove"` + BODY_BOTTOM - message.Publish(_data) + // notify + d, _ := json.Marshal(entry) + room.Publish <- room.NewEntryEvent(room.ENTRY_DELETE, entry.Id, string(d)) return storage.DeleteNode(_id) } diff --git a/resource/feed.go b/resource/feed.go index c8a735a..632b00d 100644 --- a/resource/feed.go +++ b/resource/feed.go @@ -3,12 +3,10 @@ package resource import ( "errors" "strconv" - "encoding/json" "github.com/feedlabs/feedify/graph" "github.com/feedlabs/elasticfeed/service/stream/controller/room" - "github.com/feedlabs/elasticfeed/service/stream/model" ) func (this *Feed) AddEntry(entry Entry) (EntryId string, err error) { @@ -93,10 +91,6 @@ func AddFeed(feed Feed, applicationId string, orgId string) (id string, err erro feed.Id = strconv.Itoa(_feed.Id) - // notify - data, _ := json.Marshal(feed) - room.Publish <- room.NewFeedEvent(model.EVENT_MESSAGE, "system", string(data)) - return feed.Id, nil } @@ -110,6 +104,14 @@ func DeleteFeed(id string) (error) { return storage.DeleteNode(_id) } +func ActionReloadFeed(id string) { + room.Publish <- room.NewFeedEvent(room.FEED_RELOAD, id, "reload") +} + +func ActionEmptyFeed(id string) { + room.Publish <- room.NewFeedEvent(room.FEED_EMPTY, id, "empty") +} + func init() { Feeds = make(map[string]*Feed) } diff --git a/service/db/v1/controller/feed.go b/service/db/v1/controller/feed.go index e9a4b29..e421435 100644 --- a/service/db/v1/controller/feed.go +++ b/service/db/v1/controller/feed.go @@ -5,6 +5,8 @@ import ( "github.com/feedlabs/elasticfeed/resource" "github.com/feedlabs/elasticfeed/service/db/v1/template/feed" + + "github.com/feedlabs/elasticfeed/service/db/v1/template" ) type FeedController struct { @@ -152,3 +154,19 @@ func (this *FeedController) Delete() { feed.ResponseDelete() this.Controller.ServeJson() } + +func (this *FeedController) ActionReload() { + feedId := this.Ctx.Input.Params[":feedId"] + + resource.ActionReloadFeed(feedId) + + this.ServeJson(template.Success("Feed reloaded.")) +} + +func (this *FeedController) ActionEmpty() { + feedId := this.Ctx.Input.Params[":feedId"] + + resource.ActionEmptyFeed(feedId) + + this.ServeJson(template.Success("Feed empty done.")) +} diff --git a/service/db/v1/router/entry.go b/service/db/v1/router/entry.go index 4adad9f..dfa9104 100644 --- a/service/db/v1/router/entry.go +++ b/service/db/v1/router/entry.go @@ -10,5 +10,5 @@ func init() { feedify.Router("/v1/application/:applicationId:string/entry/:feedEntryId:int", &controller.EntryController{}, "get:Get;delete:Delete;put:Put") feedify.Router("/v1/application/:applicationId:string/feed/:feedId:int/entry", &controller.EntryController{}, "get:GetListByFeed;post:PostToFeed") - feedify.Router("/v1/application/:applicationId:string/feed/:feedId:int/entry/:feedEntryId:int", &controller.EntryController{}, "get:Get;delete:Remove") + feedify.Router("/v1/application/:applicationId:string/feed/:feedId:int/entry/:feedEntryId:int", &controller.EntryController{}, "get:Get;delete:Remove;put:Put") } diff --git a/service/db/v1/router/feed.go b/service/db/v1/router/feed.go index bf70232..bde8170 100644 --- a/service/db/v1/router/feed.go +++ b/service/db/v1/router/feed.go @@ -8,4 +8,7 @@ import ( func init() { feedify.Router("/v1/application/:applicationId:string/feed", &controller.FeedController{}, "get:GetList;post:Post") feedify.Router("/v1/application/:applicationId:string/feed/:feedId:int", &controller.FeedController{}, "get:Get;delete:Delete;put:Put") + + feedify.Router("/v1/application/:applicationId:string/feed/:feedId:int/reload", &controller.FeedController{}, "get:ActionReload") + feedify.Router("/v1/application/:applicationId:string/feed/:feedId:int/empty", &controller.FeedController{}, "get:ActionEmpty") } diff --git a/service/stream/controller/channel/long_pooling.go b/service/stream/controller/channel/long_pooling.go index 67e5380..8e2d838 100644 --- a/service/stream/controller/channel/long_pooling.go +++ b/service/stream/controller/channel/long_pooling.go @@ -37,7 +37,7 @@ func (this *LongPollingController) Post() { // should be executed and returned directly to user // lastReceived time should not be changed in that case - room.Publish <- room.NewSystemEvent(model.EVENT_MESSAGE, chid, data) + room.Publish <- room.NewSystemEvent(room.CHANNEL_MESSAGE, chid, data) } func (this *LongPollingController) Fetch() { diff --git a/service/stream/controller/channel/websocket.go b/service/stream/controller/channel/websocket.go index ea8619d..568cc00 100644 --- a/service/stream/controller/channel/websocket.go +++ b/service/stream/controller/channel/websocket.go @@ -6,7 +6,6 @@ import ( "github.com/feedlabs/feedify" "github.com/gorilla/websocket" - "github.com/feedlabs/elasticfeed/service/stream/model" "github.com/feedlabs/elasticfeed/service/stream/controller/room" ) @@ -43,7 +42,7 @@ func (this *WebSocketController) Join() { return } - room.Publish <- room.NewSystemEvent(model.EVENT_MESSAGE, chid, string(p)) + room.Publish <- room.NewSystemEvent(room.CHANNEL_MESSAGE, chid, string(p)) room.P2P <- ws } } diff --git a/service/stream/controller/room/feed.go b/service/stream/controller/room/feed.go index dc1fbdd..2ef6d2e 100644 --- a/service/stream/controller/room/feed.go +++ b/service/stream/controller/room/feed.go @@ -1,29 +1,41 @@ package room import ( + "strconv" "time" "container/list" "encoding/json" "github.com/feedlabs/feedify" "github.com/gorilla/websocket" - "github.com/astaxie/beego/session" - "github.com/feedlabs/elasticfeed/service/stream/model" ) const ( - FEED_ADD = iota - FEED_DELETE - FEED_UPDATE - FEED_RESET - FEED_RELOAD - - ENTRY_ADD - ENTRY_DELETE - ENTRY_UPDATE - ENTRY_RELOAD + CHANNEL_JOIN = 0 + CHANNEL_LEAVE = 1 + CHANNEL_MESSAGE = 2 + + SYSTEM_FEED_MESSAGE = 1 + + FEED_RELOAD = 1 + FEED_EMPTY = 2 + FEED_ENTRY_NEW = 3 + FEED_ENTRY_INIT = 4 + FEED_ENTRY_MORE = 5 + FEED_HIDE = 6 + FEED_SHOW = 7 + FEED_ENTRY_MESSAGE = 8 + FEED_AUTHENTICATED = 100 + FEED_AUTHENTICATION_REQUIRED = 101 + FEED_AUTHENTICATION_FAILED = 102 + FEED_LOGGED_OUT = 103 + + ENTRY_UPDATE = 1 + ENTRY_DELETE = 2 + ENTRY_SHOW = 3 + ENTRY_HIDE = 4 ) var ( @@ -49,7 +61,8 @@ type Subscriber struct { } func NewEvent(ep model.EventType, user, msg string) model.Event { - return model.Event{ep, user, time.Now().UnixNano(), msg} + ts := time.Now().UnixNano() + return model.Event{ep, user, ts, strconv.Itoa(int(ts)), msg} } func NewChannelEvent(ep model.EventType, user, msg string) model.Event { @@ -57,15 +70,28 @@ func NewChannelEvent(ep model.EventType, user, msg string) model.Event { } func NewSystemEvent(ep model.EventType, user, msg string) model.Event { - return NewChannelEvent(ep, user, msg) + event := NewEvent(ep, user, msg) + data, _ := json.Marshal(event) + + return NewChannelEvent(CHANNEL_MESSAGE, user, string(data)) } func NewFeedEvent(ep model.EventType, user, msg string) model.Event { - return NewSystemEvent(ep, user, msg) + // "msg" is a feed action; can contain entry specific event + event := NewEvent(ep, user, msg) + data, _ := json.Marshal(event) + + // "user" is and feed-id; "*" means all feeds on the client site + return NewSystemEvent(SYSTEM_FEED_MESSAGE, user, string(data)) } func NewEntryEvent(ep model.EventType, user, msg string) model.Event { - return NewSystemEvent(ep, user, msg) + // "msg" is a feed entry data as a string + event := NewEvent(ep, user, msg) + data, _ := json.Marshal(event) + + // "*" all feeds on client site will receive this message + return NewFeedEvent(FEED_ENTRY_MESSAGE, "*", string(data)) } func Join(user string, ws *websocket.Conn) { @@ -82,10 +108,10 @@ func FeedManager() { case sub := <-Subscribe: Subscribers.PushBack(sub) - Publish <- NewChannelEvent(model.EVENT_JOIN, sub.Name, "") + Publish <- NewChannelEvent(CHANNEL_JOIN, sub.Name, "") case client := <-P2P: - data, _ := json.Marshal(NewSystemEvent(model.EVENT_MESSAGE, "system", "ok")) + data, _ := json.Marshal(NewSystemEvent(CHANNEL_MESSAGE, "system", "ok")) client.WriteMessage(websocket.TextMessage, data) case event := <-Publish: @@ -108,7 +134,7 @@ func FeedManager() { ws.Close() feedify.Error("WebSocket closed:", unsub) } - Publish <- NewChannelEvent(model.EVENT_LEAVE, unsub, "") + Publish <- NewChannelEvent(CHANNEL_LEAVE, unsub, "") break } } diff --git a/service/stream/model/event.go b/service/stream/model/event.go index eb8049b..eb8b776 100644 --- a/service/stream/model/event.go +++ b/service/stream/model/event.go @@ -6,20 +6,15 @@ import ( type EventType int -const ( - EVENT_JOIN = iota - EVENT_LEAVE - EVENT_MESSAGE -) - type Event struct { - Type EventType - User string - Timestamp int64 - Content string + Type EventType + User string + Ts int64 + Timestamp string + Content string } -const archiveSize = 100 +const archiveSize = 1 var Archive = list.New() @@ -34,7 +29,8 @@ func GetEvents(lastReceived int) []Event { events := make([]Event, 0, Archive.Len()) for event := Archive.Front(); event != nil; event = event.Next() { e := event.Value.(Event) - if e.Timestamp > int64(lastReceived) { + if e.Ts > int64(lastReceived) { + events = append(events, e) } } diff --git a/service/stream/static/elasticfeed.js b/service/stream/static/elasticfeed.js index d592b96..4f155a8 100644 --- a/service/stream/static/elasticfeed.js +++ b/service/stream/static/elasticfeed.js @@ -10,49 +10,48 @@ function includeJs(jsFilePath) { includeJs('lib/feed.js'); includeJs('lib/entry.js'); includeJs('lib/channel.js'); -includeJs('lib/event/channel.js'); -includeJs('lib/event/system.js'); +includeJs('lib/event.js'); (function(window) { + /** @type {Object} */ + var defaultOptions = { + channel: { + url: 'localhost', + transport: 'ws' + }, + stylerFunction: function(data) { + } + } + var elasticfeed = { + /** @type {Object} */ + options: {}, + /** @type {Object} */ channelList: {}, /** @type {Object} */ feedList: {}, - initFeed: function(options) { - options = _extend({ - channel: { - url: 'ws://localhost:80/ws', - transport: 'ws' - }, - styler: function(data) { - } - }, options); - - channel = this.getChannel(options.channel); - - return new Feed(options, channel); + init: function(options) { + this.options = _extend(defaultOptions, options); }, - /** - * Returns Feed object - * @param options - * @returns {*} - */ - getFeed: function(options) { - if (options.id == undefined) { - return null; + initFeed: function(id, options) { + if (id == undefined) { + return false; } if (this.feedList[id] == undefined) { - this.feedList[id] = new Feed(options) + opts = _extend(this.options, options || {}); + channel = this.getChannel(opts.channel); + + this.feedList[id] = new Feed(id, opts, channel); } - return this.feedList[options.id]; + return this.feedList[id]; }, /** @@ -63,7 +62,7 @@ includeJs('lib/event/system.js'); */ getChannel: function(options, credential) { if (options.url == undefined) { - return null; + return false; } if (this.channelList[options.url] == undefined) { @@ -74,11 +73,17 @@ includeJs('lib/event/system.js'); }, findFeed: function(id) { - return this.getFeed({id: id}); + if (this.feedList[id] == undefined) { + return false; + } + return this.feedList[id]; }, findChannel: function(url) { - return this.getChannel({url: url}); + if (this.channelList[url] == undefined) { + return false; + } + return this.channelList[url]; } }; diff --git a/service/stream/static/lib/channel.js b/service/stream/static/lib/channel.js index 8a6e711..24d1ba6 100644 --- a/service/stream/static/lib/channel.js +++ b/service/stream/static/lib/channel.js @@ -31,12 +31,15 @@ var Channel = (function() { /** @type {Object} */ this._handlers = {}; + + /** @type {WebSocket} */ + this._socket = null; } // Handlers /** - * @param {ChannelEvent} event + * @param {Event} event * @param {Function} callback */ Channel.prototype.on = function(name, callback) { @@ -64,18 +67,18 @@ var Channel = (function() { // Events /** - * @param {ChannelEvent} event + * @param {Event} event */ Channel.prototype.onData = function(event) { - switch (event.Type) { + switch (event.type) { case JOIN: - this.onJoin(event.User, event.ts) + this.onJoin(event.user, event.ts) break; case LEAVE: - this.onLeave(event.User, event.ts) + this.onLeave(event.user, event.ts) break; case MESSAGE: - this.onMessage(event.User, event.ts, event.Content) + this.onMessage(event.user, event.ts, event.content) break; } } @@ -93,13 +96,19 @@ var Channel = (function() { } Channel.prototype.onMessage = function(chid, timestamp, data) { + systemEvent = new Event(data); + for (var i in this._handlers[MESSAGE]) { - this._handlers[MESSAGE][i].call(this, chid, timestamp, data); + this._handlers[MESSAGE][i].call(this, chid, timestamp, systemEvent); } } // Connection + Channel.prototype.isWebSocket = function() { + return this._socket != undefined; + } + Channel.prototype.getConnection = function() { } @@ -108,7 +117,7 @@ var Channel = (function() { self = this this._socket.onmessage = function(event) { - event = new ChannelEvent(JSON.parse(event.data)) + event = new Event(JSON.parse(event.data)) self.onData(event) }; @@ -124,7 +133,8 @@ var Channel = (function() { var lastReceived = 0; var isWait = false; - this.getJSON('http://localhost:10100/stream/lp/join?chid=' + this.id, function() { + this.getJSON('http://localhost:10100/stream/lp/join?chid=' + this.id, function(data) { + // should set timestamp to proper one! }) self = this; @@ -144,7 +154,7 @@ var Channel = (function() { } self.each(data, function(i, event) { - event = new ChannelEvent(event) + event = new Event(event) self.onData(event) lastReceived = event.GetTimestamp(); @@ -244,20 +254,6 @@ var Channel = (function() { xhr1.send(dataString); } - Channel.prototype.EventToString = function(event) { - switch (event.Type) { - case JOIN: - console.log(event.User + " joined the chat room"); - break; - case LEAVE: - console.log(event.User + " left the chat room"); - break; - case MESSAGE: - console.log(event.User + ", " + event.PrintContent()); - break; - } - } - // Helpers var _extend = function(a, b) { diff --git a/service/stream/static/lib/entry.js b/service/stream/static/lib/entry.js index 149fefe..ced554a 100644 --- a/service/stream/static/lib/entry.js +++ b/service/stream/static/lib/entry.js @@ -1,80 +1,182 @@ var Entry = (function() { - const ACTION_GROUP_TYPE = 2 - - const ACTION_ADD = 1 - const ACTION_DELETE = 2 - const ACTION_UPDATE = 3 - const ACTION_HIDE = 4 - const ACTION_SHOW = 5 - - const PUBLISH_METRIC = 6 + const UPDATE = 1 + const DELETE = 2 + const HIDE = 3 + const SHOW = 4 /** @type {Entry} */ var localCache = {} - function Entry(data) { + function Entry(data, options) { /** @type {String} */ this.id = null; - /** @type {Integer} */ - this.ts = null; + /** @type {String} */ + this.viewId = _uniqueId(); + + /** @type {String} */ + this.data = data; + + /** @type {Object} */ + this._feed = null; - /** @type {Feed} */ - this.parent = null; + /** @type {Function} */ + this._styler = (options ? options.styler : undefined) || function() { + return data; + }; + + /** @type {Object} */ + this._handlers = {}; } - // Management + Entry.prototype.setParent = function(feed) { + this._feed = feed; + this.bindMessages(); + } - Entry.prototype.update = function() { + Entry.prototype.getViewId = function() { + return this.viewId; } - Entry.prototype.remove = function() { + // UI + + // TODO: + // should make animations, should be configurable by developer + // first level is style function; second level is render function + Entry.prototype.render = function() { + try { + document.getElementById(this.viewId).innerHTML = this._styler.call(this, JSON.stringify(this.data)); + } catch (e) { + // serious error + } } - Entry.prototype.hide = function() { + // Events + + Entry.prototype.on = function(type, callback) { + switch (name) { + case 'delete': + type = DELETE + break; + case 'update': + type = UPDATE + break; + case 'hide': + type = HIDE + break; + case 'show': + type = SHOW + break; + default: + return false; + break; + } + if (this._handlers[type] == undefined) { + this._handlers[type] = [] + } + this._handlers[type].push(callback); + return true; } - Entry.prototype.show = function() { + Entry.prototype.onData = function(entryEvent) { + switch (entryEvent.type) { + case DELETE: + this.onDelete(entryEvent.ts) + break; + case UPDATE: + this.onUpdate(entryEvent.ts, entryEvent.content) + break; + case HIDE: + this.onHide(entryEvent.ts) + break; + case SHOW: + this.onShow(entryEvent.ts) + break; + } } - // Events callbacks + // Management - Entry.prototype.onBeforeUpdate = function(callback) { + Entry.prototype.update = function(timestamp, data) { + this.data = data; + this.render(); + } + Entry.prototype.delete = function() { + this.unbindMessages(); + document.getElementById(this.viewId).remove(); } - Entry.prototype.onAfterUpdate = function(callback) { + Entry.prototype.hide = function() { + } + Entry.prototype.show = function() { } - Entry.prototype.onBeforeRemove = function(callback) { + // API + + Entry.prototype.apiEntryUpdate = function(data) { + } + Entry.prototype.apiMetricSave = function(data) { } - Entry.prototype.onAfterRemove = function(callback) { + // Events callbacks + + Entry.prototype.onUpdate = function(timestamp, data) { + this.update(timestamp, data); + + for (var i in this._handlers[UPDATE]) { + this._handlers[UPDATE][i].call(this, timestamp, data); + } + } + Entry.prototype.onDelete = function(timestamp) { + for (var i in this._handlers[DELETE]) { + this._handlers[DELETE][i].call(this, timestamp); + } } - Entry.prototype.onData = function(callback) { + Entry.prototype.onHide = function(timestamp) { + for (var i in this._handlers[HIDE]) { + this._handlers[HIDE][i].call(this, timestamp); + } + } + Entry.prototype.onShow = function(timestamp) { + for (var i in this._handlers[SHOW]) { + this._handlers[SHOW][i].call(this, timestamp); + } } // Handlers - Entry.prototype.registerHandlers = function() { - // bind to feed events + Entry.prototype.bindMessages = function() { + var self = this; + this.__bindFeed = this._feed.on('entry-message', function(ts, entryEvent) { + if (entryEvent.id == self.id || entryEvent.id == '*') { + self.onData(entryEvent); + } + }); } - // Helpers - - Entry.prototype.getParent = function() { + Entry.prototype.unbindMessages = function() { + this._feed.off(this.__bindFeed); } + // Getters + Entry.prototype.getTimestamp = function() { return this.ts; } + // Helpers + + var _uniqueId = function() { + return '_' + Math.random().toString(36).substr(2, 36); + } + return Entry; })(); diff --git a/service/stream/static/lib/event.js b/service/stream/static/lib/event.js new file mode 100644 index 0000000..5706608 --- /dev/null +++ b/service/stream/static/lib/event.js @@ -0,0 +1,48 @@ +var Event = (function() { + + function Event(event) { + + /** @type {String} */ + this.id = event.Id || null; + + /** @type {Integer} */ + this.ts = event.Timestamp; + + /** @type {Integer} */ + this.actionGroup = null + + /** @type {Integer} */ + this.actionType = null + + /** @type {String} */ + this.user = event.User + + /** @type {String} */ + this.type = event.Type + + /** @type {String} */ + this.contentType = 'string' + + /** @type {String} */ + try { + this.content = JSON.parse(event.Content) + this.contentType = 'json' + } catch (e) { + this.content = event.Content + } + } + + Event.prototype.GetTimestamp = function() { + return this.ts; + } + + Event.prototype.PrintContent = function() { + if (this.contentType == 'string') { + return this.content + } + return JSON.stringify(this.content) + } + + return Event; + +})(); diff --git a/service/stream/static/lib/event/channel.js b/service/stream/static/lib/event/channel.js deleted file mode 100644 index dfbf253..0000000 --- a/service/stream/static/lib/event/channel.js +++ /dev/null @@ -1,48 +0,0 @@ -var ChannelEvent = (function() { - - function ChannelEvent(event) { - - /** @type {String} */ - this.id = null; - - /** @type {Integer} */ - this.ts = event.Timestamp; - - /** @type {Integer} */ - this.actionGroup = null - - /** @type {Integer} */ - this.actionType = null - - /** @type {String} */ - this.User = event.User - - /** @type {String} */ - this.Type = event.Type - - /** @type {String} */ - this.ContentType = 'string' - - /** @type {String} */ - try { - this.Content = JSON.parse(event.Content) - this.ContentType = 'json' - } catch (e) { - this.Content = event.Content - } - } - - ChannelEvent.prototype.GetTimestamp = function() { - return this.ts; - } - - ChannelEvent.prototype.PrintContent = function() { - if (this.ContentType == 'string') { - return this.Content - } - return JSON.stringify(this.Content) - } - - return ChannelEvent; - -})(); diff --git a/service/stream/static/lib/event/entry.js b/service/stream/static/lib/event/entry.js deleted file mode 100644 index fef5b97..0000000 --- a/service/stream/static/lib/event/entry.js +++ /dev/null @@ -1,48 +0,0 @@ -var EntryEvent = (function() { - - function EntryEvent(event) { - - /** @type {String} */ - this.id = null; - - /** @type {Integer} */ - this.ts = event.Timestamp; - - /** @type {Integer} */ - this.actionGroup = null - - /** @type {Integer} */ - this.actionType = null - - /** @type {String} */ - this.User = event.User - - /** @type {String} */ - this.Type = event.Type - - /** @type {String} */ - this.ContentType = 'string' - - /** @type {String} */ - try { - this.Content = JSON.parse(event.Content) - this.ContentType = 'json' - } catch (e) { - this.Content = event.Content - } - } - - EntryEvent.prototype.GetTimestamp = function() { - return this.ts; - } - - EntryEvent.prototype.PrintContent = function() { - if (this.ContentType == 'string') { - return this.Content - } - return JSON.stringify(this.Content) - } - - return EntryEvent; - -})(); diff --git a/service/stream/static/lib/event/feed.js b/service/stream/static/lib/event/feed.js deleted file mode 100644 index 3b0432e..0000000 --- a/service/stream/static/lib/event/feed.js +++ /dev/null @@ -1,48 +0,0 @@ -var FeedEvent = (function() { - - function FeedEvent(event) { - - /** @type {String} */ - this.id = null; - - /** @type {Integer} */ - this.ts = event.Timestamp; - - /** @type {Integer} */ - this.actionGroup = null - - /** @type {Integer} */ - this.actionType = null - - /** @type {String} */ - this.User = event.User - - /** @type {String} */ - this.Type = event.Type - - /** @type {String} */ - this.ContentType = 'string' - - /** @type {String} */ - try { - this.Content = JSON.parse(event.Content) - this.ContentType = 'json' - } catch (e) { - this.Content = event.Content - } - } - - FeedEvent.prototype.GetTimestamp = function() { - return this.ts; - } - - FeedEvent.prototype.PrintContent = function() { - if (this.ContentType == 'string') { - return this.Content - } - return JSON.stringify(this.Content) - } - - return FeedEvent; - -})(); diff --git a/service/stream/static/lib/event/system.js b/service/stream/static/lib/event/system.js deleted file mode 100644 index 2afedb6..0000000 --- a/service/stream/static/lib/event/system.js +++ /dev/null @@ -1,48 +0,0 @@ -var SystemEvent = (function() { - - function SystemEvent(event) { - - /** @type {String} */ - this.id = null; - - /** @type {Integer} */ - this.ts = event.Timestamp; - - /** @type {Integer} */ - this.actionGroup = null - - /** @type {Integer} */ - this.actionType = null - - /** @type {String} */ - this.User = event.User - - /** @type {String} */ - this.Type = event.Type - - /** @type {String} */ - this.ContentType = 'string' - - /** @type {String} */ - try { - this.Content = JSON.parse(event.Content) - this.ContentType = 'json' - } catch (e) { - this.Content = event.Content - } - } - - SystemEvent.prototype.GetTimestamp = function() { - return this.ts; - } - - SystemEvent.prototype.PrintContent = function() { - if (this.ContentType == 'string') { - return this.Content - } - return JSON.stringify(this.Content) - } - - return SystemEvent; - -})(); diff --git a/service/stream/static/lib/feed.js b/service/stream/static/lib/feed.js index 945dd1e..626fb6f 100644 --- a/service/stream/static/lib/feed.js +++ b/service/stream/static/lib/feed.js @@ -1,13 +1,15 @@ var Feed = (function() { - const ACTION_GROUP_TYPE = 1 + const SYSTEM_FEED_MESSAGE = 1 - const ACTION_RELOAD = 1 - const ACTION_RESET = 2 - const ACTION_DATA_INIT = 3 - const ACTION_DATA_MORE = 4 - const ACTION_HIDE = 5 - const ACTION_SHOW = 6 + const RELOAD = 1 + const EMPTY = 2 + const ENTRY_NEW = 3 + const ENTRY_INIT = 4 + const ENTRY_MORE = 5 + const HIDE = 6 + const SHOW = 7 + const ENTRY_MESSAGE = 8 const AUTHENTICATED = 100 const AUTHENTICATION_REQUIRED = 101 @@ -32,10 +34,10 @@ var Feed = (function() { method: 'basic' }; - function Feed(options, channel) { + function Feed(id, options, channel) { /** @type {String} */ - this.id = null; + this.id = id; /** @type {Channel} */ this.channel = channel; @@ -45,74 +47,281 @@ var Feed = (function() { /** @type {Object} */ if (this.channel.options.transport == 'ws') { - this.socket = this.channel.getWebSocketConnection(); + if (this.channel._socket == undefined) { + this.socket = this.channel.getWebSocketConnection(); + } else { + this.socket = this.channel._socket; + } } else if (this.channel.options.transport == 'lp') { this.socket = this.channel.getLongPoolingConnection(); } + /** @type {Object} */ this.options = _extend(globalOptions, options); - this._stylerFunction = options.styler || this._stylerFunction; + + /** @type {Function} */ + this.stylerFunction = options.stylerFunction || this._stylerFunction; + + /** @type {Function} */ + this.renderFunction = options.renderFunction || this._renderFunction; + + /** @type {DOM} */ this.outputContainer = document.getElementById(this.options.outputContainerId); this.bindChannel(this.channel); + + /** @type {Object} */ + this._handlers = {}; } - Feed.prototype.on = function(type, callback) { + Feed.prototype.on = function(name, callback) { + switch (name) { + case 'reload': + type = RELOAD + break; + case 'empty': + type = EMPTY + break; + case 'entry': + type = ENTRY_NEW + break; + case 'entry-init': + type = ENTRY_INIT + break; + case 'entry-more': + type = ENTRY_MORE + break; + case 'hide': + type = HIDE + break; + case 'show': + type = SHOW + break; + case 'entry-message': + type = ENTRY_MESSAGE + break; + case 'authenticated': + type = AUTHENTICATED + break; + case 'authentication-required': + type = AUTHENTICATION_REQUIRED + break; + case 'authentication-failed': + type = AUTHENTICATION_FAILED + break; + case 'logout': + type = LOGGED_OUT + break; + default: + break; + } + if (this._handlers[type] == undefined) { + this._handlers[type] = [] + } + this._handlers[type].push(callback); + return callback; + } + + Feed.prototype.off = function(callback) { + for (var i in this._handlers) { + for (var x in this._handlers[i]) { + if (this._handlers[i][x] == callback) { + delete this._handlers[i][x]; + return; + } + } + } + } + + Feed.prototype.onData = function(feedEvent) { + switch (feedEvent.type) { + case RELOAD: + this.onReload(feedEvent.ts) + break; + case EMPTY: + this.onEmpty(feedEvent.ts) + break; + case ENTRY_NEW: + this.onEntryNew(feedEvent.ts, feedEvent.content) + break; + case ENTRY_INIT: + this.onEntryInit(feedEvent.ts, feedEvent.content) + break; + case ENTRY_MORE: + this.onEntryMore(feedEvent.ts, feedEvent.content) + break; + case HIDE: + this.onHide(feedEvent.ts) + break; + case SHOW: + this.onShow(feedEvent.ts) + break; + case ENTRY_MESSAGE: + this.onEntryMessage(feedEvent.ts, feedEvent.content) + break; + case AUTHENTICATED: + this.onAuthenticated(feedEvent.ts, feedEvent.content) + break; + case AUTHENTICATION_REQUIRED: + this.onAuthenticationRequired(feedEvent.ts, feedEvent.content) + break; + case AUTHENTICATION_FAILED: + this.onAuthenticationFailed(feedEvent.ts, feedEvent.content) + break; + case LOGGED_OUT: + this.onLogout(feedEvent.ts, feedEvent.content) + break; + } } // Events callbacks - Feed.prototype.onReload = function(callback) { + Feed.prototype.onReload = function(timestamp) { + for (var i in this._handlers[RELOAD]) { + this._handlers[RELOAD][i].call(this, timestamp); + } } - Feed.prototype.onReset = function(callback) { + Feed.prototype.onEmpty = function(timestamp) { + for (var i in this._handlers[EMPTY]) { + this._handlers[EMPTY][i].call(this, timestamp); + } } - Feed.prototype.onEntryAdd = function(callback) { + Feed.prototype.onEntryNew = function(timestamp, data) { + entry = new Entry(data, {styler: this.stylerFunction}); + + this.addEntry(entry) + + for (var i in this._handlers[ENTRY_NEW]) { + this._handlers[ENTRY_NEW][i].call(this, timestamp, entry); + } } - Feed.prototype.onEntryDelete = function(callback) { + Feed.prototype.onEntryInit = function(timestamp, data) { + entries = JSON.parse(data); + + for (var i in this._handlers[ENTRY_INIT]) { + this._handlers[ENTRY_INIT][i].call(this, timestamp, entries); + } } - Feed.prototype.onEntryUpdate = function(callback) { + Feed.prototype.onEntryMore = function(timestamp, data) { + entries = JSON.parse(data); + + for (var i in this._handlers[ENTRY_MORE]) { + this._handlers[ENTRY_MORE][i].call(this, timestamp, entries); + } } - Feed.prototype.onEvent = function(eventName, callback) { + Feed.prototype.onHide = function(timestamp) { + for (var i in this._handlers[HIDE]) { + this._handlers[HIDE][i].call(this, timestamp); + } } - Feed.prototype.onData = function(callback) { + Feed.prototype.onShow = function(timestamp) { + for (var i in this._handlers[SHOW]) { + this._handlers[SHOW][i].call(this, timestamp); + } + } + + Feed.prototype.onEntryMessage = function(timestamp, content) { + entryEvent = new Event(content); + + for (var i in this._handlers[ENTRY_MESSAGE]) { + this._handlers[ENTRY_MESSAGE][i].call(this, timestamp, entryEvent); + } + } + + Feed.prototype.onAuthenticated = function(timestamp, content) { + for (var i in this._handlers[AUTHENTICATED]) { + this._handlers[AUTHENTICATED][i].call(this, timestamp); + } + } + + Feed.prototype.onAuthenticationRequired = function(timestamp, content) { + for (var i in this._handlers[AUTHENTICATION_REQUIRED]) { + this._handlers[AUTHENTICATION_REQUIRED][i].call(this, timestamp); + } + } + + Feed.prototype.onAuthenticationFailed = function(timestamp, content) { + for (var i in this._handlers[AUTHENTICATION_FAILED]) { + this._handlers[AUTHENTICATION_FAILED][i].call(this, timestamp); + } + } + + Feed.prototype.onLogout = function(timestamp, content) { + for (var i in this._handlers[LOGGED_OUT]) { + this._handlers[LOGGED_OUT][i].call(this, timestamp); + } } // Entries management - Feed.prototype.addEntry = function(data) { - this.entryList.push(new Entry(data)) + Feed.prototype.addEntry = function(entry) { + + // types + // add by: timestamp up/down; always to top; always to bottom + + entry.setParent(this); + this.entryList.push(entry); + + this.outputContainer.innerHTML = '
' + this.outputContainer.innerHTML; + + entry.render(); } - Feed.prototype.deleteEntry = function(id) { + Feed.prototype.deleteEntry = function(entry) { + entry.delete(); } - Feed.prototype.updateEntry = function(id, data) { + Feed.prototype.updateEntry = function(entry, data) { + } + + Feed.prototype.empty = function() { + for (var i in this.entryList) { + this.deleteEntry(this.entryList[i]); + delete this.entryList[i]; + } + this.entryList = [] } Feed.prototype.findEntry = function(id) { } + // UI + + Feed.prototype.render = function(id) { + for (var i in this.entryList) { + this.entryList[i].render(); + } + } + // Handlers Feed.prototype.bindChannel = function(channel) { - channel.on('message', function(chid, ts, data) { - // should detect type of message - // if feed addressed then check id - // trigger action if needed + var self = this; + channel.on('message', function(chid, ts, systemEvent) { + if (systemEvent.type == SYSTEM_FEED_MESSAGE) { + feedEvent = new Event(systemEvent.content); + if (feedEvent.user == self.id || feedEvent.user == '*') { + self.onData(feedEvent); + } + } }); } // Stylers Feed.prototype._stylerFunction = function(data) { - return JSON.stringify(data.Data); + return JSON.stringify(data); + } + + Feed.prototype._renderFunction = function(data) { + return JSON.stringify(data); } // Helpers diff --git a/service/stream/static/lp.html b/service/stream/static/lp.html index 14a059e..85eca45 100644 --- a/service/stream/static/lp.html +++ b/service/stream/static/lp.html @@ -5,28 +5,28 @@ window.onload = function() { - feed = elasticfeed.initFeed({ + elasticfeed.init({ channel: { url: 'http://localhost:80', - transport: 'lp' + transport: 'ws' } }); - feed.channel.on('join', function(chid, ts) { - console.log(chid + " joined the chat room"); - }); - - feed.channel.on('leave', function(chid, ts) { - console.log(chid + " left the chat room"); - }); - - feed.channel.on('message', function(chid, ts, data) { - console.log(chid + " " + JSON.stringify(data)); + feed = elasticfeed.initFeed('1', { + channel: { + url: 'http://localhost:80', + transport: 'lp' + }, + outputContainerId: 'my-elastic-feed', + stylerFunction: function(data) { + return '
' + data + '
'; + } }); - window['socket'] = feed.socket } + +
diff --git a/service/stream/static/ws.html b/service/stream/static/ws.html index b658150..032a291 100644 --- a/service/stream/static/ws.html +++ b/service/stream/static/ws.html @@ -5,25 +5,61 @@ window.onload = function() { - feed = elasticfeed.initFeed({ - transport: 'ws' + elasticfeed.init({ + channel: { + url: 'ws://localhost:80', + transport: 'ws' + } }); - feed.channel.on('join', function(chid, ts) { - console.log(chid + " joined the chat room"); + feedList = {} + + feedList[1] = elasticfeed.initFeed('86', { + outputContainerId: 'my-elastic-feed-1', + stylerFunction: function(data) { + return '
' + data + '
'; + } + }); + + feedList[2] = elasticfeed.initFeed('87', { + outputContainerId: 'my-elastic-feed-2', + stylerFunction: function(data) { + return '
' + data + '
'; + } }); - feed.channel.on('leave', function(chid, ts) { - console.log(chid + " left the chat room"); + feedList[3] = elasticfeed.initFeed('88', { + outputContainerId: 'my-elastic-feed-3', + stylerFunction: function(data) { + return '
' + data + '
'; + } }); - feed.channel.on('message', function(chid, ts, data) { - console.log(chid + " " + JSON.stringify(data)); + feedList[4] = elasticfeed.initFeed('89', { + outputContainerId: 'my-elastic-feed-4', + stylerFunction: function(data) { + return '
' + data + '
'; + } }); - window['socket'] = feed.socket + for (var i in feedList) { + feedList[i].on('reload', function(ts) { + this.addEntry(new Entry("reload feed by ts:" + ts)) + }) + + feedList[i].on('empty', function(ts) { + this.empty(); + this.addEntry(new Entry("empty feed by ts:" + ts)) + }) + } + } + +
+
+
+