SkillAgentSearch skills...

Angelo

Sinatra-like DSL for Reel that supports WebSockets and SSE

Install / Use

/learn @kenichi/Angelo
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Angelo

Build Status

A Sinatra-like DSL for Reel.

tl;dr

  • websocket support via websocket('/path'){|s| ... } route builder
  • SSE support via eventsource('/path'){|s| ... } route builder
  • contextual websocket/sse stashing via websockets and sses helpers
  • task handling via async and future helpers
  • no rack
  • erb, haml, and markdown support
  • mustermann support

What is Angelo?

Just like Sinatra, Angelo gives you an expressive DSL for creating web applications. There are some notable differences, but the basics remain the same: you can either create a "classic" application by requiring 'angelo/main' and using the DSL at the top level of your script, or a "modular" application by requiring 'angelo', subclassing Angelo::Base, and calling .run! on that class for the service to start. In addition, and perhaps more importantly, Angelo is built on Reel, which is built on Celluloid::IO and gives you a reactor with evented IO in Ruby!

Things will feel very familiar to anyone experienced with Sinatra. You can define route handlers denoted by HTTP verb and path with parameters set from path matching (using Mustermann), the query string, and post body. A route block may return:

  • The body of the response in full as a String.
  • A Hash (or anything that respond_to? :to_json) if the content type is set to :json.
  • Any object that responds to #each(&block) if the transfer encoding is set to :chunked. There is also a chunked_response helper that will take a block, set the transfer encoding, and return an appropriate object.

Angelo also features before and after filter blocks, just like Sinatra. Filters are ordered as defined, and called in that order. When defined without a path, they run for all matched requests. With a path, the path is interpreted as a Mustermann pattern and params are merged. before filters can set instance variables which can be used in the route block and the after filter. For more info on the difference in how after blocks are handled, see the Errors section below for more info.

Websockets!

One of the main motivations for Angelo was the ability to define websocket handlers with ease. Through the addition of a websocket route builder and a websockets helper, Angelo attempts to make it easy for you to build real-time web applications.

Route Builder

The websocket route builder accepts a path and a block, and passes the actual websocket to the block as the only argument. This socket is an instance of Reel's WebSocket class, and, as such, responds to methods like on_message and on_close. A service-wide on_pong handler may be defined to customize the behavior when a pong frame comes back from a connected websocket client.

websockets helper

Angelo includes a "stash" helper for connected websockets. One can << a websocket into websockets from inside a websocket handler block. These can "later" be iterated over so one can do things like emit a message on every connected websocket when the service receives a POST request.

The websockets helper also includes a context ability, so you can stash connected websocket clients into different "sections". Also, by default, the helper will reject! any closed sockets before returning; you may optionally pass false to the helper to skip this step.

Example!

Here is an example of the websocket route builder, the websockets helper, and the context feature:

require 'angelo'

class Foo < Angelo::Base

  websocket '/' do |ws|
    websockets << ws
  end

  websocket '/bar' do |ws|
    websockets[:bar] << ws
  end

  post '/' do
    websockets.each {|ws| ws.write params[:foo]}
  end

  post '/bar' do
    websockets[:bar].each {|ws| ws.write params[:bar]}
  end

end

Foo.run!

In this case, any clients that connect to a websocket at the path '/' will be stashed in the default websockets array; clients that connect to '/bar' will be stashed in the :bar section.

Each "section" is accessed with a familiar, Hash-like syntax, and can be iterated over with a .each block.

When a POST / with a 'foo' param is received, any value is messaged out to all '/' connected websockets. When a POST /bar with a 'bar' param is received, any value is messaged out to all websockets connected to '/bar'.

SSE - Server-Sent Events

The eventsource route builder also accepts a path and a block, and passes the socket to the block, just like the websocket builder. This socket is actually the raw Celluloid::IO::TCPSocket and is "detached" from the regular handling. There are no "on-*" methods; the write method should suffice. To make it easier to deal with creation of the properly formatted Strings to send, Angelo provides a couple helpers.

sse_event helper

To create an "event" that a javascript EventListener on the client can respond to:

eventsource '/sse' do |s|
  event = sse_event :foo, some_key: 'blah', other_key: 'boo'
  s.write event
  s.close
end

In this case, the EventListener would have to be configured to listen for the foo event:

var sse = new EventSource('/sse');
sse.addEventListener('foo', function(e){ console.log("got foo event!\n" + JSON.parse(e.data)); });

The sse_event helper accepts a normal String for the data, but will automatically convert a Hash argument to a JSON object.

NOTE: there is a shortcut helper on the actual SSE object itself:

eventsource '/sse' do |sse|
  sse.event :foo, some_key: 'blah', other_key: 'boo'
  sse.event :close
end
sse_message helper

The sse_message helper behaves exactly the same as sse_event, but does not take an event name:

eventsource '/sse' do |s|
  msg = sse_message some_key: 'blah', other_key: 'boo'
  s.write msg
  s.close
end

The client javascript would need to be altered to use the EventSource.onmessage property as well:

var sse = new EventSource('/sse');
sse.onmessage = function(e){ console.log("got message!\n" + JSON.parse(e.data)); };

NOTE: there is a shortcut helper on the actual SSE object itself:

eventsource '/sse' do |sse|
  sse.message some_key: 'blah', other_key: 'boo'
  sse.event :close
end
sses helper

Angelo also includes a "stash" helper for SSE connections. One can << a socket into sses from inside an eventsource handler block. These can "later" be iterated over so one can do things like emit a message on every SSE connection when the service receives a POST request.

The sses helper includes the same context ability as the websockets helper. Also, by default, the helper will reject! any closed sockets before returning, just like websockets. You may optionally pass false to the helper to skip this step. In addition, the sses stash includes methods for easily sending events or messages to all stashed connections. Note that the Stash::SSE#event method only works on non-default contexts and uses the context name as the event name.

eventsource '/sse' do |s|
  sses[:foo] << s
end

post '/sse_message' do
  sses[:foo].message params[:data]
end

post '/sse_event' do
  sses[:foo].event params[:data]
end
eventsource instance helper

Additionally, you can also start SSE handling conditionally from inside a GET block:

get '/sse_maybe' do
  if params[:sse]
    eventsource do |c|
      sses << c
      c.write sse_message 'wooo fancy SSE for you!'
    end
  else
    'boring regular old get response'
  end
end

post '/sse_event' do
  sses.each {|sse| sse.write sse_event(:foo, 'fancy sse event!')}
end

Handling this on the client may require conditionals for browsers that do not support EventSource yet, since this will respond with a non-"text/event-stream" Content-Type if 'sse' is not present in the params.

EventSource#on_close helper

When inside an eventsource block, you may want to do something specific when a client closes the connection. For this case, there are on_close and on_close= methods on the object passed to the block that will get called if the client closes the socket. The assignment method takes a proc object and the other one takes a block:

get '/' do
  eventsource do |es|

    # assignment!
    es.on_close = ->{sses(false).remove_socket es}

    sses << es
  end
end

eventsource '/sse' do |es|

  # just passing a block here
  es.on_close {sses(false).remove_socket es}

  sses << es
end

Note the use of the optional parameter of the stashes here; by default, stash accessors (websockets and sses) will reject! any closed sockets before letting you in. If you pass false to the stash accessors, they will skip the reject! step.

Tasks + Async / Future

Angelo is built on Reel and Celluloid::IO, giving your web application the ability to define "tasks" and call them from route handler blocks in an async or future style.

task builder

You can define a task on the reactor using the task class method and giving it a symbol and a block. The block can take arguments that you can pass later, with async or future.

# defining a task on the reactor called `:in_sec` which will sleep for
# the given number of seconds, then return the given message.
#
task :in_sec do |sec, msg|
  sleep sec.to_i
  msg
end
async helper

This helper is directly analogous to the Celluoid method of the same name. Once tasks are defined, you can call them with this helper method, passing the symbol of the task name and any arguments. The task will run on the reactor, asynchronously, and return immediately.

get '/' do
  # run the task defined above asyn

Related Skills

View on GitHub
GitHub Stars301
CategoryCustomer
Updated1mo ago
Forks22

Languages

Ruby

Security Score

85/100

Audited on Jan 26, 2026

No findings