Skip to content

Commit

Permalink
3.0.0 - Swappable backends (#65)
Browse files Browse the repository at this point in the history
* Swappable backends

* Update readme

* Clean up

* fix block
  • Loading branch information
rawleyfowler authored Nov 8, 2023
1 parent d2b05d6 commit 19f6c44
Show file tree
Hide file tree
Showing 20 changed files with 534 additions and 578 deletions.
8 changes: 5 additions & 3 deletions META6.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
"raku": "6.d",
"provides": {
"Humming-Bird::Core": "lib/Humming-Bird/Core.rakumod",
"Humming-Bird::HTTPServer": "lib/Humming-Bird/HTTPServer.rakumod",
"Humming-Bird::Backend": "lib/Humming-Bird/Backend.rakumod",
"Humming-Bird::Backend::HTTPServer": "lib/Humming-Bird/Backend/HTTPServer.rakumod",
"Humming-Bird::Middleware": "lib/Humming-Bird/Middleware.rakumod",
"Humming-Bird::Advice": "lib/Humming-Bird/Advice.rakumod"
"Humming-Bird::Advice": "lib/Humming-Bird/Advice.rakumod",
"Humming-Bird::Glue": "lib/Humming-Bird/Glue.rakumod"
},
"resources": [
],
Expand All @@ -42,5 +44,5 @@
"Test::Util::ServerPort",
"Cro::HTTP::Client"
],
"version": "2.2.0"
"version": "3.0.0"
}
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ Humming-Bird was inspired mainly by [Sinatra](https://sinatrarb.com), and [Expre
things minimal, allowing the user to pull in things like templating engines, and ORM's on their own terms.

## Features
Humming-Bird has 2 simple layers, at its core we have `Humming-Bird::HTTPServer` which handles all of the low-level HTTP bits. Then you have the
routing stack that handles: routing, middleware, error handling, cookies, etc.
Humming-Bird has 2 simple layers, at the lowest levels we have `Humming-Bird::Glue` which is a simple "glue-like" layer for interfacing with
`Humming-Bird::Backend`'s.
Then you have the actual application logic in `Humming-Bird::Core` that handles: routing, middleware, error handling, cookies, etc.

- Powerful function composition based routing and application logic
- Routers
Expand All @@ -28,6 +29,8 @@ routing stack that handles: routing, middleware, error handling, cookies, etc.
- Static files served have their content type infered
- Request/Response stash's for inter-layer route talking

- Swappable backends

**Note**: Humming-Bird is not meant to face the internet directly. Please use a reverse proxy such as httpd or NGiNX.

## How to install
Expand All @@ -44,9 +47,9 @@ zef install Humming-Bird
```

## Performance
Around ~20% faster than Ruby's `Sinatra`, and only improving as time goes on!

See [this](https://github.com/rawleyfowler/Humming-Bird/issues/43#issuecomment-1454252501) for a more detailed performance preview.
See [this](https://github.com/rawleyfowler/Humming-Bird/issues/43#issuecomment-1454252501) for a more detailed performance preview
vs. Ruby's Sinatra using `Humming-Bird::Backend::HTTPServer`.

## Examples

Expand Down Expand Up @@ -151,6 +154,20 @@ get('/no-firefox', -> $request, $response {
}, [ &middleware-logger, &block-firefox ]);
```

#### Swappable Backends
```raku
use v6.d;

use Humming-Bird::Core;

get('/, -> $request, $response {
$response.html('This request has been logged!');
});
# Run on a different backend, assuming:
listen(:backend(Humming-Bird::Backend::MyBackend));
```
More examples can be found in the [examples](https://github.com/rawleyfowler/Humming-Bird/tree/main/examples) directory.
## Design
Expand Down
10 changes: 10 additions & 0 deletions lib/Humming-Bird/Backend.rakumod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use v6.d;

unit role Humming-Bird::Backend;

has Int:D $.port = 8080;
has Int:D $.timeout is required;

method listen(&handler) {
die "{ self.^name } does not properly implement Humming-Bird::Backend.";
}
145 changes: 145 additions & 0 deletions lib/Humming-Bird/Backend/HTTPServer.rakumod
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use v6;

# This code is based on the excellent code by the Raku community, adapted to work with Humming-Bird.
# https://github.com/raku-community-modules/HTTP-Server-Async

# A simple, single-threaded asynchronous HTTP Server.

use Humming-Bird::Backend;
use Humming-Bird::Glue;

unit class Humming-Bird::Backend::HTTPServer does Humming-Bird::Backend;

my constant $DEFAULT-RN = "\r\n\r\n".encode.Buf;
my constant $RN = "\r\n".encode.Buf;

has Channel:D $.requests .= new;
has Lock $!lock .= new;
has @!connections;

method !timeout {
start {
react {
whenever Supply.interval(1) {
CATCH { default { warn $_ } }
$!lock.protect({
@!connections = @!connections.grep({ !$_<closed>.defined }); # Remove dead connections
for @!connections.grep({ now - $_<last-active> >= $!timeout }) {
{
$_<closed> = True;
$_<socket>.write(Blob.new);
$_<socket>.close;

CATCH { default { warn $_ } }
}
}
});
}
}
}
}

method !respond(&handler) {
start {
react {
whenever $.requests -> $request {
CATCH { default { .say } }
my $hb-request = Humming-Bird::Glue::Request.decode($request<data>.decode);
my Humming-Bird::Glue::Response $response = &handler($hb-request);
$request<connection><socket>.write: $response.encode;
$request<connection><closed> = True with $hb-request.header('keep-alive');
}
}
}
}

method !handle-request($data is rw, $index is rw, $connection) {
my $request = {
:$connection,
data => Buf.new
};

my @header-lines = Buf.new($data[0..$index]).decode.lines.tail(*-1).grep({ .chars });
return unless @header-lines.elems;

$request<data> ~= $data.subbuf(0, $index);

my $content-length = $data.elems - $index;
for @header-lines -> $header {
my ($key, $value) = $header.split(': ', 2, :skip-empty);
given $key.lc {
when 'content-length' {
$content-length = +$value // ($data.elems - $index);
}
when 'transfer-encoding' {
if $value.chomp.lc.index('chunked') !~~ Nil {
my Int $i;
my Int $b;
while $i < $data.elems {
$i++ while $data[$i] != $RN[0]
&& $data[$i+1] != $RN[1]
&& $i + 1 < $data.elems;

last if $i + 1 >= $data.elems;

$b = :16($data[0..$i].decode);
last if $data.elems < $i + $b;
if $b == 0 {
try $data .= subbuf(3);
last;
}

$i += 2;
$request<data> ~= $data.subbuf($i, $i+$b-3);
try $data .= subbuf($i+$b+2);
$i = 0;
}
}
}
}
}

$request<data> ~= $data.subbuf($index, $content-length+4);
$.requests.send: $request;
}

method listen(&handler) {
react {
self!timeout;
self!respond(&handler);

whenever IO::Socket::Async.listen('0.0.0.0', $.port) -> $connection {
my %connection-map := {
socket => $connection,
last-active => now
}

$!lock.protect({ @!connections.push: %connection-map });

whenever $connection.Supply: :bin -> $bytes {
my Buf $data .= new;
my Int:D $idx = 0;
my $req;

CATCH { default { .say } }
$data ~= $bytes;
%connection-map<last-active> = now;
while $idx++ < $data.elems - 4 {
# Read up to headers
$idx--, last if $data[$idx] == $DEFAULT-RN[0]
&& $data[$idx+1] == $DEFAULT-RN[1]
&& $data[$idx+2] == $DEFAULT-RN[2]
&& $data[$idx+3] == $DEFAULT-RN[3];
}

$idx += 4;

self!handle-request($data, $idx, %connection-map);
}

CATCH { default { .say; $connection.close; %connection-map<closed> = True } }
}
}
}

# vim: expandtab shiftwidth=4
Loading

0 comments on commit 19f6c44

Please sign in to comment.