Feersum

 view release on metacpan or  search on metacpan

eg/sse-server.pl  view on Meta::CPAN

            const timeSpan = document.createElement('span');
            timeSpan.className = 'event-time';
            timeSpan.textContent = '[' + time + ']';
            const typeB = document.createElement('b');
            typeB.textContent = type;
            const dataSpan = document.createElement('span');
            dataSpan.className = 'event-data';
            dataSpan.textContent = data;
            div.appendChild(timeSpan);
            div.appendChild(document.createTextNode(' '));
            div.appendChild(typeB);
            div.appendChild(document.createTextNode(' (id:' + id + '): '));
            div.appendChild(dataSpan);
            events.insertBefore(div, events.firstChild);
            // Keep only last 50 events
            while (events.children.length > 50) {
                events.removeChild(events.lastChild);
            }
        }

        // Auto-connect on load
        connect();
    </script>
</body>
</html>
HTML

# Create listen socket
my $sock = IO::Socket::INET->new(
    LocalAddr => "0.0.0.0:$PORT",
    ReuseAddr => 1,
    Proto     => 'tcp',
    Listen    => 1024,
    Blocking  => 0,
) or die "Cannot create socket: $!";

my $feersum = Feersum->endjinn;
$feersum->use_socket($sock);

my $scheme = 'http';
if ($tls_cert && $tls_key) {
    $feersum->set_tls(cert_file => $tls_cert, key_file => $tls_key, h2 => 1);
    $scheme = 'https';
}

print "SSE server listening on $scheme://localhost:$PORT/\n";

# Request handler
$feersum->request_handler(sub {
    my $req = shift;
    my $path = $req->env->{PATH_INFO};

    if ($path eq '/' || $path eq '/index.html') {
        # Serve HTML page
        $req->send_response(200, [
            'Content-Type' => 'text/html; charset=utf-8',
            'Cache-Control' => 'no-cache',
        ], \$html_page);
    }
    elsif ($path eq '/events') {
        # SSE endpoint
        start_sse_stream($req);
    }
    else {
        $req->send_response(404, ['Content-Type' => 'text/plain'], \"Not Found\n");
    }
});

sub start_sse_stream {
    my $req = shift;
    my $id = ++$client_id;

    # Start streaming with SSE headers
    my $w = $req->start_streaming(200, [
        'Content-Type'  => 'text/event-stream',
        'Cache-Control' => 'no-cache',
        'Connection'    => 'keep-alive',
        'X-Accel-Buffering' => 'no',  # Disable nginx buffering
    ]);

    # Store client info
    my $client = {
        id     => $id,
        writer => $w,
    };
    push @clients, $client;

    print "Client $id connected (total: " . scalar(@clients) . ")\n";

    # Send initial connection event
    send_sse_event($w, 'message', "Connected as client $id", ++$event_id);
}

sub send_sse_event {
    my ($writer, $event, $data, $id) = @_;
    return unless $writer;

    # SSE format: id, event, data fields followed by blank line
    my $msg = "";
    $msg .= "id: $id\n" if defined $id;
    $msg .= "event: $event\n" if $event && $event ne 'message';
    $msg .= "data: $data\n";
    $msg .= "\n";  # End of event

    eval { $writer->write($msg) };
    return !$@;
}

sub broadcast {
    my ($event, $data) = @_;
    my $id = ++$event_id;

    # Clean up dead clients and broadcast
    @clients = grep {
        my $c = $_;
        if ($c->{writer}) {
            my $ok = send_sse_event($c->{writer}, $event, $data, $id);
            if (!$ok) {
                print "Client $c->{id} disconnected\n";
                0;
            } else {



( run in 1.123 second using v1.01-cache-2.11-cpan-16aabcbfbcb )