1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-20 03:17:04 +00:00

Update to 4.0.0-alpha.3 - Response Engines, 'inject' the context.JSON/JSONP/Text/Data/Markdown/Render, Read HISTORY.md

## 4.0.0-alpha.2 -> 4.0.0-alpha.3

**New**

A **Response Engine** gives you the freedom to create/change the
render/response writer for

- `context.JSON`
- `context.JSONP`
- `context.XML`
- `context.Text`
- `context.Markdown`
- `context.Data`
- `context.Render("my_custom_type",mystructOrData{},
iris.RenderOptions{"gzip":false,"charset":"UTF-8"})`
- `context.MarkdownString`
- `iris.ResponseString(...)`

**Fix**
- https://github.com/kataras/iris/issues/294

**Small changes**

- `iris.Config.Charset`, before alpha.3 was `iris.Config.Rest.Charset` &
`iris.Config.Render.Template.Charset`, but you can override it at
runtime by passinth a map `iris.RenderOptions` on the `context.Render`
call .
- `iris.Config.IsDevelopment` , before alpha.1 was
`iris.Config.Render.Template.IsDevelopment`

**Websockets changes**

No need to import the `github.com/kataras/iris/websocket` to use the
`Connection` iteral, the websocket moved inside `kataras/iris` , now all
exported variables' names have the prefix of `Websocket`, so the old
`websocket.Connection` is now `iris.WebsocketConnection`.

Generally, no other changes on the 'frontend API', for response engines
examples and how you can register your own to add more features on
existing response engines or replace them, look
[here](https://github.com/iris-contrib/response).

**BAD SIDE**: E-Book is still pointing on the v3 release, but will be
updated soon.
This commit is contained in:
Gerasimos Maropoulos
2016-07-18 17:40:42 +03:00
parent 077984bd60
commit 675c0d510c
17 changed files with 1717 additions and 1358 deletions

View File

@@ -2,6 +2,41 @@
**How to upgrade**: remove your `$GOPATH/src/github.com/kataras/iris` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`.
## 4.0.0-alpha.2 -> 4.0.0-alpha.3
**New**
A **Response Engine** gives you the freedom to create/change the render/response writer for
- `context.JSON`
- `context.JSONP`
- `context.XML`
- `context.Text`
- `context.Markdown`
- `context.Data`
- `context.Render("my_custom_type",mystructOrData{}, iris.RenderOptions{"gzip":false,"charset":"UTF-8"})`
- `context.MarkdownString`
- `iris.ResponseString(...)`
**Fix**
- https://github.com/kataras/iris/issues/294
**Small changes**
- `iris.Config.Charset`, before alpha.3 was `iris.Config.Rest.Charset` & `iris.Config.Render.Template.Charset`, but you can override it at runtime by passinth a map `iris.RenderOptions` on the `context.Render` call .
- `iris.Config.IsDevelopment` , before alpha.1 was `iris.Config.Render.Template.IsDevelopment`
**Websockets changes**
No need to import the `github.com/kataras/iris/websocket` to use the `Connection` iteral, the websocket moved inside `kataras/iris` , now all exported variables' names have the prefix of `Websocket`, so the old `websocket.Connection` is now `iris.WebsocketConnection`.
Generally, no other changes on the 'frontend API', for response engines examples and how you can register your own to add more features on existing response engines or replace them, look [here](https://github.com/iris-contrib/response).
**BAD SIDE**: E-Book is still pointing on the v3 release, but will be updated soon.
## 4.0.0-alpha.1 -> 4.0.0-alpha.2
**Sessions were re-written **

View File

@@ -145,7 +145,7 @@ I recommend writing your API tests using this new library, [httpexpect](https://
Versioning
------------
Current: **v4.0.0-alpha.2**
Current: **v4.0.0-alpha.3**
> Iris is an active project
@@ -159,7 +159,7 @@ Todo
- [x] Refactor & extend view engine, separate the engines from the main code base, easier for the community to create new view engines
- [x] Refactor & extend sessions, split the different databases functionality to the iris-contrib
- [ ] Refactor & extends the rest render engine in order to be able to developer to use their own implemention for rendering restful types, like, for example a custom JSON implementation using no-standard go package for encode/decode
- [ ] Move the iris/websocket package's source code inside iris/websocket.go one file, to be easier to use by users without import a new package
- [x] Move the iris/websocket package's source code inside iris/websocket.go one file, to be easier to use by users without import a new package
- [ ] configs package should be removed after all these, we will not need big configurations because of different packages splitted & moved to the iris-contrib, we will keep interfaces and all required things inside kataras/iris.go.
- [ ] Implement all [opened community's feature requests](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aopen+label%3A%22feature+request%22)
- [ ] Extend i18n middleware for easier and better internalization support
@@ -192,7 +192,7 @@ License can be found [here](LICENSE).
[Travis]: http://travis-ci.org/kataras/iris
[License Widget]: https://img.shields.io/badge/license-MIT%20%20License%20-E91E63.svg?style=flat-square
[License]: https://github.com/kataras/iris/blob/master/LICENSE
[Release Widget]: https://img.shields.io/badge/release-v4.0.0--alpha.2-blue.svg?style=flat-square
[Release Widget]: https://img.shields.io/badge/release-v4.0.0--alpha.3-blue.svg?style=flat-square
[Release]: https://github.com/kataras/iris/releases
[Chat Widget]: https://img.shields.io/badge/community-chat-00BCD4.svg?style=flat-square
[Chat]: https://kataras.rocket.chat/channel/iris

View File

@@ -5,11 +5,6 @@ import (
"time"
)
var (
// Charset character encoding for various rendering
Charset = "UTF-8"
)
var (
// TimeFormat default time format for any kind of datetime parsing
TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"

View File

@@ -2,13 +2,13 @@ package config
import (
"github.com/imdario/mergo"
"github.com/iris-contrib/rest"
)
// Default values for base Iris conf
const (
DefaultDisablePathCorrection = false
DefaultDisablePathEscape = false
DefaultCharset = "UTF-8"
)
type (
@@ -89,12 +89,14 @@ type (
// default is false
IsDevelopment bool
// Charset character encoding for various rendering
// used for templates and the rest of the responses
// defaults to "UTF-8"
Charset string
// Sessions contains the configs for sessions
Sessions Sessions
// Rest contains the configs for rest render configuration
Rest rest.Config
// Websocket contains the configs for Websocket's server integration
Websocket *Websocket
@@ -111,9 +113,9 @@ func Default() Iris {
DisableBanner: false,
DisableTemplateEngines: false,
IsDevelopment: false,
Charset: DefaultCharset,
ProfilePath: "",
Sessions: DefaultSessions(),
Rest: rest.DefaultConfig(),
Websocket: DefaultWebsocket(),
Tester: DefaultTester(),
}

View File

@@ -39,8 +39,19 @@ const (
contentLength = "Content-Length"
// ContentHTML is the string of text/html response headers
contentHTML = "text/html"
// ContentBINARY is the string of application/octet-stream response headers
contentBINARY = "application/octet-stream"
// ContentBinary header value for binary data.
contentBinary = "application/octet-stream"
// ContentJSON header value for JSON data.
contentJSON = "application/json"
// ContentJSONP header value for JSONP data.
contentJSONP = "application/javascript"
// ContentText header value for Text data.
contentText = "text/plain"
// ContentXML header value for XML data.
contentXML = "text/xml"
// contentMarkdown custom key/content type, the real is the text/html
contentMarkdown = "text/markdown"
// LastModified "Last-Modified"
lastModified = "Last-Modified"
@@ -461,26 +472,29 @@ func (ctx *Context) Write(format string, a ...interface{}) {
ctx.RequestCtx.WriteString(fmt.Sprintf(format, a...))
}
// HTML writes html string with a http status
func (ctx *Context) HTML(httpStatus int, htmlContents string) {
ctx.SetContentType(contentHTML + ctx.framework.rest.CompiledCharset)
ctx.RequestCtx.SetStatusCode(httpStatus)
ctx.RequestCtx.WriteString(htmlContents)
// Gzip accepts bytes, which are compressed to gzip format and sent to the client
func (ctx *Context) Gzip(b []byte, status int) {
ctx.RequestCtx.Response.Header.Add("Content-Encoding", "gzip")
gzipWriter := ctx.framework.gzipWriterPool.Get().(*gzip.Writer)
gzipWriter.Reset(ctx.RequestCtx.Response.BodyWriter())
gzipWriter.Write(b)
gzipWriter.Close()
ctx.framework.gzipWriterPool.Put(gzipWriter)
}
// Data writes out the raw bytes as binary data.
func (ctx *Context) Data(status int, v []byte) error {
return ctx.framework.rest.Data(ctx.RequestCtx, status, v)
}
// RenderWithStatus builds up the response from the specified template and bindings.
// Note: parameter layout has meaning only when using the iris.HTMLTemplate
// RenderWithStatus builds up the response from the specified template or a response engine.
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or response engine
func (ctx *Context) RenderWithStatus(status int, name string, binding interface{}, options ...map[string]interface{}) error {
ctx.SetStatusCode(status)
return ctx.framework.templates.GetBy(name).Execute(ctx, name, binding, options...)
if strings.IndexByte(name, '.') > 0 { //we have template
return ctx.framework.templates.getBy(name).execute(ctx, name, binding, options...)
}
return ctx.framework.responses.getBy(name).render(ctx, binding, options...)
}
// Render same as .RenderWithStatus but with status to iris.StatusOK (200)
// Render same as .RenderWithStatus but with status to iris.StatusOK (200) if no previous status exists
// builds up the response from the specified template or a response engine.
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or response engine
func (ctx *Context) Render(name string, binding interface{}, options ...map[string]interface{}) error {
errCode := ctx.RequestCtx.Response.StatusCode()
if errCode <= 0 {
@@ -490,6 +504,8 @@ func (ctx *Context) Render(name string, binding interface{}, options ...map[stri
}
// MustRender same as .Render but returns 500 internal server http status (error) if rendering fail
// builds up the response from the specified template or a response engine.
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or response engine
func (ctx *Context) MustRender(name string, binding interface{}, options ...map[string]interface{}) {
if err := ctx.Render(name, binding, options...); err != nil {
ctx.Panic()
@@ -503,29 +519,43 @@ func (ctx *Context) TemplateString(name string, binding interface{}, options ...
return ctx.framework.TemplateString(name, binding, options...)
}
// HTML writes html string with a http status
func (ctx *Context) HTML(status int, htmlContents string) {
if err := ctx.RenderWithStatus(status, contentHTML, htmlContents); err != nil {
ctx.SetContentType(contentHTML + "; charset=" + ctx.framework.Config.Charset)
ctx.RequestCtx.SetStatusCode(status)
ctx.RequestCtx.WriteString(htmlContents)
}
}
// Data writes out the raw bytes as binary data.
func (ctx *Context) Data(status int, v []byte) error {
return ctx.RenderWithStatus(status, contentBinary, v)
}
// JSON marshals the given interface object and writes the JSON response.
func (ctx *Context) JSON(status int, v interface{}) error {
return ctx.framework.rest.JSON(ctx.RequestCtx, status, v)
return ctx.RenderWithStatus(status, contentJSON, v)
}
// JSONP marshals the given interface object and writes the JSON response.
func (ctx *Context) JSONP(status int, callback string, v interface{}) error {
return ctx.framework.rest.JSONP(ctx.RequestCtx, status, callback, v)
return ctx.RenderWithStatus(status, contentJSONP, v, map[string]interface{}{"callback": callback})
}
// Text writes out a string as plain text.
func (ctx *Context) Text(status int, v string) error {
return ctx.framework.rest.Text(ctx.RequestCtx, status, v)
return ctx.RenderWithStatus(status, contentText, v)
}
// XML marshals the given interface object and writes the XML response.
func (ctx *Context) XML(status int, v interface{}) error {
return ctx.framework.rest.XML(ctx.RequestCtx, status, v)
return ctx.RenderWithStatus(status, contentXML, v)
}
// MarkdownString parses the (dynamic) markdown string and returns the converted html string
func (ctx *Context) MarkdownString(markdownText string) string {
return ctx.framework.rest.Markdown([]byte(markdownText))
return ctx.framework.ResponseString(contentMarkdown, markdownText)
}
// Markdown parses and renders to the client a particular (dynamic) markdown string

View File

@@ -641,7 +641,7 @@ func joinMiddleware(middleware1 Middleware, middleware2 Middleware) Middleware {
func profileMiddleware(debugPath string) Middleware {
htmlMiddleware := HandlerFunc(func(ctx *Context) {
ctx.SetContentType(contentHTML + "; charset=" + config.Charset)
ctx.SetContentType(contentHTML + "; charset=" + ctx.framework.Config.Charset)
ctx.Next()
})
indexHandler := ToHandlerFunc(pprof.Index)

160
iris.go
View File

@@ -45,13 +45,14 @@
//
// -----------------------------DOCUMENTATION----------------------------
// ----------------------------_______________---------------------------
// For middleware, templates, sessions, websockets, mails, subdomains,
// For middleware, template engines, response engines, sessions, websockets, mails, subdomains,
// dynamic subdomains, routes, party of subdomains & routes and much more
// visit https://www.gitbook.com/book/kataras/iris/details
package iris // import "github.com/kataras/iris"
import (
"fmt"
"io"
"net/http"
"os"
"path"
@@ -61,23 +62,29 @@ import (
"testing"
"time"
"github.com/klauspost/compress/gzip"
"sync"
"github.com/gavv/httpexpect"
"github.com/iris-contrib/errors"
"github.com/iris-contrib/logger"
"github.com/iris-contrib/rest"
"github.com/iris-contrib/response/data"
"github.com/iris-contrib/response/json"
"github.com/iris-contrib/response/jsonp"
"github.com/iris-contrib/response/markdown"
"github.com/iris-contrib/response/text"
"github.com/iris-contrib/response/xml"
"github.com/iris-contrib/template/html"
"github.com/kataras/iris/config"
"github.com/kataras/iris/context"
"github.com/kataras/iris/utils"
"github.com/kataras/iris/websocket"
"github.com/valyala/fasthttp"
)
const (
// Version of the iris
Version = "4.0.0-alpha.2"
Version = "4.0.0-alpha.3"
banner = ` _____ _
|_ _| (_)
@@ -93,7 +100,7 @@ var (
Config *config.Iris
Logger *logger.Logger
Plugins PluginContainer
Websocket websocket.Server
Websocket WebsocketServer
Servers *ServerList
// Available is a channel type of bool, fired to true when the server is opened and all plugins ran
// never fires false, if the .Close called then the channel is re-allocating.
@@ -151,6 +158,7 @@ type (
Path(string, ...interface{}) string
URL(string, ...interface{}) string
TemplateString(string, interface{}, ...map[string]interface{}) string
ResponseString(string, interface{}, ...map[string]interface{}) string
Tester(t *testing.T) *httpexpect.Expect
}
@@ -159,18 +167,18 @@ type (
// Implements the FrameworkAPI
Framework struct {
*muxAPI
rest *rest.Render
Config *config.Iris
gzipWriterPool sync.Pool // used for several methods, usually inside context
sessions *sessionsManager
templates *TemplateEngines
responses *responseEngines
templates *templateEngines
// fields which are useful to the user/dev
// the last added server is the main server
Servers *ServerList
Config *config.Iris
// configuration by instance.Logger.Config
Logger *logger.Logger
Plugins PluginContainer
Websocket websocket.Server
Websocket WebsocketServer
Available chan bool
// this is setted once when .Tester(t) is called
testFramework *httpexpect.Expect
@@ -188,7 +196,14 @@ func New(cfg ...config.Iris) *Framework {
// we always use 's' no 'f' because 's' is easier for me to remember because of 'station'
// some things never change :)
s := &Framework{Config: &c, Available: make(chan bool)}
s := &Framework{
Config: &c,
gzipWriterPool: sync.Pool{New: func() interface{} {
return &gzip.Writer{}
}},
responses: &responseEngines{},
Available: make(chan bool),
}
{
///NOTE: set all with s.Config pointer
// set the Logger
@@ -196,17 +211,17 @@ func New(cfg ...config.Iris) *Framework {
// set the plugin container
s.Plugins = &pluginContainer{logger: s.Logger}
// set the templates
s.templates = &TemplateEngines{
Helpers: map[string]interface{}{
s.templates = &templateEngines{
helpers: map[string]interface{}{
"url": s.URL,
"urlpath": s.Path,
},
Engines: make([]*TemplateEngineWrapper, 0),
engines: make([]*templateEngineWrapper, 0),
}
//set the session manager
s.sessions = newSessionsManager(c.Sessions)
// set the websocket server
s.Websocket = websocket.NewServer(s.Config.Websocket)
s.Websocket = NewWebsocketServer(s.Config.Websocket)
// set the servemux, which will provide us the public API also, with its context pool
mux := newServeMux(sync.Pool{New: func() interface{} { return &Context{framework: s} }}, s.Logger)
mux.onLookup = s.Plugins.DoPreLookup
@@ -220,22 +235,46 @@ func New(cfg ...config.Iris) *Framework {
}
func (s *Framework) initialize() {
// set the rest
s.rest = rest.New(s.Config.Rest)
// prepare the response engines, if no response engines setted for the default content-types
// then add them
for _, ctype := range defaultResponseKeys {
if rengine := s.responses.getBy(ctype); rengine == nil {
// if not exists
switch ctype {
case contentText:
s.UseResponse(text.New(), ctype)
case contentBinary:
s.UseResponse(data.New(), ctype)
case contentJSON:
s.UseResponse(json.New(), ctype)
case contentJSONP:
s.UseResponse(jsonp.New(), ctype)
case contentXML:
s.UseResponse(xml.New(), ctype)
case contentMarkdown:
s.UseResponse(markdown.New(), ctype)
}
}
}
// prepare the templates if enabled
if !s.Config.DisableTemplateEngines {
if err := s.templates.LoadAll(); err != nil {
s.Logger.Panic(err) // panic on templates loading before listening if we have an error.
}
// check and prepare the templates
if len(s.templates.Engines) == 0 { // no template engine is registered, let's use the default
if len(s.templates.engines) == 0 { // no template engine is registered, let's use the default
s.UseTemplate(html.New())
}
s.templates.Reload = s.Config.IsDevelopment
if err := s.templates.loadAll(); err != nil {
s.Logger.Panic(err) // panic on templates loading before listening if we have an error.
}
s.templates.reload = s.Config.IsDevelopment
}
// listen to websocket connections
websocket.RegisterServer(s, s.Websocket, s.Logger)
RegisterWebsocketServer(s, s.Websocket, s.Logger)
// prepare the mux & the server
s.mux.setCorrectPath(!s.Config.DisablePathCorrection)
@@ -486,6 +525,40 @@ func (s *Framework) UseSessionDB(db SessionDatabase) {
s.sessions.provider.registerDatabase(db)
}
// UseResponse accepts a ResponseEngine and the key or content type on which the developer wants to register this response engine
// the gzip and charset are automatically supported by Iris, by passing the iris.RenderOptions{} map on the context.Render
// context.Render renders this response or a template engine if no response engine with the 'key' found
// with these engines you can inject the context.JSON,Text,Data,JSONP,XML also
// to do that just register with UseResponse(myEngine,"application/json") and so on
// look at the https://github.com/iris-contrib/response for examples
//
// if more than one respone engine with the same key/content type exists then the results will be appended to the final request's body
// this allows the developer to be able to create 'middleware' responses engines
//
// Note: if you pass an engine which contains a dot('.') as key, then the engine will not be registered.
// you don't have to import and use github.com/iris-contrib/json, jsonp, xml, data, text, markdown
// because iris uses these by default if no other response engine is registered for these content types
func UseResponse(e ResponseEngine, forContentTypesOrKeys ...string) {
Default.UseResponse(e, forContentTypesOrKeys...)
}
// UseResponse accepts a ResponseEngine and the key or content type on which the developer wants to register this response engine
// the gzip and charset are automatically supported by Iris, by passing the iris.RenderOptions{} map on the context.Render
// context.Render renders this response or a template engine if no response engine with the 'key' found
// with these engines you can inject the context.JSON,Text,Data,JSONP,XML also
// to do that just register with UseResponse(myEngine,"application/json") and so on
// look at the https://github.com/iris-contrib/response for examples
//
// if more than one respone engine with the same key/content type exists then the results will be appended to the final request's body
// this allows the developer to be able to create 'middleware' responses engines
//
// Note: if you pass an engine which contains a dot('.') as key, then the engine will not be registered.
// you don't have to import and use github.com/iris-contrib/json, jsonp, xml, data, text, markdown
// because iris uses these by default if no other response engine is registered for these content types
func (s *Framework) UseResponse(e ResponseEngine, forContentTypesOrKeys ...string) {
s.responses.add(e, forContentTypesOrKeys...)
}
// UseTemplate adds a template engine to the iris view system
// it does not build/load them yet
func UseTemplate(e TemplateEngine) *TemplateEngineLocation {
@@ -495,7 +568,7 @@ func UseTemplate(e TemplateEngine) *TemplateEngineLocation {
// UseTemplate adds a template engine to the iris view system
// it does not build/load them yet
func (s *Framework) UseTemplate(e TemplateEngine) *TemplateEngineLocation {
return s.templates.Add(e)
return s.templates.add(e)
}
// UseGlobal registers Handler middleware to the beginning, prepends them instead of append
@@ -716,6 +789,25 @@ func (s *Framework) URL(routeName string, args ...interface{}) (url string) {
return
}
// AcquireGzip prepares a gzip writer and returns it
//
// Note that: each iris station has its own pool
// see ReleaseGzip
func (s *Framework) AcquireGzip(w io.Writer) *gzip.Writer {
gzipWriter := s.gzipWriterPool.Get().(*gzip.Writer)
gzipWriter.Reset(w)
return gzipWriter
}
// ReleaseGzip called when flush/close and put the gzip writer back to the pool
//
// Note that: each iris station has its own pool
// see AcquireGzip
func (s *Framework) ReleaseGzip(gzipWriter *gzip.Writer) {
gzipWriter.Close()
s.gzipWriterPool.Put(gzipWriter)
}
// TemplateString executes a template from the default template engine and returns its result as string, useful when you want it for sending rich e-mails
// returns empty string on error
func TemplateString(templateFile string, pageContext interface{}, options ...map[string]interface{}) string {
@@ -728,7 +820,25 @@ func (s *Framework) TemplateString(templateFile string, pageContext interface{},
if s.Config.DisableTemplateEngines {
return ""
}
res, err := s.templates.GetBy(templateFile).ExecuteToString(templateFile, pageContext, options...)
res, err := s.templates.getBy(templateFile).executeToString(templateFile, pageContext, options...)
if err != nil {
return ""
}
return res
}
// ResponseString returns the string of a response engine,
// does not render it to the client
// returns empty string on error
func ResponseString(keyOrContentType string, obj interface{}, options ...map[string]interface{}) string {
return Default.ResponseString(keyOrContentType, obj, options...)
}
// ResponseString returns the string of a response engine,
// does not render it to the client
// returns empty string on error
func (s *Framework) ResponseString(keyOrContentType string, obj interface{}, options ...map[string]interface{}) string {
res, err := s.responses.getBy(keyOrContentType).toString(obj, options...)
if err != nil {
return ""
}

220
response.go Normal file
View File

@@ -0,0 +1,220 @@
package iris
import (
"strings"
"github.com/iris-contrib/errors"
)
type (
// notes for me:
// edw an kai kalh idea alla den 9a borw na exw ta defaults mesa sto iris
// kai 9a prepei na to metaferw auto sto context i sto utils kai pragmatika den leei
// na kanoun import auto gia na kanoun to response engine, ara na prospa9isw kapws aliws mesw context.IContext mono
// ektos an metaferw ta defaults mesa sto iris
// alla an to kanw auto 9a prepei na vrw tropo na kanoun configuration ta defaults
// kai to idio prepei na kanw kai sto template engine html tote...
// diladi na kanw prwta to render tou real engine kai meta na parw
// ta contents tou body kai na ta kanw gzip ? na kanw resetbody kai na ta ksanasteilw?
// alla auto einai argh methodos kai gia ton poutso dn m aresei
// kai ola auta gia na mh valw ena property parapanw? 9a valw ...
// 9a einai writer,headers,object,options... anagastika.
/* notes for me:
english 'final' thoughs' results now:
the response engine will be registered with its content type
for example: iris.UseResponse("application/json", the engine or func here, optionalOptions...)
if more than one response engines registered for the same content type
then all the content results will be sent to the client
there will be available a system something like middleware but for the response engines
this will be useful when one response engine's job is only to add more content to the existing/parent response engine's results .
for example when you want to add a standard json object like a last_fetch { "year":...,"month":...,"day":...} for all json responses.
The engine will not have access to context or something like that.
The default engines will registered when no other engine with the same content type
already registered, like I did with template engines, so if someone wants to make a 'middleware' for the default response engines
must register them explicit and after register his/her response engine too, for that reason
the default engines will not be located inside iris but inside iris-contrib (like the default,and others, template engine),
the reason is to be easier to the user/dev to remember what import path should use when he/she wants to edit something,
for templates it's 'iris-contrib/template', for response engines will be 'iris-contrib/response'.
The body content will return as []byte, al/mong with an error if something bad happened.
Now you may ask why not set the header inside from response engine? because to do that we could have one of these four downsides:
1.to have access to context.IContext or *Context(if *Context then default engines should live here in iris repo)
and if we have context.IContext or *Context we will not be able to set a fast built'n gzip option,
because we would copy the contents to the gzip writer, and after copy these contents back to the response body's writer
but with an io.Writer as parameter we can simple change this writer to gzip writer and continue to the response engine after.
2. we could make something like ResponseWriter struct { io.Writer,Header *fasthttp.ResponseHeader}
inside iris repo(then the default response engines should exists in the iris repo and configuration will depends on the iris' configs )
or inside context/ folder inside iris repo, then the user/dev should import this path to
do his/her response engine, and I want simple things as usual, also we would make a pool for this response writer and create new if not available exist,
and this is performarnce downs witch I dissalow on Iris whne no need.
3. to have 4 parameters, the writer, the headers(again the user should import the fasthttp to do his/her response engine and I want simple things, as I told before),
the object and the optional parameters
4. one more function to implement like 'ContentType() string', but if we select this we lose the functionality for ResponseEngine created as simple function,
and the biggest issue will be that one response engine must explicit exists for one content type, the user/dev will not be available (to easly)
to set the content type for the engine.
these were the reasons I decide to set the content type by the frontend iris API itself and not taken by the response engine.
The Response will have two parameters (one required only) interface{], ...options}, and two return values([]byte,error)
The (first) parameter will be an interface{}, for json a json struct, for xml an xml struct, for binary data .([]byte) and so on
There will be available a second optional parameter, map of options, the "gzip" option will be built'n implemented by iris
so the response engines no need to manually add gzip support(same with template engines).
The Charset will be added to the headers automatically, for the previous example of json and the default charset which is UTF-8
the end "Content-Type" header content will be: "application/json; charset=UTF-8"
if the registered content type is not a $content/type then the text/plain will be sent to the client.
OR WAIT, some engines maybe want to set the content type or other headers dynamically or render a response depends on cookies or some other existence headers
on that situtions it will be impossible with this implementation I explained before, so...
access to context.IContext and return the []byte, in order to be able to add the built'n gzip support
the dev/user will have to make this import no no no we stick to the previous though, because
if the user wants to check all that he/she can just use a middleware with .Use/.UseFunc
this is not a middleware implementation, this is a custom content rendering, let's stick to that.
Ok I did that and I realized that template and response engines, final method structure (string,interface{},options...) is the same
so I make the ctx.Render/RenderWithStatus to work with both engines, so the developer can use any type of response engine and render it with ease.
Maybe at the future I could have one file 'render.go' which will contain the template engines and response engines, we will see, these all are unique so excuse me if something goes wrong xD
That's all. Hope some one (other than me) will understand the english here...
*/
// ResponseEngine is the interface which all response engines should implement to send responses
// ResponseEngine(s) can be registered with,for example: iris.UseResponse(json.New(), "application/json")
ResponseEngine interface {
Response(interface{}, ...map[string]interface{}) ([]byte, error)
}
// ResponseEngineFunc is the alternative way to implement a ResponseEngine using a simple function
ResponseEngineFunc func(interface{}, ...map[string]interface{}) ([]byte, error)
// responseEngineMap is a wrapper with key (content type or name) values(engines) for the registered response engine
// it contains all response engines for a specific contentType and two functions, render and toString
// these will be used by the iris' context and iris' ResponseString, yes like TemplateToString
// it's an internal struct, no need to be exported and return that on registration,
// because the two top funcs will be easier to use by the user/dev for multiple engines
responseEngineMap struct {
values []ResponseEngine
// this is used in order to the wrapper to be gettable by the responseEngines iteral,
// if key is not a $content/type then the text/plain will be sent to the client
key string
contentType string // it's not the full content type with charset
}
)
var (
// markdown is custom type, used inside iris to initialize the defaults response engines if no other engine registered with these keys
defaultResponseKeys = [...]string{contentText, contentXML, contentBinary, contentJSON, contentJSONP, contentMarkdown}
)
var errNoResponseEngineFound = errors.New("No response engine found")
// Response returns a response to the client(request's body content)
func (r ResponseEngineFunc) Response(obj interface{}, options ...map[string]interface{}) ([]byte, error) {
return r(obj, options...)
}
// on context: Send(contentType string, obj interface{}, ...options)
func (r *responseEngineMap) add(engine ResponseEngine) {
r.values = append(r.values, engine)
}
// the gzip and charset options are built'n with iris
func (r *responseEngineMap) render(ctx *Context, obj interface{}, options ...map[string]interface{}) error {
if r == nil {
//render, but no response engine registered, this caused by context.RenderWithStatus, and responseEngines. getBy
return errNoResponseEngineFound.Return()
}
var finalResult []byte
for i, n := 0, len(r.values); i < n; i++ {
result, err := r.values[i].Response(obj, options...)
if err != nil { // fail on first the first error
return err
}
finalResult = append(finalResult, result...)
}
gzipEnabled := false
charset := ctx.framework.Config.Charset
if len(options) > 0 {
gzipEnabled = getGzipOption(options[0]) // located to the template.go below the RenderOptions
if chs := getCharsetOption(options[0]); chs != "" {
charset = chs
}
}
ctx.SetContentType(r.contentType + "; charset=" + charset)
if gzipEnabled {
ctx.Response.Header.Add("Content-Encoding", "gzip")
gzipWriter := ctx.framework.AcquireGzip(ctx.Response.BodyWriter())
defer ctx.framework.ReleaseGzip(gzipWriter)
_, err := gzipWriter.Write(finalResult)
if err != nil {
return err
}
} else {
ctx.Response.SetBody(finalResult)
}
return nil
}
func (r *responseEngineMap) toString(obj interface{}, options ...map[string]interface{}) (string, error) {
if r == nil {
//render, but no response engine registered, this caused by context.RenderWithStatus, and responseEngines. getBy
return "", errNoResponseEngineFound.Return()
}
var finalResult []byte
for i, n := 0, len(r.values); i < n; i++ {
result, err := r.values[i].Response(obj, options...)
if err != nil {
return "", err
}
finalResult = append(finalResult, result...)
}
return string(finalResult), nil
}
type responseEngines struct {
engines []*responseEngineMap
}
// add accepts a simple response engine with its content type or key, key should not contains a dot('.').
func (r *responseEngines) add(engine ResponseEngine, forContentTypesOrKeys ...string) {
if r.engines == nil {
r.engines = make([]*responseEngineMap, 0)
}
for _, key := range forContentTypesOrKeys {
if strings.IndexByte(key, '.') != -1 { // the dot is not allowed as key
continue // skip this engine
}
defaultCtypeAndKey := contentText
if len(key) == 0 {
//if empty key, then set it to text/plain
key = defaultCtypeAndKey
}
engineMap := r.getBy(key)
if engineMap == nil {
ctype := defaultCtypeAndKey
if strings.IndexByte(key, slashByte) != -1 { // pure check, but developer should know the content types at least.
// we have 'valid' content type
ctype = key
}
engineMap = &responseEngineMap{values: make([]ResponseEngine, 0), key: key, contentType: ctype}
r.engines = append(r.engines, engineMap)
}
engineMap.add(engine)
}
}
func (r *responseEngines) getBy(key string) *responseEngineMap {
for i, n := 0, len(r.engines); i < n; i++ {
return r.engines[i]
}
return nil
}

View File

@@ -1,11 +1,9 @@
package iris
import (
"compress/gzip"
"io"
"path/filepath"
"sync"
"github.com/iris-contrib/errors"
"github.com/kataras/iris/utils"
@@ -17,14 +15,6 @@ var (
// DefaultTemplateDirectory the default directory if empty setted
DefaultTemplateDirectory = "." + utils.PathSeparator + "templates"
)
var (
// ContentTypeHTML the content type header for rendering
// this can be changed
ContentTypeHTML = "text/html"
// Charset the charset header for rendering
// this can be changed
Charset = "UTF-8"
)
const (
@@ -81,6 +71,22 @@ func (t TemplateFuncs) IsFree(key string) bool {
return true
}
func getGzipOption(options map[string]interface{}) bool {
gzipOpt := options["gzip"] // we only need that, so don't create new map to keep the options.
if b, isBool := gzipOpt.(bool); isBool {
return b
}
return false
}
func getCharsetOption(options map[string]interface{}) string {
charsetOpt := options["charset"]
if s, isString := charsetOpt.(string); isString {
return s
}
return "" // we return empty in order to set the default charset if not founded.
}
type (
// TemplateEngineLocation contains the funcs to set the location for the templates by directory or by binary
TemplateEngineLocation struct {
@@ -119,14 +125,12 @@ func (t *TemplateEngineLocation) isBinary() bool {
return t.assetFn != nil && t.namesFn != nil
}
// TemplateEngineWrapper is the wrapper of a template engine
type TemplateEngineWrapper struct {
// templateEngineWrapper is the wrapper of a template engine
type templateEngineWrapper struct {
TemplateEngine
location *TemplateEngineLocation
buffer *utils.BufferPool
gzipWriterPool sync.Pool
reload bool
combiledContentType string
}
var (
@@ -134,7 +138,7 @@ var (
errNoTemplateEngineForExt = errors.New("No template engine found to manage '%s' extensions")
)
func (t *TemplateEngineWrapper) load() error {
func (t *templateEngineWrapper) load() error {
if t.location.isBinary() {
t.LoadAssets(t.location.directory, t.location.extension, t.location.assetFn, t.location.namesFn)
} else if t.location.directory != "" {
@@ -145,13 +149,14 @@ func (t *TemplateEngineWrapper) load() error {
return nil
}
// Execute execute a template and write its result to the context's body
// execute execute a template and write its result to the context's body
// options are the optional runtime options can be passed by user and catched by the template engine when render
// an example of this is the "layout"
// note that gzip option is an iris dynamic option which exists for all template engines
func (t *TemplateEngineWrapper) Execute(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) (err error) {
// the gzip and charset options are built'n with iris
func (t *templateEngineWrapper) execute(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) (err error) {
if t == nil {
//file extension, but no template engine registered, this caused by context, and TemplateEngines. GetBy
//file extension, but no template engine registered, this caused by context, and templateEngines. getBy
return errNoTemplateEngineForExt.Format(filepath.Ext(filename))
}
if t.reload {
@@ -162,10 +167,11 @@ func (t *TemplateEngineWrapper) Execute(ctx *Context, filename string, binding i
// we do all these because we don't want to initialize a new map for each execution...
gzipEnabled := false
charset := ctx.framework.Config.Charset
if len(options) > 0 {
gzipOpt := options[0]["gzip"] // we only need that, so don't create new map to keep the options.
if b, isBool := gzipOpt.(bool); isBool {
gzipEnabled = b
gzipEnabled = getGzipOption(options[0])
if chs := getCharsetOption(options[0]); chs != "" {
charset = chs
}
}
@@ -178,26 +184,25 @@ func (t *TemplateEngineWrapper) Execute(ctx *Context, filename string, binding i
}
}
ctx.SetContentType(contentHTML + "; charset=" + charset)
var out io.Writer
if gzipEnabled {
ctx.Response.Header.Add("Content-Encoding", "gzip")
gzipWriter := t.gzipWriterPool.Get().(*gzip.Writer)
gzipWriter.Reset(ctx.Response.BodyWriter())
defer gzipWriter.Close()
defer t.gzipWriterPool.Put(gzipWriter)
gzipWriter := ctx.framework.AcquireGzip(ctx.Response.BodyWriter())
defer ctx.framework.ReleaseGzip(gzipWriter)
out = gzipWriter
} else {
out = ctx.Response.BodyWriter()
}
ctx.SetHeader("Content-Type", t.combiledContentType)
return t.ExecuteWriter(out, filename, binding, options...)
}
// ExecuteToString executes a template from a specific template engine and returns its contents result as string, it doesn't renders
func (t *TemplateEngineWrapper) ExecuteToString(filename string, binding interface{}, opt ...map[string]interface{}) (result string, err error) {
// executeToString executes a template from a specific template engine and returns its contents result as string, it doesn't renders
func (t *templateEngineWrapper) executeToString(filename string, binding interface{}, opt ...map[string]interface{}) (result string, err error) {
if t == nil {
//file extension, but no template engine registered, this caused by context, and TemplateEngines. GetBy
//file extension, but no template engine registered, this caused by context, and templateEngines. getBy
return "", errNoTemplateEngineForExt.Format(filepath.Ext(filename))
}
if t.reload {
@@ -215,18 +220,18 @@ func (t *TemplateEngineWrapper) ExecuteToString(filename string, binding interfa
return
}
// TemplateEngines is the container and manager of the template engines
type TemplateEngines struct {
Helpers map[string]interface{}
Engines []*TemplateEngineWrapper
Reload bool
// templateEngines is the container and manager of the template engines
type templateEngines struct {
helpers map[string]interface{}
engines []*templateEngineWrapper
reload bool
}
// GetBy receives a filename, gets its extension and returns the template engine responsible for that file extension
func (t *TemplateEngines) GetBy(filename string) *TemplateEngineWrapper {
// getBy receives a filename, gets its extension and returns the template engine responsible for that file extension
func (t *templateEngines) getBy(filename string) *templateEngineWrapper {
extension := filepath.Ext(filename)
for i, n := 0, len(t.Engines); i < n; i++ {
e := t.Engines[i]
for i, n := 0, len(t.engines); i < n; i++ {
e := t.engines[i]
if e.location.extension == extension {
return e
@@ -235,38 +240,34 @@ func (t *TemplateEngines) GetBy(filename string) *TemplateEngineWrapper {
return nil
}
// Add adds but not loads a template engine
func (t *TemplateEngines) Add(e TemplateEngine) *TemplateEngineLocation {
// add adds but not loads a template engine
func (t *templateEngines) add(e TemplateEngine) *TemplateEngineLocation {
location := &TemplateEngineLocation{}
// add the iris helper funcs
if funcer, ok := e.(TemplateEngineFuncs); ok {
if funcer.Funcs() != nil {
for k, v := range t.Helpers {
for k, v := range t.helpers {
funcer.Funcs()[k] = v
}
}
}
tmplEngine := &TemplateEngineWrapper{
tmplEngine := &templateEngineWrapper{
TemplateEngine: e,
location: location,
buffer: utils.NewBufferPool(20),
gzipWriterPool: sync.Pool{New: func() interface{} {
return &gzip.Writer{}
}},
reload: t.Reload,
combiledContentType: ContentTypeHTML + "; " + Charset,
buffer: utils.NewBufferPool(8),
reload: t.reload,
}
t.Engines = append(t.Engines, tmplEngine)
t.engines = append(t.engines, tmplEngine)
return location
}
// LoadAll loads all templates using all template engines, returns the first error
// loadAll loads all templates using all template engines, returns the first error
// called on iris' initialize
func (t *TemplateEngines) LoadAll() error {
for i, n := 0, len(t.Engines); i < n; i++ {
e := t.Engines[i]
func (t *templateEngines) loadAll() error {
for i, n := 0, len(t.engines); i < n; i++ {
e := t.engines[i]
if e.location.directory == "" {
e.location.directory = DefaultTemplateDirectory // the defualt dir ./templates
}

1207
websocket.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
# Package information
This package is new and unique, if you notice a bug or issue [post it here](https://github.com/kataras/iris/issues).
# How to use
[E-Book section](https://kataras.gitbooks.io/iris/content/package-websocket.html)
## Notes
On **OSX + Safari**, we had an issue which is **fixed** now. BUT by the browser's Engine Design the socket is not closed until the whole browser window is closed,
so the **connection.OnDisconnect** event will fire when the user closes the **window browser**, **not just the browser's tab**.
- Relative issue: https://github.com/kataras/iris/issues/175

View File

@@ -1,258 +0,0 @@
const stringMessageType = 0;
const intMessageType = 1;
const boolMessageType = 2;
// bytes is missing here for reasons I will explain somewhen
const jsonMessageType = 4;
const prefix = "iris-websocket-message:";
const separator = ";";
const prefixLen = prefix.length;
var separatorLen = separator.length;
var prefixAndSepIdx = prefixLen + separatorLen - 1;
var prefixIdx = prefixLen - 1;
var separatorIdx = separatorLen - 1;
type onConnectFunc = () => void;
type onDisconnectFunc = () => void;
type onNativeMessageFunc = (websocketMessage: string) => void;
type onMessageFunc = (message: any) => void;
class Ws {
private conn: WebSocket;
private isReady: boolean;
// events listeners
private connectListeners: onConnectFunc[] = [];
private disconnectListeners: onDisconnectFunc[] = [];
private nativeMessageListeners: onNativeMessageFunc[] = [];
private messageListeners: { [event: string]: onMessageFunc[] } = {};
//
constructor(endpoint: string, protocols?: string[]) {
if (!window["WebSocket"]) {
return;
}
if (endpoint.indexOf("ws") == -1) {
endpoint = "ws://" + endpoint;
}
if (protocols != null && protocols.length > 0) {
this.conn = new WebSocket(endpoint, protocols);
} else {
this.conn = new WebSocket(endpoint);
}
this.conn.onopen = ((evt: Event): any => {
this.fireConnect();
this.isReady = true;
return null;
});
this.conn.onclose = ((evt: Event): any => {
this.fireDisconnect();
return null;
});
this.conn.onmessage = ((evt: MessageEvent) => {
this.messageReceivedFromConn(evt);
});
}
//utils
private isNumber(obj: any): boolean {
return !isNaN(obj - 0) && obj !== null && obj !== "" && obj !== false;
}
private isString(obj: any): boolean {
return Object.prototype.toString.call(obj) == "[object String]";
}
private isBoolean(obj: any): boolean {
return typeof obj === 'boolean' ||
(typeof obj === 'object' && typeof obj.valueOf() === 'boolean');
}
private isJSON(obj: any): boolean {
try {
JSON.parse(obj);
} catch (e) {
return false;
}
return true;
}
//
// messages
private _msg(event: string, messageType: number, dataMessage: string): string {
return prefix + event + separator + String(messageType) + separator + dataMessage;
}
private encodeMessage(event: string, data: any): string {
let m = "";
let t = 0;
if (this.isNumber(data)) {
t = intMessageType;
m = data.toString();
} else if (this.isBoolean(data)) {
t = boolMessageType;
m = data.toString();
} else if (this.isString(data)) {
t = stringMessageType;
m = data.toString();
} else if (this.isJSON(data)) {
//propably json-object
t = jsonMessageType;
m = JSON.stringify(data);
} else {
console.log("Invalid");
}
return this._msg(event, t, m);
}
private decodeMessage<T>(event: string, websocketMessage: string): T | any {
//iris-websocket-message;user;4;themarshaledstringfromajsonstruct
let skipLen = prefixLen + separatorLen + event.length + 2;
if (websocketMessage.length < skipLen + 1) {
return null;
}
let messageType = parseInt(websocketMessage.charAt(skipLen - 2));
let theMessage = websocketMessage.substring(skipLen, websocketMessage.length);
if (messageType == intMessageType) {
return parseInt(theMessage);
} else if (messageType == boolMessageType) {
return Boolean(theMessage);
} else if (messageType == stringMessageType) {
return theMessage;
} else if (messageType == jsonMessageType) {
return JSON.parse(theMessage);
} else {
return null; // invalid
}
}
private getCustomEvent(websocketMessage: string): string {
if (websocketMessage.length < prefixAndSepIdx) {
return "";
}
let s = websocketMessage.substring(prefixAndSepIdx, websocketMessage.length);
let evt = s.substring(0, s.indexOf(separator));
return evt;
}
private getCustomMessage(event: string, websocketMessage: string): string {
let eventIdx = websocketMessage.indexOf(event + separator);
let s = websocketMessage.substring(eventIdx + event.length + separator.length+2, websocketMessage.length);
return s;
}
//
// Ws Events
// messageReceivedFromConn this is the func which decides
// if it's a native websocket message or a custom iris-ws message
// if native message then calls the fireNativeMessage
// else calls the fireMessage
//
// remember Iris gives you the freedom of native websocket messages if you don't want to use this client side at all.
private messageReceivedFromConn(evt: MessageEvent): void {
//check if iris-ws message
let message = <string>evt.data;
if (message.indexOf(prefix) != -1) {
let event = this.getCustomEvent(message);
if (event != "") {
// it's a custom message
this.fireMessage(event, this.getCustomMessage(event, message));
return;
}
}
// it's a native websocket message
this.fireNativeMessage(message);
}
OnConnect(fn: onConnectFunc): void {
if (this.isReady) {
fn();
}
this.connectListeners.push(fn);
}
fireConnect(): void {
for (let i = 0; i < this.connectListeners.length; i++) {
this.connectListeners[i]();
}
}
OnDisconnect(fn: onDisconnectFunc): void {
this.disconnectListeners.push(fn);
}
fireDisconnect(): void {
for (let i = 0; i < this.disconnectListeners.length; i++) {
this.disconnectListeners[i]();
}
}
OnMessage(cb: onNativeMessageFunc): void {
this.nativeMessageListeners.push(cb);
}
fireNativeMessage(websocketMessage: string): void {
for (let i = 0; i < this.nativeMessageListeners.length; i++) {
this.nativeMessageListeners[i](websocketMessage);
}
}
On(event: string, cb: onMessageFunc): void {
if (this.messageListeners[event] == null || this.messageListeners[event] == undefined) {
this.messageListeners[event] = [];
}
this.messageListeners[event].push(cb);
}
fireMessage(event: string, message: any): void {
for (let key in this.messageListeners) {
if (this.messageListeners.hasOwnProperty(key)) {
if (key == event) {
for (let i = 0; i < this.messageListeners[key].length; i++) {
this.messageListeners[key][i](message);
}
}
}
}
}
//
// Ws Actions
Disconnect(): void {
this.conn.close();
}
// EmitMessage sends a native websocket message
EmitMessage(websocketMessage: string): void {
this.conn.send(websocketMessage);
}
// Emit sends an iris-custom websocket message
Emit(event: string, data: any): void {
let messageStr = this.encodeMessage(event, data);
this.EmitMessage(messageStr);
}
//
}
// node-modules export {Ws};

View File

@@ -1,296 +0,0 @@
package websocket
import (
"time"
"bytes"
"strconv"
"github.com/iris-contrib/websocket"
"github.com/kataras/iris/utils"
)
type (
// DisconnectFunc is the callback which fires when a client/connection closed
DisconnectFunc func()
// ErrorFunc is the callback which fires when an error happens
ErrorFunc (func(string))
// NativeMessageFunc is the callback for native websocket messages, receives one []byte parameter which is the raw client's message
NativeMessageFunc func([]byte)
// MessageFunc is the second argument to the Emitter's Emit functions.
// A callback which should receives one parameter of type string, int, bool or any valid JSON/Go struct
MessageFunc interface{}
// Connection is the client
Connection interface {
// Emitter implements EmitMessage & Emit
Emitter
// ID returns the connection's identifier
ID() string
// OnDisconnect registers a callback which fires when this connection is closed by an error or manual
OnDisconnect(DisconnectFunc)
// OnError registers a callback which fires when this connection occurs an error
OnError(ErrorFunc)
// EmitError can be used to send a custom error message to the connection
//
// It does nothing more than firing the OnError listeners. It doesn't sends anything to the client.
EmitError(errorMessage string)
// To defines where server should send a message
// returns an emmiter to send messages
To(string) Emitter
// OnMessage registers a callback which fires when native websocket message received
OnMessage(NativeMessageFunc)
// On registers a callback to a particular event which fires when a message to this event received
On(string, MessageFunc)
// Join join a connection to a room, it doesn't check if connection is already there, so care
Join(string)
// Leave removes a connection from a room
Leave(string)
}
connection struct {
underline *websocket.Conn
id string
send chan []byte
onDisconnectListeners []DisconnectFunc
onErrorListeners []ErrorFunc
onNativeMessageListeners []NativeMessageFunc
onEventListeners map[string][]MessageFunc
// these were maden for performance only
self Emitter // pre-defined emmiter than sends message to its self client
broadcast Emitter // pre-defined emmiter that sends message to all except this
all Emitter // pre-defined emmiter which sends message to all clients
server *server
}
)
var _ Connection = &connection{}
// connection implementation
func newConnection(websocketConn *websocket.Conn, s *server) *connection {
c := &connection{
id: utils.RandomString(64),
underline: websocketConn,
send: make(chan []byte, 256),
onDisconnectListeners: make([]DisconnectFunc, 0),
onErrorListeners: make([]ErrorFunc, 0),
onNativeMessageListeners: make([]NativeMessageFunc, 0),
onEventListeners: make(map[string][]MessageFunc, 0),
server: s,
}
c.self = newEmitter(c, c.id)
c.broadcast = newEmitter(c, NotMe)
c.all = newEmitter(c, All)
return c
}
func (c *connection) write(messageType int, data []byte) error {
c.underline.SetWriteDeadline(time.Now().Add(c.server.config.WriteTimeout))
return c.underline.WriteMessage(messageType, data)
}
func (c *connection) writer() {
ticker := time.NewTicker(c.server.config.PingPeriod)
defer func() {
ticker.Stop()
c.underline.Close()
}()
for {
select {
case msg, ok := <-c.send:
if !ok {
defer func() {
// FIX FOR: https://github.com/kataras/iris/issues/175
// AS I TESTED ON TRIDENT ENGINE (INTERNET EXPLORER/SAFARI):
// NAVIGATE TO SITE, CLOSE THE TAB, NOTHING HAPPENS
// CLOSE THE WHOLE BROWSER, THEN THE c.conn is NOT NILL BUT ALL ITS FUNCTIONS PANICS, MEANS THAT IS THE STRUCT IS NOT NIL BUT THE WRITER/READER ARE NIL
// THE ONLY SOLUTION IS TO RECOVER HERE AT ANY PANIC
// THE FRAMETYPE = 8, c.closeSend = true
// NOTE THAT THE CLIENT IS NOT DISCONNECTED UNTIL THE WHOLE WINDOW BROWSER CLOSED, this is engine's bug.
//
if err := recover(); err != nil {
ticker.Stop()
c.server.free <- c
c.underline.Close()
}
}()
c.write(websocket.CloseMessage, []byte{})
return
}
c.underline.SetWriteDeadline(time.Now().Add(c.server.config.WriteTimeout))
res, err := c.underline.NextWriter(websocket.TextMessage)
if err != nil {
return
}
res.Write(msg)
n := len(c.send)
for i := 0; i < n; i++ {
res.Write(<-c.send)
}
if err := res.Close(); err != nil {
return
}
// if err := c.write(websocket.TextMessage, msg); err != nil {
// return
// }
case <-ticker.C:
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}
func (c *connection) reader() {
defer func() {
c.server.free <- c
c.underline.Close()
}()
conn := c.underline
conn.SetReadLimit(c.server.config.MaxMessageSize)
conn.SetReadDeadline(time.Now().Add(c.server.config.PongTimeout))
conn.SetPongHandler(func(s string) error {
conn.SetReadDeadline(time.Now().Add(c.server.config.PongTimeout))
return nil
})
for {
if _, data, err := conn.ReadMessage(); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
c.EmitError(err.Error())
}
break
} else {
c.messageReceived(data)
}
}
}
// messageReceived checks the incoming message and fire the nativeMessage listeners or the event listeners (iris-ws custom message)
func (c *connection) messageReceived(data []byte) {
if bytes.HasPrefix(data, prefixBytes) {
customData := string(data)
//it's a custom iris-ws message
receivedEvt := getCustomEvent(customData)
listeners := c.onEventListeners[receivedEvt]
if listeners == nil { // if not listeners for this event exit from here
return
}
customMessage, err := deserialize(receivedEvt, customData)
if customMessage == nil || err != nil {
return
}
for i := range listeners {
if fn, ok := listeners[i].(func()); ok { // its a simple func(){} callback
fn()
} else if fnString, ok := listeners[i].(func(string)); ok {
if msgString, is := customMessage.(string); is {
fnString(msgString)
} else if msgInt, is := customMessage.(int); is {
// here if server side waiting for string but client side sent an int, just convert this int to a string
fnString(strconv.Itoa(msgInt))
}
} else if fnInt, ok := listeners[i].(func(int)); ok {
fnInt(customMessage.(int))
} else if fnBool, ok := listeners[i].(func(bool)); ok {
fnBool(customMessage.(bool))
} else if fnBytes, ok := listeners[i].(func([]byte)); ok {
fnBytes(customMessage.([]byte))
} else {
listeners[i].(func(interface{}))(customMessage)
}
}
} else {
// it's native websocket message
for i := range c.onNativeMessageListeners {
c.onNativeMessageListeners[i](data)
}
}
}
func (c *connection) ID() string {
return c.id
}
func (c *connection) fireDisconnect() {
for i := range c.onDisconnectListeners {
c.onDisconnectListeners[i]()
}
}
func (c *connection) OnDisconnect(cb DisconnectFunc) {
c.onDisconnectListeners = append(c.onDisconnectListeners, cb)
}
func (c *connection) OnError(cb ErrorFunc) {
c.onErrorListeners = append(c.onErrorListeners, cb)
}
func (c *connection) EmitError(errorMessage string) {
for _, cb := range c.onErrorListeners {
cb(errorMessage)
}
}
func (c *connection) To(to string) Emitter {
if to == NotMe { // if send to all except me, then return the pre-defined emmiter, and so on
return c.broadcast
} else if to == All {
return c.all
} else if to == c.id {
return c.self
}
// is an emmiter to another client/connection
return newEmitter(c, to)
}
func (c *connection) EmitMessage(nativeMessage []byte) error {
return c.self.EmitMessage(nativeMessage)
}
func (c *connection) Emit(event string, message interface{}) error {
return c.self.Emit(event, message)
}
func (c *connection) OnMessage(cb NativeMessageFunc) {
c.onNativeMessageListeners = append(c.onNativeMessageListeners, cb)
}
func (c *connection) On(event string, cb MessageFunc) {
if c.onEventListeners[event] == nil {
c.onEventListeners[event] = make([]MessageFunc, 0)
}
c.onEventListeners[event] = append(c.onEventListeners[event], cb)
}
func (c *connection) Join(roomName string) {
payload := roomPayload{roomName, c.id}
c.server.join <- payload
}
func (c *connection) Leave(roomName string) {
payload := roomPayload{roomName, c.id}
c.server.leave <- payload
}
//

View File

@@ -1,50 +0,0 @@
package websocket
const (
// All is the string which the Emitter use to send a message to all
All = ""
// NotMe is the string which the Emitter use to send a message to all except this connection
NotMe = ";iris;to;all;except;me;"
// Broadcast is the string which the Emitter use to send a message to all except this connection, same as 'NotMe'
Broadcast = NotMe
)
type (
// Emitter is the message/or/event manager
Emitter interface {
// EmitMessage sends a native websocket message
EmitMessage([]byte) error
// Emit sends a message on a particular event
Emit(string, interface{}) error
}
emitter struct {
conn *connection
to string
}
)
var _ Emitter = &emitter{}
// emitter implementation
func newEmitter(c *connection, to string) *emitter {
return &emitter{conn: c, to: to}
}
func (e *emitter) EmitMessage(nativeMessage []byte) error {
mp := messagePayload{e.conn.id, e.to, nativeMessage}
e.conn.server.messages <- mp
return nil
}
func (e *emitter) Emit(event string, data interface{}) error {
message, err := serialize(event, data)
if err != nil {
return err
}
e.EmitMessage([]byte(message))
return nil
}
//

View File

@@ -1,145 +0,0 @@
package websocket
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/kataras/iris/utils"
)
/*
serializer, [de]serialize the messages from the client to the server and from the server to the client
*/
// The same values are exists on client side also
const (
stringMessageType messageType = iota
intMessageType
boolMessageType
bytesMessageType
jsonMessageType
)
const (
prefix = "iris-websocket-message:"
separator = ";"
prefixLen = len(prefix)
separatorLen = len(separator)
prefixAndSepIdx = prefixLen + separatorLen - 1
prefixIdx = prefixLen - 1
separatorIdx = separatorLen - 1
)
var (
separatorByte = separator[0]
buf = utils.NewBufferPool(256)
prefixBytes = []byte(prefix)
)
type (
messageType uint8
)
func (m messageType) String() string {
return strconv.Itoa(int(m))
}
func (m messageType) Name() string {
if m == stringMessageType {
return "string"
} else if m == intMessageType {
return "int"
} else if m == boolMessageType {
return "bool"
} else if m == bytesMessageType {
return "[]byte"
} else if m == jsonMessageType {
return "json"
}
return "Invalid(" + m.String() + ")"
}
// serialize serializes a custom websocket message from server to be delivered to the client
// returns the string form of the message
// Supported data types are: string, int, bool, bytes and JSON.
func serialize(event string, data interface{}) (string, error) {
var msgType messageType
var dataMessage string
if s, ok := data.(string); ok {
msgType = stringMessageType
dataMessage = s
} else if i, ok := data.(int); ok {
msgType = intMessageType
dataMessage = strconv.Itoa(i)
} else if b, ok := data.(bool); ok {
msgType = boolMessageType
dataMessage = strconv.FormatBool(b)
} else if by, ok := data.([]byte); ok {
msgType = bytesMessageType
dataMessage = string(by)
} else {
//we suppose is json
res, err := json.Marshal(data)
if err != nil {
return "", err
}
msgType = jsonMessageType
dataMessage = string(res)
}
b := buf.Get()
b.WriteString(prefix)
b.WriteString(event)
b.WriteString(separator)
b.WriteString(msgType.String())
b.WriteString(separator)
b.WriteString(dataMessage)
dataMessage = b.String()
buf.Put(b)
return dataMessage, nil
}
// deserialize deserializes a custom websocket message from the client
// ex: iris-websocket-message;chat;4;themarshaledstringfromajsonstruct will return 'hello' as string
// Supported data types are: string, int, bool, bytes and JSON.
func deserialize(event string, websocketMessage string) (message interface{}, err error) {
t, formaterr := strconv.Atoi(websocketMessage[prefixAndSepIdx+len(event)+1 : prefixAndSepIdx+len(event)+2]) // in order to iris-websocket-message;user;-> 4
if formaterr != nil {
return nil, formaterr
}
_type := messageType(t)
_message := websocketMessage[prefixAndSepIdx+len(event)+3:] // in order to iris-websocket-message;user;4; -> themarshaledstringfromajsonstruct
if _type == stringMessageType {
message = string(_message)
} else if _type == intMessageType {
message, err = strconv.Atoi(_message)
} else if _type == boolMessageType {
message, err = strconv.ParseBool(_message)
} else if _type == bytesMessageType {
message = []byte(_message)
} else if _type == jsonMessageType {
err = json.Unmarshal([]byte(_message), message)
} else {
return nil, fmt.Errorf("Type %s is invalid for message: %s", _type.Name(), websocketMessage)
}
return
}
// getCustomEvent return empty string when the websocketMessage is native message
func getCustomEvent(websocketMessage string) string {
if len(websocketMessage) < prefixAndSepIdx {
return ""
}
s := websocketMessage[prefixAndSepIdx:]
evt := s[:strings.IndexByte(s, separatorByte)]
return evt
}

View File

@@ -1,189 +0,0 @@
package websocket
import (
"sync"
"github.com/iris-contrib/websocket"
"github.com/kataras/iris/config"
"github.com/kataras/iris/context"
)
type (
// ConnectionFunc is the callback which fires when a client/connection is connected to the server.
// Receives one parameter which is the Connection
ConnectionFunc func(Connection)
// Rooms is just a map with key a string and value slice of string
Rooms map[string][]string
// Server is the websocket server
Server interface {
// Upgrade upgrades the client in order websocket works
Upgrade(context.IContext) error
// OnConnection registers a callback which fires when a connection/client is connected to the server
OnConnection(ConnectionFunc)
// Config returns a pointer to server's configs
Config() *config.Websocket
}
// roomPayload is used as payload from the connection to the server
roomPayload struct {
roomName string
connectionID string
}
// payloads, connection -> server
messagePayload struct {
from string
to string
data []byte
}
//
server struct {
config *config.Websocket
upgrader websocket.Upgrader
put chan *connection
free chan *connection
connections map[string]*connection
join chan roomPayload
leave chan roomPayload
rooms Rooms // by default a connection is joined to a room which has the connection id as its name
mu sync.Mutex // for rooms
messages chan messagePayload
onConnectionListeners []ConnectionFunc
//connectionPool *sync.Pool // sadly I can't make this because the websocket connection is live until is closed.
}
)
var _ Server = &server{}
// server implementation
// newServer creates a websocket server and returns it
func newServer(c *config.Websocket) *server {
s := &server{
config: c,
put: make(chan *connection),
free: make(chan *connection),
connections: make(map[string]*connection),
join: make(chan roomPayload, 1), // buffered because join can be called immediately on connection connected
leave: make(chan roomPayload),
rooms: make(Rooms),
messages: make(chan messagePayload, 1), // buffered because messages can be sent/received immediately on connection connected
onConnectionListeners: make([]ConnectionFunc, 0),
}
s.upgrader = websocket.Custom(s.handleConnection, s.config.ReadBufferSize, s.config.WriteBufferSize, false)
go s.serve() // start the server automatically
return s
}
func (s *server) Config() *config.Websocket {
return s.config
}
func (s *server) Upgrade(ctx context.IContext) error {
return s.upgrader.Upgrade(ctx)
}
func (s *server) handleConnection(websocketConn *websocket.Conn) {
c := newConnection(websocketConn, s)
s.put <- c
go c.writer()
c.reader()
}
func (s *server) OnConnection(cb ConnectionFunc) {
s.onConnectionListeners = append(s.onConnectionListeners, cb)
}
func (s *server) joinRoom(roomName string, connID string) {
s.mu.Lock()
if s.rooms[roomName] == nil {
s.rooms[roomName] = make([]string, 0)
}
s.rooms[roomName] = append(s.rooms[roomName], connID)
s.mu.Unlock()
}
func (s *server) leaveRoom(roomName string, connID string) {
s.mu.Lock()
if s.rooms[roomName] != nil {
for i := range s.rooms[roomName] {
if s.rooms[roomName][i] == connID {
s.rooms[roomName][i] = s.rooms[roomName][len(s.rooms[roomName])-1]
s.rooms[roomName] = s.rooms[roomName][:len(s.rooms[roomName])-1]
break
}
}
if len(s.rooms[roomName]) == 0 { // if room is empty then delete it
delete(s.rooms, roomName)
}
}
s.mu.Unlock()
}
func (s *server) serve() {
for {
select {
case c := <-s.put: // connection connected
s.connections[c.id] = c
// make and join a room with the connection's id
s.rooms[c.id] = make([]string, 0)
s.rooms[c.id] = []string{c.id}
for i := range s.onConnectionListeners {
s.onConnectionListeners[i](c)
}
case c := <-s.free: // connection closed
if _, found := s.connections[c.id]; found {
// leave from all rooms
for roomName := range s.rooms {
s.leaveRoom(roomName, c.id)
}
delete(s.connections, c.id)
close(c.send)
c.fireDisconnect()
}
case join := <-s.join:
s.joinRoom(join.roomName, join.connectionID)
case leave := <-s.leave:
s.leaveRoom(leave.roomName, leave.connectionID)
case msg := <-s.messages: // message received from the connection
if msg.to != All && msg.to != NotMe && s.rooms[msg.to] != nil {
// it suppose to send the message to a room
for _, connectionIDInsideRoom := range s.rooms[msg.to] {
if c, connected := s.connections[connectionIDInsideRoom]; connected {
c.send <- msg.data //here we send it without need to continue below
} else {
// the connection is not connected but it's inside the room, we remove it on disconnect but for ANY CASE:
s.leaveRoom(c.id, msg.to)
}
}
} else { // it suppose to send the message to all opened connections or to all except the sender
for connID, c := range s.connections {
if msg.to != All { // if it's not suppose to send to all connections (including itself)
if msg.to == NotMe && msg.from == connID { // if broadcast to other connections except this
continue //here we do the opossite of previous block, just skip this connection when it's suppose to send the message to all connections except the sender
}
}
select {
case s.connections[connID].send <- msg.data: //send the message back to the connection in order to send it to the client
default:
close(c.send)
delete(s.connections, connID)
c.fireDisconnect()
}
}
}
}
}
}
//

View File

@@ -1,288 +0,0 @@
package websocket
import (
"github.com/iris-contrib/logger"
"github.com/kataras/iris/config"
"github.com/kataras/iris/context"
)
// to avoid the import cycle to /kataras/iris. The ws package is used inside iris' station configuration
// inside Iris' configuration like kataras/iris/sessions, kataras/iris/render/rest, kataras/iris/render/template, kataras/iris/server and so on.
type irisStation interface {
H_(string, string, func(context.IContext)) func(string)
StaticContent(string, string, []byte) func(string)
}
//
// New returns a new running websocket server, registers this to the iris station
//
// Note that:
// This is not usable for you, unless you need more than one websocket server,
// because iris' station already has one which you can configure and start
//
// This is deprecated after rc-1, now we create the server and after register it
// because I want to be able to call the Websocket via a property and no via func before iris.Listen.
func New(station irisStation, c *config.Websocket, logger *logger.Logger) Server {
if c.Endpoint == "" {
//station.Logger().Panicf("Websockets - config's Endpoint is empty, you have to set it in order to enable and start the websocket server!!. Refer to the docs if you can't figure out.")
return nil
}
server := newServer(c)
RegisterServer(station, server, logger)
return server
}
// NewServer creates a websocket server and returns it
func NewServer(c *config.Websocket) Server {
return newServer(c)
}
// RegisterServer registers the handlers for the websocket server
// it's a bridge between station and websocket server
func RegisterServer(station irisStation, server Server, logger *logger.Logger) {
c := server.Config()
if c.Endpoint == "" {
return
}
websocketHandler := func(ctx context.IContext) {
if err := server.Upgrade(ctx); err != nil {
logger.Panic(err)
}
}
if c.Headers != nil && len(c.Headers) > 0 { // only for performance matter just re-create the websocketHandler if we have headers to set
websocketHandler = func(ctx context.IContext) {
for k, v := range c.Headers {
ctx.SetHeader(k, v)
}
if err := server.Upgrade(ctx); err != nil {
logger.Panic(err)
}
}
}
station.H_("GET", c.Endpoint, websocketHandler)
// serve the client side on domain:port/iris-ws.js
station.StaticContent("/iris-ws.js", "application/json", clientSource)
}
var clientSource = []byte(`var stringMessageType = 0;
var intMessageType = 1;
var boolMessageType = 2;
// bytes is missing here for reasons I will explain somewhen
var jsonMessageType = 4;
var prefix = "iris-websocket-message:";
var separator = ";";
var prefixLen = prefix.length;
var separatorLen = separator.length;
var prefixAndSepIdx = prefixLen + separatorLen - 1;
var prefixIdx = prefixLen - 1;
var separatorIdx = separatorLen - 1;
var Ws = (function () {
//
function Ws(endpoint, protocols) {
var _this = this;
// events listeners
this.connectListeners = [];
this.disconnectListeners = [];
this.nativeMessageListeners = [];
this.messageListeners = {};
if (!window["WebSocket"]) {
return;
}
if (endpoint.indexOf("ws") == -1) {
endpoint = "ws://" + endpoint;
}
if (protocols != null && protocols.length > 0) {
this.conn = new WebSocket(endpoint, protocols);
}
else {
this.conn = new WebSocket(endpoint);
}
this.conn.onopen = (function (evt) {
_this.fireConnect();
_this.isReady = true;
return null;
});
this.conn.onclose = (function (evt) {
_this.fireDisconnect();
return null;
});
this.conn.onmessage = (function (evt) {
_this.messageReceivedFromConn(evt);
});
}
//utils
Ws.prototype.isNumber = function (obj) {
return !isNaN(obj - 0) && obj !== null && obj !== "" && obj !== false;
};
Ws.prototype.isString = function (obj) {
return Object.prototype.toString.call(obj) == "[object String]";
};
Ws.prototype.isBoolean = function (obj) {
return typeof obj === 'boolean' ||
(typeof obj === 'object' && typeof obj.valueOf() === 'boolean');
};
Ws.prototype.isJSON = function (obj) {
try {
JSON.parse(obj);
}
catch (e) {
return false;
}
return true;
};
//
// messages
Ws.prototype._msg = function (event, messageType, dataMessage) {
return prefix + event + separator + String(messageType) + separator + dataMessage;
};
Ws.prototype.encodeMessage = function (event, data) {
var m = "";
var t = 0;
if (this.isNumber(data)) {
t = intMessageType;
m = data.toString();
}
else if (this.isBoolean(data)) {
t = boolMessageType;
m = data.toString();
}
else if (this.isString(data)) {
t = stringMessageType;
m = data.toString();
}
else if (this.isJSON(data)) {
//propably json-object
t = jsonMessageType;
m = JSON.stringify(data);
}
else {
console.log("Invalid");
}
return this._msg(event, t, m);
};
Ws.prototype.decodeMessage = function (event, websocketMessage) {
//iris-websocket-message;user;4;themarshaledstringfromajsonstruct
var skipLen = prefixLen + separatorLen + event.length + 2;
if (websocketMessage.length < skipLen + 1) {
return null;
}
var messageType = parseInt(websocketMessage.charAt(skipLen - 2));
var theMessage = websocketMessage.substring(skipLen, websocketMessage.length);
if (messageType == intMessageType) {
return parseInt(theMessage);
}
else if (messageType == boolMessageType) {
return Boolean(theMessage);
}
else if (messageType == stringMessageType) {
return theMessage;
}
else if (messageType == jsonMessageType) {
return JSON.parse(theMessage);
}
else {
return null; // invalid
}
};
Ws.prototype.getCustomEvent = function (websocketMessage) {
if (websocketMessage.length < prefixAndSepIdx) {
return "";
}
var s = websocketMessage.substring(prefixAndSepIdx, websocketMessage.length);
var evt = s.substring(0, s.indexOf(separator));
return evt;
};
Ws.prototype.getCustomMessage = function (event, websocketMessage) {
var eventIdx = websocketMessage.indexOf(event + separator);
var s = websocketMessage.substring(eventIdx + event.length + separator.length + 2, websocketMessage.length);
return s;
};
//
// Ws Events
// messageReceivedFromConn this is the func which decides
// if it's a native websocket message or a custom iris-ws message
// if native message then calls the fireNativeMessage
// else calls the fireMessage
//
// remember Iris gives you the freedom of native websocket messages if you don't want to use this client side at all.
Ws.prototype.messageReceivedFromConn = function (evt) {
//check if iris-ws message
var message = evt.data;
if (message.indexOf(prefix) != -1) {
var event_1 = this.getCustomEvent(message);
if (event_1 != "") {
// it's a custom message
this.fireMessage(event_1, this.getCustomMessage(event_1, message));
return;
}
}
// it's a native websocket message
this.fireNativeMessage(message);
};
Ws.prototype.OnConnect = function (fn) {
if (this.isReady) {
fn();
}
this.connectListeners.push(fn);
};
Ws.prototype.fireConnect = function () {
for (var i = 0; i < this.connectListeners.length; i++) {
this.connectListeners[i]();
}
};
Ws.prototype.OnDisconnect = function (fn) {
this.disconnectListeners.push(fn);
};
Ws.prototype.fireDisconnect = function () {
for (var i = 0; i < this.disconnectListeners.length; i++) {
this.disconnectListeners[i]();
}
};
Ws.prototype.OnMessage = function (cb) {
this.nativeMessageListeners.push(cb);
};
Ws.prototype.fireNativeMessage = function (websocketMessage) {
for (var i = 0; i < this.nativeMessageListeners.length; i++) {
this.nativeMessageListeners[i](websocketMessage);
}
};
Ws.prototype.On = function (event, cb) {
if (this.messageListeners[event] == null || this.messageListeners[event] == undefined) {
this.messageListeners[event] = [];
}
this.messageListeners[event].push(cb);
};
Ws.prototype.fireMessage = function (event, message) {
for (var key in this.messageListeners) {
if (this.messageListeners.hasOwnProperty(key)) {
if (key == event) {
for (var i = 0; i < this.messageListeners[key].length; i++) {
this.messageListeners[key][i](message);
}
}
}
}
};
//
// Ws Actions
Ws.prototype.Disconnect = function () {
this.conn.close();
};
// EmitMessage sends a native websocket message
Ws.prototype.EmitMessage = function (websocketMessage) {
this.conn.send(websocketMessage);
};
// Emit sends an iris-custom websocket message
Ws.prototype.Emit = function (event, data) {
var messageStr = this.encodeMessage(event, data);
this.EmitMessage(messageStr);
};
return Ws;
}());
`)