Erlang in Web

with 

       N2O

facebook.com/synrc

N2O SHEN REST

FS MAD SH OTP.MK

KVS BPE DBS MQS 

synrc.com

HTTP

REST

WebSocket

Event Streaming

 SERVICE or DB API

Static, Upload and Auth

HTTP

Static Serving

Multipart File Upload

URI parsing

[{"/static/[...]", n2o_dynalo,
     {dir, "apps/review/priv/static", mime()}},
 {"/n2o/[...]", n2o_dynalo,
     {dir, "deps/n2o/priv", mime()}}]

REST

Mapping Erlang Records

Database and API Access

JSON formatting

[{"/rest/:resource", rest_cowboy, []},
 {"/rest/:resource/:id", rest_cowboy, []}]

WebSocket

Event Streaming

JavaScript/HTML Updates

Binary Formatting

[{"/ws/[...]", bullet_handler, 
               [{handler, n2o_bullet}]}]

mochiweb   yaws

webmachine   inets

misultin   cowboy

Erlang Web Servers History

n2o   nitrogen  

chicagoboss   yaws

zotonic

Erlang Web Frameworks History

Angular  Meteor  Ember

Knockout  React  D3

jQuery  Backbone

JavaScript SPA Frameworks

PHP ASP JSP

RoR Yesod WebSharper

Lift Nitrogen Ocsigen

Server Applications

XML

JSON

HTML/JS

 

Client/Server Interchange Format

Raw Binary

BERT

MessagePack

REST/XML+JSON

 

WebSocket/BERT

Underlying Channels: REST vs RPC

XHR

ws.send

   Library         Type    Enc    Dec
   -------------   ------  -----  -----
   jsonx           C NIF   1048   998
   jiffy           C NIF   2652   2234
   jsone
   yaws2           Erlang  14208  12653 
   jsonerl         Erlang  14549  14899
   mochijson2      Erlang  16787  16512
   jsx             Erlang  27188  18333

JSON

REST

POST kvs:add(#user{}). ​
​DELETE kvs:remove(user,2).
PUT kvs:put(#user{id=2}).
GET kvs:entries(Feed,user).
GET kvs:get(user,2).
GET kvs:all(user).

REST

-compile({parse_transform, n2o_rest}).
-include("users.hrl").
-rest_record(user).

init() -> ets:new(users,[public,named_table,{keypos,#user.id}]).
populate(Users) -> ets:insert(users, Users).
exists(Id) -> ets:member(users, wf:to_list(Id)).
get() -> ets:tab2list(users).
get(Id) -> [User] = ets:lookup(users, wf:to_list(Id)), User. 
delete(Id) -> ets:delete(users, wf:to_list(Id)).
post(#user{} = User) -> ets:insert(users, User);
post(Data) -> post(from_json(Data, #user{})).
         Term term_to_binary(Term)
       ------ -----------------------
           10 <<131,97,10>>
        10000 <<131,98,0,0,39,16>>
      "maxim" <<131,107,0,5,109,97,120,105,109>>
<<"ма"/utf8>> <<131,109,0,0,0,4,208,188,208,176>>
           [] <<131,106>>
           {} <<131,104,0>>
           ok <<131,100,0,2,111,107>>
 fun()->[]end <<131,112,0,0,2,190,0,203,14,...>>

BERT

Java, JavaScript, ObjectiveC, C, C#, C++, Go

SVG

http://synrc.com:8080/static/app/index.htm

Send

Country

Client

Name

Address

FORMS

V

.column33,.column67,.column6,.column,.column2,.column3,
.column14,.column12,.column10,.column20,.column0 { float: left; }

.column14 { width: 14%;}
.column12 { width: 12%;}
.column10 { width: 10%;}
.column20 { width: 20%;}
.column33 { width: 33%;}
.column67 { width: 67%;}
.column6  { width: 16.66%;}
.column3  { width: 33.33%; }
.column2  { width: 50%; }
.column   { width: 100%; }

CSS 7KB

No jQuery

No Bootstrap

Semantic Small

wf:update(history,   
 [ #span{body="Hello"} ]).

document
  .querySelector('#history')
  .outerHTML =  
       '<span>Hello</span>';

DSL

> utf8_decode(msg);
> utf8_toByteArray(value);


> querySource('phone');

UTF-8

Pipeline

init_context(Req) -> 
    #context{ actions=[], module=index, path=[],
              req=Req, params=[], session=undefined, 
              handlers = [ {'query', wf:config('query', n2o_query)}, 
                           { session, wf:config(session, n2o_session)}.
                           { route, wf:config(route, n2o_route)} ]}.
fold(Fun,Handlers,Ctx) ->    
    lists:foldl(fun({_,Module},Ctx1) ->
        {ok,_,NewCtx} = Module:Fun([],Ctx1),  
    NewCtx end,Ctx,Handlers).
event(init) -> 
  wf:info(?MODULE, 
      "WSPid: ~p",[self()]);

<<"PING">>

> ws.send('PING');
> ws.send('N2O,');

{pickle,_,_,_,_}

ws.send(enc(tuple(atom('pickle'), binary('take'),
   binary('g2gCaAVkAAJldmQABWluZGV4ZAAEdGFrZWsABH'+
          'Rha2VkAAVldmVudGgDYgAABXViAAQKXmIAC3cK'),  
         [ tuple(atom('take'),'0') ] )));

{client,_}

ws.send(enc(tuple(atom('client'), 
                  tuple(atom('chat'),
                  querySource('message')))));

{server,_}

ROSTER

users, online, chat, offline, 

roster_item, roster_group, 

roster_end, message, event

User = #react{
        render = fun(This) ->
         #h1{body=value(email,This)} end
        },

CommentList = #react{
        props = [{data,[]}],
        render = fun(This) -> 
            Users = lists:map(fun(Item) ->                        
                    User#react{props=Item} end,
                    value(data,This)),
            Users
        end },

Erlang

var user = React.createClass({
        render: function() {
                return React.DOM.h1(null,value('email',this));
       }});

var commentlist = React.createClass({props: {data:[]},
       render: function() {
           var users = value('data',this).map(function(item) {
                   return user({props: item});
           });
                return users;
       }});

JavaScript

-module(page).
-compile(export_all).

body() -> 
    [ #span{ text="Login: " }, #textbox{ id=user }, 
      #span{ text="Pass: " }, #password{ id=pass }, 
      #button{ text="Login", postback=login,
                             source=[user,pass]} ].
main() -> [ #dtl{ file="login", bindings=[{body,body()}]} ].
event(login) -> wf:user(wf:q(user)), wf:redirect().​

Nitrogen

-module(chat).
event (chat) -> wf:send(chat, wf:q(message)),

body() -> 
    wf:async ( fun() -> loop() end, chat ),
   [ #panel { id=history }, #textbox { id=message }, 
                 #button { postback=chat } ].

loop() -> 
    receive Message -> 
         wf:insert_bottom ( history, #span { text=Message } ),
          wf:flush () end, loop ().

Chat

Made with Slides.com