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 )