Angelo
Sinatra-like DSL for Reel that supports WebSockets and SSE
Install / Use
/learn @kenichi/AngeloREADME
Angelo
tl;dr
- websocket support via
websocket('/path'){|s| ... }route builder - SSE support via
eventsource('/path'){|s| ... }route builder - contextual websocket/sse stashing via
websocketsandsseshelpers taskhandling viaasyncandfuturehelpers- 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 thatrespond_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 achunked_responsehelper 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
openhue
333.3kControl Philips Hue lights and scenes via the OpenHue CLI.
sag
333.3kElevenLabs text-to-speech with mac-style say UX.
weather
333.3kGet current weather and forecasts via wttr.in or Open-Meteo
tweakcc
1.4kCustomize Claude Code's system prompts, create custom toolsets, input pattern highlighters, themes/thinking verbs/spinners, customize input box & user message styling, support AGENTS.md, unlock private/unreleased features, and much more. Supports both native/npm installs on all platforms.

