Hypersonic

 view release on metacpan or  search on metacpan

lib/Hypersonic.pm  view on Meta::CPAN

use Hypersonic::Protocol::HTTP1;
use Hypersonic::JIT::Util;

# Cache deparser instance for handler analysis (B::Deparse lazy-loaded)
my $DEPARSER;

# Protocol module for HTTP/1.1 (extensible for HTTP/2 in future)
my $PROTOCOL = 'Hypersonic::Protocol::HTTP1';

# Optional TLS support
my $HAS_TLS = 0;
eval { require Hypersonic::TLS; $HAS_TLS = Hypersonic::TLS::check_openssl(); };

# Check for HTTP/2 support (nghttp2)
my $HAS_HTTP2 = 0;
eval { require Hypersonic::Protocol::HTTP2; $HAS_HTTP2 = Hypersonic::Protocol::HTTP2::check_nghttp2() ? 1 : 0; };

sub new {
    my ($class, %opts) = @_;
    
    # Validate TLS options
    if ($opts{tls}) {
        die "TLS support not available (OpenSSL not found)" unless $HAS_TLS;
        die "cert_file required for TLS" unless $opts{cert_file};
        die "key_file required for TLS" unless $opts{key_file};
        die "cert_file not found: $opts{cert_file}" unless -f $opts{cert_file};
        die "key_file not found: $opts{key_file}" unless -f $opts{key_file};
    }
    
    # Validate HTTP/2 options
    if ($opts{http2}) {
        die "HTTP/2 requires TLS (set tls => 1)" unless $opts{tls};
        die "HTTP/2 not available (nghttp2 not found)" unless $HAS_HTTP2;
    }
    
    # Security headers configuration
    my $security_headers = $opts{security_headers} // {};
    
    return bless {
        routes    => [],
        compiled  => 0,
        cache_dir => $opts{cache_dir} // '_hypersonic_cache',
        # Legacy fallback id: only used by compile() when Digest::MD5
        # isn't installable (extremely rare). The active code path uses
        # a content hash of the generated C source so identical server
        # configurations share the same JIT cache entry across fresh
        # perl processes - see compile() for details.
        id        => int(rand(100000)),
        # Server options
        host      => $opts{host} // '0.0.0.0',
        port      => $opts{port} // 8080,
        # TLS options
        tls       => $opts{tls} // 0,
        cert_file => $opts{cert_file},
        key_file  => $opts{key_file},
        # HTTP/2 support
        http2     => $opts{http2} // 0,
        # Security hardening options
        max_connections    => $opts{max_connections} // 10000,
        max_request_size   => $opts{max_request_size} // 8192,
        keepalive_timeout  => $opts{keepalive_timeout} // 30,
        recv_timeout       => $opts{recv_timeout} // 30,
        # WebSocket JIT options - granular control
        websocket_rooms      => $opts{websocket_rooms} // 0,  # Enable Room support
        max_rooms            => $opts{max_rooms} // 1000,
        max_clients_per_room => $opts{max_clients_per_room} // 10000,
        # Graceful shutdown
        drain_timeout      => $opts{drain_timeout} // 5,
        # JIT extension points
        c_helpers          => $opts{c_helpers},  # User C helper functions
        # Security headers (JIT optimized - pre-computed at compile time)
        security_headers   => {
            'X-Frame-Options'           => $security_headers->{'X-Frame-Options'} // 'DENY',
            'X-Content-Type-Options'    => $security_headers->{'X-Content-Type-Options'} // 'nosniff',
            'X-XSS-Protection'          => $security_headers->{'X-XSS-Protection'} // '1; mode=block',
            'Referrer-Policy'           => $security_headers->{'Referrer-Policy'} // 'strict-origin-when-cross-origin',
            'Content-Security-Policy'   => $security_headers->{'Content-Security-Policy'},  # User must set this
            'Strict-Transport-Security' => ($opts{tls} ? ($security_headers->{'Strict-Transport-Security'} // 'max-age=31536000; includeSubDomains') : undef),
            'Permissions-Policy'        => $security_headers->{'Permissions-Policy'},  # User can optionally set
        },
        enable_security_headers => $opts{enable_security_headers} // 1,  # Enabled by default
        # Middleware support
        before_middleware => [],  # Global before hooks
        after_middleware  => [],  # Global after hooks
        # Event backend (optional override)
        event_backend => $opts{event_backend},
    }, $class;
}

# Route registration methods
sub get    { shift->_add_route('GET',    @_) }
sub post   { shift->_add_route('POST',   @_) }
sub put    { shift->_add_route('PUT',    @_) }
sub del    { shift->_add_route('DELETE', @_) }
sub patch  { shift->_add_route('PATCH',  @_) }
sub head   { shift->_add_route('HEAD',   @_) }
sub options { shift->_add_route('OPTIONS', @_) }

# Health check endpoint - built-in route for load balancer / k8s probes
sub health_check {
    my ($self, $path, $handler) = @_;
    $path //= '/health';
    
    # Default handler returns JSON string (JIT compiled as constant)
    $handler //= sub {
        return '{"status":"ok"}';
    };
    
    return $self->get($path => $handler);
}

# Readiness check endpoint - separate from health for k8s
sub ready_check {
    my ($self, $path, $handler) = @_;
    $path //= '/ready';
    
    $handler //= sub {
        return '{"ready":true}';
    };
    
    return $self->get($path => $handler);

lib/Hypersonic.pm  view on Meta::CPAN

        cache_dir        => $self->{cache_dir},
        response_helpers => $analysis{needs_response_helpers},
    );

    # Pre-evaluate all static handlers and build FULL HTTP responses
    my @full_responses;
    my @dynamic_handlers;  # Store CV refs for dynamic routes
    my @route_param_info;  # Store param info for dynamic routes

    for my $i (0 .. $#{$self->{routes}}) {
        my $route = $self->{routes}[$i];

        if (!$route->{dynamic}) {
            # Check if this is a pre-built static file response
            if ($route->{_static_file}) {
                push @full_responses, $route->{_static_response};
                $route->{response_idx} = $#full_responses;
                next;
            }
            
            # need_xs_builder routes are handled later in code generation
            if ($route->{need_xs_builder}) {
                $route->{dynamic} = 1;  # Treat as dynamic for dispatch
                push @dynamic_handlers, $route->{handler};
                push @route_param_info, $route->{params};
                $route->{handler_idx} = $#dynamic_handlers;
                next;
            }
            
            # RUN THE HANDLER ONCE - this is the magic
            my $result = $route->{handler}->();
            
            # Support both string and [status, headers, body] format
            my ($status, $headers, $body);
            if (ref($result) eq 'ARRAY') {
                ($status, $headers, $body) = @$result;
                $status //= 200;
                $headers //= {};
            } elsif (ref($result) eq 'HASH') {
                $status = $result->{status} // 200;
                $headers = $result->{headers} // {};
                $body = $result->{body} // '';
            } else {
                $status = 200;
                $headers = {};
                $body = $result;
            }
            
            die "Handler for $route->{method} $route->{path} must return a string or response structure"
                unless defined $body && !ref($body);

            # Build COMPLETE HTTP response via Protocol module (JIT at compile time)
            my $security_hdrs = $self->{enable_security_headers} 
                              ? $self->_get_security_headers_string() 
                              : '';
            
            my $full_response = $PROTOCOL->build_response(
                status           => $status,
                headers          => $headers,
                body             => $body,
                keep_alive       => 1,
                security_headers => $security_hdrs,
            );

            push @full_responses, $full_response;
            $route->{response_idx} = $#full_responses;
        } else {
            # Dynamic route - store handler index and param info
            push @dynamic_handlers, $route->{handler};
            push @route_param_info, $route->{params};
            $route->{handler_idx} = $#dynamic_handlers;
        }
    }

    # Store dynamic handlers and param info for runtime access
    $self->{dynamic_handlers} = \@dynamic_handlers;
    $self->{route_param_info} = \@route_param_info;

    # JIT: Build streaming handlers lookup table (only if streaming is enabled)
    if ($self->{route_analysis}{needs_streaming}) {
        my @streaming_flags;
        for my $route (@{$self->{routes}}) {
            next unless $route->{dynamic};
            push @streaming_flags, $route->{streaming} ? 1 : 0;
        }
        $self->{_streaming_flags} = \@streaming_flags;
    }

    # JIT: Build WebSocket handlers lookup table
    if ($self->_has_websocket_routes()) {
        my @ws_handlers;
        my @ws_paths;
        for my $route (@{$self->{websocket_routes}}) {
            push @ws_handlers, $route->{handler};
            push @ws_paths, $route->{path};
            # Check for Room usage in route options
            if ($route->{opts}{rooms}) {
                $self->{route_analysis}{needs_websocket_rooms} = 1;
            }
        }
        $self->{_websocket_handlers} = \@ws_handlers;
        $self->{_websocket_paths} = \@ws_paths;
        $self->{route_analysis}{needs_websocket} = 1;
        # Handler is needed if we have websocket routes
        $self->{route_analysis}{needs_websocket_handler} = 1;
        # WebSocket uses Stream for connection handling
        $self->{route_analysis}{needs_streaming} = 1;
    }

    # JIT: Explicit opt-in for Room support (can also be set in new())
    if ($self->{websocket_rooms}) {
        $self->{route_analysis}{needs_websocket_rooms} = 1;
    }

    # JIT: Build per-route middleware arrays (only if route middleware is present)
    my $analysis = $self->{route_analysis};
    if ($analysis->{has_route_middleware}) {
        my @route_before_mw;
        my @route_after_mw;
        for my $route (@{$self->{routes}}) {
            next unless $route->{dynamic};

lib/Hypersonic.pm  view on Meta::CPAN

    # Common includes - portable across POSIX and Windows. On Windows
    # we substitute Winsock for netinet/socket/unistd, and define a
    # couple of compatibility macros so the rest of the codegen can
    # stay POSIX-shaped.
    $builder->line('#include <string.h>')
      ->line('#include <errno.h>')
      ->raw(<<'C');
#ifdef _WIN32
#  define WIN32_LEAN_AND_MEAN
#  include <winsock2.h>
#  include <ws2tcpip.h>
#  include <io.h>
#  define close(fd) closesocket(fd)
   /* Windows has no O_NONBLOCK / fcntl(F_SETFL, O_NONBLOCK); use
    * ioctlsocket(FIONBIO) instead. Wrap both fcntl invocations the
    * codegen emits so they vanish on Windows. */
#  define hs_set_nonblocking(fd) do { u_long _m = 1; ioctlsocket((fd), FIONBIO, &_m); } while(0)
   /* sockets don't raise SIGPIPE on Windows. */
#  ifndef MSG_NOSIGNAL
#    define MSG_NOSIGNAL 0
#  endif
#else
#  include <unistd.h>
#  include <fcntl.h>
#  include <sys/socket.h>
#  include <sys/types.h>
#  include <netinet/in.h>
#  include <netinet/tcp.h>
#  define hs_set_nonblocking(fd) do { int _f = fcntl((fd), F_GETFL, 0); fcntl((fd), F_SETFL, _f | O_NONBLOCK); } while(0)
#endif
C

    # Backend-specific includes
    $builder->line($backend->includes);
    
    # Add signal and time headers for graceful shutdown
    $builder->line('#include <signal.h>')
      ->line('#include <time.h>');
    
    # Compression support - include zlib if compression is enabled
    if ($self->{_compression_enabled}) {
        $builder->line('#include <zlib.h>')
          ->line('#define HYPERSONIC_COMPRESSION 1');
    }
    
    # TLS support - include OpenSSL headers if TLS is enabled
    if ($self->{tls}) {
        $builder->raw(Hypersonic::TLS::gen_includes())
          ->line('#define HYPERSONIC_TLS 1');
    }
    
    # HTTP/2 support - include nghttp2 if enabled
    if ($self->{http2}) {
        require Hypersonic::Protocol::HTTP2;
        Hypersonic::Protocol::HTTP2->gen_includes($builder);
    }

    # Security hardening configuration
    my $max_connections = $self->{max_connections};
    my $max_request_size = $self->{max_request_size};
    my $keepalive_timeout = $self->{keepalive_timeout};
    my $recv_timeout = $self->{recv_timeout};
    my $drain_timeout = $self->{drain_timeout};

    # Backend-specific defines
    $builder->blank
      ->line($backend->defines)
      ->line("#define RECV_BUF_SIZE $max_request_size")
      ->line("#define MAX_CONNECTIONS $max_connections");
    
    # Enable security headers macro if configured
    if ($self->{enable_security_headers} && $has_dynamic) {
        $builder->line('#define HYPERSONIC_SECURITY_HEADERS 1');
    }
    
    $builder
      ->line("#define KEEPALIVE_TIMEOUT $keepalive_timeout")
      ->line("#define RECV_TIMEOUT $recv_timeout")
      ->line("#define DRAIN_TIMEOUT $drain_timeout")
      ->blank;
    
    # TLS-aware I/O macros - compile-time decision, zero runtime overhead
    $builder->comment('TLS-aware I/O wrappers - compile-time branching')
      ->line('#ifdef HYPERSONIC_TLS')
      ->line('#define HYPERSONIC_SEND(fd, buf, len) do { \\')
      ->line('    TLSConnection* _tc = get_tls_connection(fd); \\')
      ->line('    if (_tc) tls_send(_tc, buf, len); \\')
      ->line('    else send(fd, buf, len, 0); \\')
      ->line('} while(0)')
      ->line('#define HYPERSONIC_CLOSE(fd) tls_close(fd)')
      ->line('#else')
      ->line('#define HYPERSONIC_SEND(fd, buf, len) send(fd, buf, len, 0)')
      ->line('#define HYPERSONIC_CLOSE(fd) close(fd)')
      ->line('#endif')
      ->blank;

    # User-defined C helpers (early, so they're available to all routes)
    if (my $helpers = $self->{c_helpers}) {
        $builder->comment('User-defined C helpers');
        if (ref $helpers eq 'CODE') {
            my $helper_builder = XS::JIT::Builder->new;
            $helpers->($helper_builder);
            $builder->raw($helper_builder->code);
        } else {
            # Raw C string
            $builder->raw($helpers);
        }
        $builder->blank;
    }

    # Graceful shutdown support
    $builder->comment('Graceful shutdown support')
      ->line('static volatile sig_atomic_t g_shutdown = 0;')
      ->line('static volatile int g_active_connections = 0;')
      ->blank
      ->line('static void handle_shutdown_signal(int sig) {')
      ->line('    (void)sig;')
      ->line('    g_shutdown = 1;')
      ->line('}')
      ->blank;
    
    # Compression support - JIT compiled zlib functions
    if ($self->{_compression_enabled}) {
        my $config = $self->{_compression_config};
        my $min_size = $config->{min_size} // 1024;
        my $level = $config->{level} // 6;
        
        $builder->comment('Gzip compression support - JIT compiled')
          ->line('static __thread unsigned char gzip_out_buf[131072];')
          ->blank
          ->line('static int accepts_gzip(const char* accept_encoding, size_t len) {')
          ->line('    if (!accept_encoding || len == 0) return 0;')
          ->line('    const char* p = accept_encoding;')
          ->line('    const char* end = accept_encoding + len;')
          ->line('    while (p < end - 3) {')
          ->line('        if (p[0] == \'g\' && p[1] == \'z\' && p[2] == \'i\' && p[3] == \'p\') return 1;')
          ->line('        p++;')
          ->line('    }')
          ->line('    return 0;')
          ->line('}')
          ->blank
          ->line('static size_t gzip_compress(const char* input, size_t input_len, unsigned char** output) {')
          ->line('    size_t max_out;')
          ->line('    z_stream strm;')
          ->line('    int ret;')
          ->line('    size_t compressed_len;')
          ->line("    if (input_len < $min_size) return 0;")
          ->line('    max_out = compressBound(input_len) + 18;')
          ->line('    if (max_out > sizeof(gzip_out_buf)) return 0;')
          ->line('    memset(&strm, 0, sizeof(strm));')
          ->line("    if (deflateInit2(&strm, $level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) return 0;")
          ->line('    strm.next_in = (Bytef*)input;')
          ->line('    strm.avail_in = input_len;')
          ->line('    strm.next_out = gzip_out_buf;')
          ->line('    strm.avail_out = sizeof(gzip_out_buf);')
          ->line('    ret = deflate(&strm, Z_FINISH);')
          ->line('    compressed_len = strm.total_out;')
          ->line('    deflateEnd(&strm);')
          ->line('    if (ret != Z_STREAM_END || compressed_len >= input_len) return 0;')
          ->line('    *output = gzip_out_buf;')
          ->line('    return compressed_len;')
          ->line('}')
          ->blank;
    }
    
    # Connection tracking for keep-alive timeout - O(1) using fd as index
    $builder->comment('Connection tracking - O(1) using fd as direct index')
      ->line('#define MAX_FD 65536')
      ->line('static time_t g_conn_time[MAX_FD];')
      ->line('static time_t g_current_time = 0;')
      ->blank
      ->line("static $inline void track_connection(int fd, time_t now) {")
      ->line('    if (fd >= 0 && fd < MAX_FD) {')
      ->line('        g_conn_time[fd] = now;')
      ->line('        g_active_connections++;')
      ->line('    }')
      ->line('}')
      ->blank
      ->line("static $inline void update_connection(int fd, time_t now) {")
      ->line('    if (fd >= 0 && fd < MAX_FD) {')
      ->line('        g_conn_time[fd] = now;')
      ->line('    }')
      ->line('}')
      ->blank
      ->line("static $inline void remove_connection(int fd) {")
      ->line('    if (fd >= 0 && fd < MAX_FD && g_conn_time[fd] > 0) {')
      ->line('        g_conn_time[fd] = 0;')
      ->line('        g_active_connections--;')
      ->line('    }')
      ->line('}')
      ->blank;

    # TLS code generation - SSL context, accept, read/write wrappers
    if ($self->{tls}) {
        $builder->comment('TLS/HTTPS support via OpenSSL')
          ->raw(Hypersonic::TLS::gen_ssl_ctx_init(http2 => $self->{http2}))
          ->blank
          ->raw(Hypersonic::TLS::gen_ssl_accept())
          ->blank
          ->raw(Hypersonic::TLS::gen_ssl_io())
          ->blank
          ->raw(Hypersonic::TLS::gen_ssl_close())
          ->blank;
    }
    
    # HTTP/2 code generation - nghttp2 callbacks, session init, dispatchers
    if ($self->{http2}) {
        require Hypersonic::Protocol::HTTP2;
        $builder->comment('HTTP/2 support via nghttp2');
        Hypersonic::Protocol::HTTP2->gen_connection_struct($builder);
        Hypersonic::Protocol::HTTP2->gen_connection_preface_check($builder);
        Hypersonic::Protocol::HTTP2->gen_response_sender($builder);
        Hypersonic::Protocol::HTTP2->gen_404_response($builder);
        Hypersonic::Protocol::HTTP2->gen_callbacks($builder);
        Hypersonic::Protocol::HTTP2->gen_session_init($builder);
        Hypersonic::Protocol::HTTP2->gen_dispatcher($builder);
        Hypersonic::Protocol::HTTP2->gen_input_processor($builder);
        $builder->blank;
    }

    # Streaming support - JIT: only generate when streaming handlers detected
    my $analysis = $self->{route_analysis};
    if ($analysis->{needs_streaming}) {
        require Hypersonic::Stream;
        Hypersonic::Stream->generate_c_code($builder, {
            max_streams => $self->{max_connections},

lib/Hypersonic.pm  view on Meta::CPAN

      ->line('signal(SIGTERM, handle_shutdown_signal);')
      ->line('signal(SIGINT, handle_shutdown_signal);')
      ->blank
      ->comment('Initialize connection tracking')
      ->line('memset(g_conn_time, 0, sizeof(g_conn_time));')
      ->line('g_active_connections = 0;')
      ->blank;

    # TLS initialization
    if ($self->{tls}) {
        my $cert_file = _escape_c_string($self->{cert_file});
        my $key_file = _escape_c_string($self->{key_file});
        $builder->comment('Initialize TLS/HTTPS')
          ->line('#ifdef HYPERSONIC_TLS')
          ->line("if (init_ssl_ctx(\"$cert_file\", \"$key_file\") != 0) {")
          ->line('    croak("Failed to initialize TLS context - check cert/key files");')
          ->line('}')
          ->line('memset(g_tls_connections, 0, sizeof(g_tls_connections));')
          ->line('#endif')
          ->blank;
    }

    $builder->comment('Thread-local receive buffer - each worker gets its own')
      ->line('static __thread char recv_buf[RECV_BUF_SIZE];')
      ->blank;

    # Backend-specific: Create event loop and add listen socket
    $backend->gen_create($builder, 'listen_fd');

    # Async Pool: Initialize thread pool and add notify fd to event loop
    if ($analysis->{needs_async_pool}) {
        $builder->blank
          ->comment('Initialize async thread pool')
          ->line('pool_init();')
          ->line('int pool_notify_fd = pool_get_notify_fd();');
        $backend->gen_add_pool_notify($builder, 'ev_fd', 'pool_notify_fd');
    }

    # Declare event structure variable based on backend
    if ($backend_name eq 'kqueue') {
        # kqueue's gen_create already declares 'ev', but we need events array
        $builder->line('struct kevent events[MAX_EVENTS];');
    } elsif ($backend_name eq 'epoll') {
        # epoll's gen_create already declares 'ev'
        $builder->line('struct epoll_event events[MAX_EVENTS];');
    } elsif ($backend_name eq 'io_uring') {
        # io_uring uses completion queue entries
        $builder->line('struct io_uring_cqe** events = NULL;');
    } else {
        # poll/select manage their own fd arrays internally
        # Declare a dummy events pointer to satisfy gen_wait signature
        $builder->line('void* events = NULL;');
    }

    $builder->line('time_t last_cleanup = time(NULL);')
      ->line('int accepting = 1;  /* Flag to control accepting new connections */')
      ->blank;

    # Main event loop
    $builder->while('!g_shutdown || g_active_connections > 0')
        ->comment('Use timeout for keep-alive cleanup and shutdown check');

    # Backend-specific: Wait for events
    $backend->gen_wait($builder, 'ev_fd', 'events', 'n', '1000');

    $builder->blank
        ->comment('Check for graceful shutdown - stop accepting new connections')
        ->if('g_shutdown && accepting');

    # Backend-specific: Remove listen socket from event loop
    $backend->gen_del($builder, 'ev_fd', 'listen_fd');
    $builder->line('accepting = 0;')
        ->endif
        ->blank
        ->comment('Get time once per event batch')
        ->line('time_t now = time(NULL);')
        ->line('g_current_time = now;')
        ->blank;

    # Keep-alive cleanup
    $builder->comment('Periodic keep-alive timeout cleanup')
      ->if('now - last_cleanup >= 5')
        ->declare('int', 'cleanup_i', '0')
        ->for('cleanup_i = 0', 'cleanup_i < MAX_FD', 'cleanup_i++')
          ->if('g_conn_time[cleanup_i] > 0')
            ->if('now - g_conn_time[cleanup_i] > KEEPALIVE_TIMEOUT')
              ->comment('Close idle connection')
              ->line('int idle_fd = cleanup_i;');

    # Backend-specific: Remove idle connection
    $backend->gen_del($builder, 'ev_fd', 'idle_fd');
    $builder->line('HYPERSONIC_CLOSE(idle_fd);')
              ->line('remove_connection(idle_fd);')
            ->endif
          ->endif
        ->endfor
        ->line('last_cleanup = now;')
      ->endif
      ->blank;

    # Event processing loop
    $builder->declare('int', 'i', '0')
      ->for('i = 0', 'i < n', 'i++');

    # Backend-specific: Get fd from event
    $backend->gen_get_fd($builder, 'events', 'i', 'fd');

    $builder->blank
        ->if('fd == listen_fd && accepting');

    # Accept loop
    $builder->comment('Accept new connections with limit check')
      ->while('1')
        ->comment('Check connection limit before accepting')
        ->if('g_active_connections >= MAX_CONNECTIONS')
          ->line('break;  /* At capacity, stop accepting */')
        ->endif
        ->blank
        ->line('struct sockaddr_in client_addr;')
        ->line('socklen_t client_len = sizeof(client_addr);')
        ->line('int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);')
        ->if('client_fd < 0')
          ->line('break;')
        ->endif
        ->blank
        ->comment('Set non-blocking (hs_set_nonblocking handles Win/POSIX divergence)')
        ->line('hs_set_nonblocking(client_fd);')
        ->blank
        ->comment('Disable Nagle')
        ->line('int one = 1;')
        ->line('setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, (const char*)&one, sizeof(one));')
        ->blank
        ->comment('Set receive timeout for security')
        ->line('struct timeval tv;')
        ->line('tv.tv_sec = RECV_TIMEOUT;')
        ->line('tv.tv_usec = 0;')
        ->line('setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));')
        ->blank
        ->comment('Track connection for keep-alive timeout')
        ->line('track_connection(client_fd, now);')
        ->blank
        ->comment('TLS handshake if enabled')
        ->line('#ifdef HYPERSONIC_TLS')
        ->line('if (tls_accept(client_fd) < 0) {')
        ->line('    close(client_fd);')
        ->line('    remove_connection(client_fd);')
        ->line('    continue;')
        ->line('}')
        ->line('#endif')
        ->blank
        ->comment('Add to event loop');

    # Backend-specific: Add client to event loop
    $backend->gen_add($builder, 'ev_fd', 'client_fd');
    $builder->endwhile;

    # Async Pool: Handle pool notify fd - process completed futures
    if ($analysis->{needs_async_pool}) {
        $builder->elsif('fd == pool_notify_fd')
          ->comment('Thread pool notification - process completed async operations')
          ->line('pool_process_ready();');
    }

    # Handle client request
    $builder->elsif('fd != listen_fd')
      ->comment('Handle client request')
      ->line('#ifdef HYPERSONIC_TLS')
      ->line('TLSConnection* tls_conn = get_tls_connection(fd);')
      ->line('ssize_t len = tls_conn ? tls_recv(tls_conn, recv_buf, RECV_BUF_SIZE - 1) : -1;')
      ->line('#else')
      ->line('ssize_t len = recv(fd, recv_buf, RECV_BUF_SIZE - 1, 0);')
      ->line('#endif')
      ->blank
      ->if('len <= 0')
        ->comment('Connection closed or error');

    # Backend-specific: Remove from event loop
    $backend->gen_del($builder, 'ev_fd', 'fd');
    $builder->line('#ifdef HYPERSONIC_TLS')
        ->line('tls_close(fd);')
        ->line('#else')
        ->line('close(fd);')
        ->line('#endif');

    # Reset WebSocket state if WebSocket routes exist
    if ($analysis->{needs_websocket}) {
        $builder->line('ws_reset(fd);');
    }

    $builder->line('remove_connection(fd);')
        ->line('continue;')
      ->endif
      ->blank
      ->comment('Update connection activity for keep-alive timeout')
      ->line('update_connection(fd, now);')
      ->blank
      ->line('recv_buf[len] = \'\\0\';')
      ->blank;

    # WebSocket frame handling - JIT: only generate if WebSocket routes exist
    if ($analysis->{needs_websocket}) {
        $self->_gen_websocket_frame_handler($builder);
    }

    # Method parser - delegates to Protocol module
    $self->_gen_method_parser($builder);
    $builder->blank;

    # Path parsing - delegates to Protocol module
    $PROTOCOL->gen_path_parser($builder);
    $builder->blank;

    # WebSocket upgrade detection - JIT: only generate if WebSocket routes exist
    if ($analysis->{needs_websocket}) {
        $self->_gen_websocket_dispatch($builder);
    }

    # Dispatch
    $builder->comment('Dispatch request')
      ->line('const char* resp;')
      ->line('int resp_len;')
      ->line('int handler_idx;')
      ->line('int dispatch_result = dispatch_request(method, method_len, path, path_len, &resp, &resp_len, &handler_idx);')
      ->blank;

    # Dynamic dispatch
    if ($has_dynamic) {
        # Check for XS builder routes first (dispatch_result == 2)
        if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) {
            $builder->if('dispatch_result == 2')
              ->comment('XS builder route - call generated XS function directly');
            
            # Body parsing for XS builder routes (they may need body access)
            $PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);
            
            $builder->blank
              ->line('char* xs_resp;')
              ->line('int xs_resp_len;')
              ->line('call_xs_builder_handler(aTHX_ handler_idx, fd, method, method_len, path, path_len, body, body_len, &xs_resp, &xs_resp_len);')
              ->line('HYPERSONIC_SEND(fd, xs_resp, xs_resp_len);')
            ->elsif('dispatch_result == 1');
        } else {
            $builder->if('dispatch_result == 1');
        }
        
        $builder->comment('Dynamic route - call Perl handler');

        # Body parsing - delegates to Protocol module
        $PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);

        $builder->blank
          ->line('char* dyn_resp;')
          ->line('int dyn_resp_len;')
          ->line('call_dynamic_handler(aTHX_ handler_idx, fd, method, method_len, path, full_path_len, body, body_len, recv_buf, len, &dyn_resp, &dyn_resp_len);')
          ->comment('dyn_resp_len == -1 means streaming handler (response already sent)')
          ->line('if (dyn_resp_len >= 0) {')
          ->line('    HYPERSONIC_SEND(fd, dyn_resp, dyn_resp_len);')
          ->line('}')
        ->else
          ->line('HYPERSONIC_SEND(fd, resp, resp_len);')
        ->endif;
    } else {
        $builder->line('HYPERSONIC_SEND(fd, resp, resp_len);');
    }
    $builder->blank;

    # Keep-alive check - delegates to Protocol module
    $PROTOCOL->gen_keepalive_check($builder);
    $builder->blank
      ->if('!keep_alive');

    # Backend-specific: Remove from event loop on close
    $backend->gen_del($builder, 'ev_fd', 'fd');
    $builder->line('HYPERSONIC_CLOSE(fd);');

    # Reset WebSocket state if WebSocket routes exist
    if ($analysis->{needs_websocket}) {
        $builder->line('ws_reset(fd);');
    }

    $builder->line('remove_connection(fd);')
      ->endif;

    # Close event processing
    $builder->endif  # fd == listen_fd
      ->endfor  # for i
      ->endwhile  # main loop
      ->blank;

    # Async Pool: Shutdown thread pool on server exit
    if ($analysis->{needs_async_pool}) {
        $builder->comment('Shutdown async thread pool')
          ->line('pool_shutdown();');
    }

    $builder->line('close(ev_fd);')
      ->xs_return('0')
      ->xs_end;

    return $builder;
}

sub _gen_xs_builder_dispatcher {
    my ($self) = @_;
    
    my $builder = XS::JIT::Builder->new;
    my $xs_routes = $self->{_xs_builder_routes} || [];
    
    return '' unless @$xs_routes;
    
    $builder->comment('XS Builder route dispatcher')
      ->comment('Dispatches to user-defined XS functions based on handler_idx')
      ->line('static void call_xs_builder_handler(pTHX_ int handler_idx, int fd,')
      ->line('                                     const char* method, int method_len,')
      ->line('                                     const char* path, int path_len,')
      ->line('                                     const char* body, int body_len,')
      ->line('                                     char** resp_out, int* resp_len_out) {')
      ->line('    switch (handler_idx) {');
    
    for my $entry (@$xs_routes) {
        my $handler_idx = $entry->{route}{handler_idx};
        my $xs_func = $entry->{result}{xs_function};
        
        $builder->line("        case $handler_idx:")
          ->line("            $xs_func(aTHX_ fd, method, method_len, path, path_len, body, body_len, resp_out, resp_len_out);")
          ->line("            break;");
    }
    
    $builder->line('        default:')
      ->line('            *resp_out = (char*)RESP_404;')

lib/Hypersonic.pm  view on Meta::CPAN

      ->line('                }')
      ->line('            }')
      ->comment('            Plain string response')
      ->line('            else {')
      ->line('                body_str = SvPV(result, len);')
      ->line('            }')
      ->blank
      ->comment('            Auto-detect JSON content type')
      ->line('            if (strcmp(content_type, "text/plain") == 0 && len > 0 &&')
      ->line('                (body_str[0] == \'{\' || body_str[0] == \'[\')) {')
      ->line('                content_type = "application/json";')
      ->line('            }')
      ->blank;
    
    # JIT: Add compression logic only if compression is enabled
    if ($self->{_compression_enabled}) {
        my $config = $self->{_compression_config};
        my $min_size = $config->{min_size} // 1024;
        
        $builder
          ->comment('            Gzip compression - check Accept-Encoding')
          ->line('#ifdef HYPERSONIC_COMPRESSION')
          ->line('            int use_gzip = 0;')
          ->line('            unsigned char* compressed_body = NULL;')
          ->line('            size_t compressed_len = 0;')
          ->blank
          ->comment('            Get Accept-Encoding from request (from SLOT_HEADERS)')
          ->line('            SV** req_arr = AvARRAY(req);')
          ->line('            HV* hdrs = NULL;')
          ->line('            if (req_arr[6] && SvROK(req_arr[6])) {')
          ->line('                hdrs = (HV*)SvRV(req_arr[6]);')
          ->line('            }')
          ->line('            if (hdrs && len >= ' . $min_size . ') {')
          ->line('                SV** ae = hv_fetch(hdrs, "accept_encoding", 15, 0);')
          ->line('                if (ae && SvOK(*ae)) {')
          ->line('                    STRLEN ae_len;')
          ->line('                    const char* ae_str = SvPV(*ae, ae_len);')
          ->line('                    if (accepts_gzip(ae_str, ae_len)) {')
          ->line('                        compressed_len = gzip_compress(body_str, len, &compressed_body);')
          ->line('                        if (compressed_len > 0) {')
          ->line('                            use_gzip = 1;')
          ->line('                            body_str = (const char*)compressed_body;')
          ->line('                            len = compressed_len;')
          ->line('                        }')
          ->line('                    }')
          ->line('                }')
          ->line('            }')
          ->line('#endif')
          ->blank;
    }
    
    $builder
      ->comment('            Build response with custom headers support')
      ->line('            static __thread char resp_buf[65536];')
      ->line('            int hdr_len;')
      ->line('#ifdef HYPERSONIC_SECURITY_HEADERS')
      ->line('            hdr_len = snprintf(resp_buf, 2048,')
      ->line('                "HTTP/1.1 %d %s\\r\\n"')
      ->line('                "Content-Type: %s\\r\\n"')
      ->line('                "Content-Length: %zu\\r\\n"')
      ->line('                "Connection: keep-alive\\r\\n"')
      ->line('                "%s",')
      ->line('                status, get_status_text(status), content_type, len, SECURITY_HEADERS);')
      ->line('#else')
      ->line('            hdr_len = snprintf(resp_buf, 512,')
      ->line('                "HTTP/1.1 %d %s\\r\\n"')
      ->line('                "Content-Type: %s\\r\\n"')
      ->line('                "Content-Length: %zu\\r\\n"')
      ->line('                "Connection: keep-alive\\r\\n",')
      ->line('                status, get_status_text(status), content_type, len);')
      ->line('#endif')
      ->blank
      ->comment('            Add custom headers from response (Location, Set-Cookie, etc.)')
      ->line('            if (custom_headers) {')
      ->line('                HE* entry;')
      ->line('                hv_iterinit(custom_headers);')
      ->line('                while ((entry = hv_iternext(custom_headers))) {')
      ->line('                    I32 klen;')
      ->line('                    const char* key = hv_iterkey(entry, &klen);')
      ->comment('                    Skip Content-Type/Content-Length (already added)')
      ->line('                    if (klen == 12 && memcmp(key, "Content-Type", 12) == 0) continue;')
      ->line('                    if (klen == 14 && memcmp(key, "Content-Length", 14) == 0) continue;')
      ->line('                    SV* val = hv_iterval(custom_headers, entry);')
      ->comment('                    Handle Set-Cookie array (multiple cookies)')
      ->line('                    if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {')
      ->line('                        AV* arr = (AV*)SvRV(val);')
      ->line('                        SSize_t arr_len = av_len(arr) + 1;')
      ->line('                        SSize_t j;')
      ->line('                        for (j = 0; j < arr_len; j++) {')
      ->line('                            SV** item = av_fetch(arr, j, 0);')
      ->line('                            if (item && SvOK(*item)) {')
      ->line('                                STRLEN vlen;')
      ->line('                                const char* vstr = SvPV(*item, vlen);')
      ->line('                                hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
      ->line('                                    "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);')
      ->line('                            }')
      ->line('                        }')
      ->line('                    } else if (SvOK(val)) {')
      ->line('                        STRLEN vlen;')
      ->line('                        const char* vstr = SvPV(val, vlen);')
      ->line('                        hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
      ->line('                            "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);')
      ->line('                    }')
      ->line('                }')
      ->line('            }')
      ->blank
      ->comment('            Add Content-Encoding header if gzip was used')
      ->line('#ifdef HYPERSONIC_COMPRESSION')
      ->line('            if (use_gzip) {')
      ->line('                hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
      ->line('                    "Content-Encoding: gzip\\r\\n");')
      ->line('            }')
      ->line('#endif')
      ->blank
      ->comment('            End headers')
      ->line('            memcpy(resp_buf + hdr_len, "\\r\\n", 2);')
      ->line('            hdr_len += 2;')
      ->blank
      ->line('            if (hdr_len + len < sizeof(resp_buf)) {')
      ->line('                memcpy(resp_buf + hdr_len, body_str, len);')
      ->line('                *resp_out = resp_buf;')
      ->line('                *resp_len_out = hdr_len + (int)len;')
      ->line('            } else {')
      ->line('                *resp_out = (char*)RESP_404;')
      ->line('                *resp_len_out = RESP_404_LEN;')
      ->line('            }');
    
    # Different closing braces based on middleware presence
    if ($analysis->{has_any_middleware}) {

lib/Hypersonic.pm  view on Meta::CPAN

        my $max_age = $config->{max_age};
        my $gen_etag = $config->{etag};
        
        # Recursively find all files
        File::Find::find({
            no_chdir => 1,
            wanted => sub {
                return unless -f $_;
                my $file_path = $_;
                my $rel_path = $file_path;
                $rel_path =~ s{^\Q$dir\E/?}{};
                
                # URL path for this file
                my $url_path = "$prefix/$rel_path";
                $url_path =~ s{//+}{/}g;
                
                # Read file content
                open my $fh, '<:raw', $file_path or return;
                local $/;
                my $content = <$fh>;
                close $fh;
                
                # Get MIME type
                my $mime = _get_mime_type($file_path);
                
                # Generate ETag (MD5 of content)
                my $etag = '';
                if ($gen_etag) {
                    $etag = Digest::MD5::md5_hex($content);
                }
                
                # Store file info
                push @static_files, {
                    url_path => $url_path,
                    content  => $content,
                    mime     => $mime,
                    etag     => $etag,
                    max_age  => $max_age,
                    length   => length($content),
                };
            },
        }, $dir);
    }
    
    # Store for code generation
    $self->{_static_files} = \@static_files;
    
    # Create static routes - these are essentially pre-computed responses
    for my $file (@static_files) {
        my $url_path = $file->{url_path};
        my $content = $file->{content};
        my $mime = $file->{mime};
        my $etag = $file->{etag};
        my $max_age = $file->{max_age};
        my $len = $file->{length};
        
        # Build complete HTTP response at compile time
        my $response = "HTTP/1.1 200 OK\r\n"
                     . "Content-Type: $mime\r\n"
                     . "Content-Length: $len\r\n"
                     . "Connection: keep-alive\r\n";
        $response .= "Cache-Control: public, max-age=$max_age\r\n" if $max_age;
        $response .= "ETag: \"$etag\"\r\n" if $etag;
        
        # Add security headers
        if ($self->{enable_security_headers}) {
            $response .= $self->_get_security_headers_string();
        }
        
        $response .= "\r\n" . $content;
        
        # Store as static route
        push @{$self->{routes}}, {
            method   => 'GET',
            path     => $url_path,
            handler  => sub { $content },  # Dummy handler for static
            dynamic  => 0,
            params   => [],
            segments => [split('/', $url_path)],
            features => {},
            before   => [],
            after    => [],
            # Mark as static file with pre-built response
            _static_response => $response,
            _static_file     => 1,
        };
    }
}

# Generate security headers string for HTTP responses
# Pre-computed at compile time - zero runtime overhead
sub _get_security_headers_string {
    my ($self) = @_;
    my $headers = '';
    
    for my $name (sort keys %{$self->{security_headers}}) {
        my $value = $self->{security_headers}{$name};
        next unless defined $value && length($value);
        $headers .= "$name: $value\r\n";
    }
    
    return $headers;
}

# Generate security headers as C string constant for dynamic routes
sub _gen_security_headers_c_constant {
    my ($self) = @_;
    return '' unless $self->{enable_security_headers};
    
    my $headers = $self->_get_security_headers_string();
    return '' unless length($headers);
    
    my $escaped = _escape_c_string($headers);
    return "static const char SECURITY_HEADERS[] = \"$escaped\";\n"
         . "static const int SECURITY_HEADERS_LEN = " . length($headers) . ";\n";
}

sub dispatch {
    my ($self, $req) = @_;
    die "Must call compile() first" unless $self->{compiled};
    return $self->{dispatch_fn}->($req);

lib/Hypersonic.pm  view on Meta::CPAN

Hypersonic is a benchmark-focused micro HTTP server that uses XS::JIT to
generate and compile C code at runtime. The entire event loop runs in C
with no Perl in the hot path.

B<What it does:>

=over 4

=item 1. Static route handlers run ONCE at C<compile()> time

=item 2. Response strings (including HTTP headers) are baked into C as static constants

=item 3. Dynamic routes run Perl handlers per-request with JIT-compiled request objects

=item 4. A pure C event loop (kqueue/epoll) is generated and compiled

=item 5. Security headers are pre-computed and baked into responses at compile time

=item 6. Optional TLS/HTTPS support via OpenSSL with JIT-compiled wrappers

=back

B<Performance:> ~290,000 requests/second on a single core (macOS/kqueue).

=head1 METHODS

=head2 new

    my $server = Hypersonic->new(%options);

Create a new Hypersonic server instance.

B<Options:>

=over 4

=item cache_dir

Directory for caching compiled XS modules. Default: C<_hypersonic_cache>

=item tls

Enable TLS/HTTPS support. Requires C<cert_file> and C<key_file>. Default: C<0>

=item cert_file

Path to TLS certificate file (PEM format). Required if C<tls> is enabled.

=item key_file

Path to TLS private key file (PEM format). Required if C<tls> is enabled.

=item max_connections

Maximum number of concurrent connections. Default: C<10000>

=item max_request_size

Maximum request size in bytes. Default: C<8192>

=item keepalive_timeout

Keep-alive connection timeout in seconds. Default: C<30>

=item recv_timeout

Receive timeout in seconds. Default: C<30>

=item drain_timeout

Graceful shutdown drain timeout in seconds. Default: C<5>

=item enable_security_headers

Enable security headers (X-Frame-Options, X-Content-Type-Options, etc.). Default: C<1>

=item security_headers

HashRef of custom security header values. Example:

    security_headers => {
        'X-Frame-Options'         => 'SAMEORIGIN',
        'Content-Security-Policy' => "default-src 'self'",
    }

=item websocket_rooms

Enable WebSocket Room support for broadcast groups. Default: C<0>

Only set to C<1> if you need L<Hypersonic::WebSocket::Room> for broadcasting
to groups of connections. This adds Room-specific XS code to your compiled server.

=item max_rooms

Maximum number of rooms when C<websocket_rooms> is enabled. Default: C<1000>

=item max_clients_per_room

Maximum clients per room when C<websocket_rooms> is enabled. Default: C<10000>

=item c_helpers

C helper functions to inject early in the generated code, making them available
to all routes. Can be a coderef that receives an L<XS::JIT::Builder>, or a raw
C string.

    # Using a coderef
    c_helpers => sub {
        my ($builder) = @_;
        $builder->line('static int double_value(int x) { return x * 2; }');
    }

    # Or raw C string
    c_helpers => 'static int double_value(int x) { return x * 2; }'

These helpers are available to C<need_xs_builder> routes.

=back

B<Example with TLS:>

    my $server = Hypersonic->new(
        tls       => 1,

lib/Hypersonic.pm  view on Meta::CPAN

=item $room->count_open

Get number of OPEN (not closing/closed) connections in the room.

=item $room->broadcast($message, $exclude)

Send text message to all room members. Optional C<$exclude> WebSocket
connection to skip (typically the sender).

=item $room->broadcast_binary($data, $exclude)

Send binary data to all room members.

=item $room->close_all($code, $reason)

Close all connections in the room with given code and reason.

=item $room->clear

Remove all connections from the room (without closing them).

=item $room->clients

Get list of all WebSocket connections in the room.

=back

B<Global Broadcast Pattern:>

To broadcast to ALL connected WebSocket clients, use a global room:

    my $global = Hypersonic::WebSocket::Room->new('__global__');
    
    $server->websocket('/ws' => sub {
        my ($ws) = @_;
        
        $ws->on(open => sub {
            $global->join($ws);
            $global->broadcast("A user joined! (" . $global->count . " online)");
        });
        
        $ws->on(message => sub {
            my ($msg) = @_;
            $global->broadcast($msg, $ws);  # Send to all except sender
        });
        
        $ws->on(close => sub {
            $global->leave($ws);
            $global->broadcast("A user left");
        });
    });

=head2 streaming

    $server->get('/events' => sub {
        my ($stream) = @_;

        # Send SSE events
        my $sse = $stream->sse;
        $sse->event(type => 'update', data => 'Hello');
        $sse->keepalive;
        $sse->close;
    }, { streaming => 1 });

Enable streaming responses for a route. The handler receives a
L<Hypersonic::Stream> object instead of returning a static response.

B<Stream Object Methods:>

=over 4

=item $stream->write($data)

Write data to the response (chunked encoding).

=item $stream->end($data)

Write final data and close the stream.

=item $stream->sse

Get an L<Hypersonic::SSE> object for Server-Sent Events.

=back

B<SSE Object Methods:>

=over 4

=item $sse->event(type => $type, data => $data, id => $id)

Send an SSE event with optional type and id.

=item $sse->data($data)

Send a data-only event (no type field).

=item $sse->retry($ms)

Set client reconnection interval in milliseconds.

=item $sse->keepalive

Send a keepalive comment to prevent timeout.

=item $sse->comment($text)

Send an SSE comment.

=item $sse->close

Close the SSE stream.

=back

B<Example - Server-Sent Events:>

    $server->get('/notifications' => sub {
        my ($stream) = @_;
        my $sse = $stream->sse;

        $sse->retry(3000);  # Reconnect after 3s
        $sse->event(
            type => 'notification',
            data => '{"message":"New update!"}',
            id   => '12345',
        );

        # Keep connection alive...
        $sse->keepalive;
    }, { streaming => 1 });

=head2 Route Handler Options

All route methods accept an optional hashref as the third argument:

    $server->get('/path' => sub { ... }, {
        dynamic       => 1,           # Force dynamic handler
        parse_query   => 1,           # Parse query string
        parse_headers => 1,           # Parse HTTP headers
        parse_cookies => 1,           # Parse Cookie header
        parse_json    => 1,           # Parse JSON body
        parse_form    => 1,           # Parse form-urlencoded body
        before        => [\&mw1],     # Per-route before middleware
        after         => [\&mw2],     # Per-route after middleware
        need_xs_builder => 1,         # Generate C code at compile time
    });

=head2 need_xs_builder Routes

When C<need_xs_builder =E<gt> 1>, the route handler is called at compile time
with a fresh L<XS::JIT::Builder> object instead of a request object. The handler
must generate C code and return a hashref with the XS function name:

    $server->get('/xs/counter' => sub {
        my ($builder) = @_;
        
        # Generate C code for this route
        $builder->line('static int counter = 0;')
          ->line('static void handle_counter(pTHX_ int fd,')
          ->line('    const char* method, int method_len,')
          ->line('    const char* path, int path_len,')
          ->line('    const char* body, int body_len,')
          ->line('    char** resp_out, int* resp_len_out) {')
          ->line('    counter++;')
          ->line('    static char response[256];')
          ->line('    int n = snprintf(response, sizeof(response),')
          ->line('        "HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\n"')
          ->line('        "Content-Length: 14\\r\\n\\r\\n{\\"count\\":%d}", counter);')
          ->line('    *resp_out = response;')
          ->line('    *resp_len_out = n;')
          ->line('}');
        
        return { xs_function => 'handle_counter' };
    }, { need_xs_builder => 1 });

The XS function signature must match:

    void func_name(pTHX_ int fd,
                   const char* method, int method_len,
                   const char* path, int path_len,
                   const char* body, int body_len,
                   char** resp_out, int* resp_len_out);

The function should write a complete HTTP response to C<*resp_out> and set
C<*resp_len_out> to the response length.

Use with C<c_helpers> to share utility functions:

    my $server = Hypersonic->new(

lib/Hypersonic.pm  view on Meta::CPAN

Must be called after all routes are registered, before C<run()>.

=head2 JIT Feature Detection

Hypersonic uses a "JIT philosophy" - only code that's actually needed gets
compiled. The C<compile()> method analyzes your routes and sets these flags:

=over 4

=item needs_streaming

Set when any route has C<streaming =E<gt> 1>. Compiles L<Hypersonic::Stream>
and L<Hypersonic::SSE> XS code.

=item needs_websocket

Set when any C<websocket()> routes are registered. Compiles
L<Hypersonic::WebSocket> and L<Hypersonic::Protocol::WebSocket::Frame> XS code.

=item needs_websocket_handler

Automatically set when C<needs_websocket> is true. Compiles
L<Hypersonic::WebSocket::Handler> for connection registry.

=item needs_websocket_rooms

Set when C<websocket_rooms =E<gt> 1> is passed to C<new()>, or when any
WebSocket route has C<rooms =E<gt> 1> in its options. Compiles
L<Hypersonic::WebSocket::Room> for broadcast groups.

=item has_any_middleware

Set when global C<before()> or C<after()> middleware is registered.

=item has_route_middleware

Set when any route has per-route C<before> or C<after> options.

=item needs_async_pool

Set when C<async_pool()> is called. Compiles L<Hypersonic::Future> and
L<Hypersonic::Future::Pool> for async thread pool operations.

=back

You can inspect these flags after compile:

    $server->compile();
    my $analysis = $server->{route_analysis};
    
    say "Has streaming: ", $analysis->{needs_streaming} ? "yes" : "no";
    say "Has WebSocket: ", $analysis->{needs_websocket} ? "yes" : "no";
    say "Has Rooms: ", $analysis->{needs_websocket_rooms} ? "yes" : "no";

=head2 dispatch

    my $response = $server->dispatch($request_arrayref);

Dispatch a request and return the response. Primarily for testing.

Request is an arrayref: C<[method, path, body, keep_alive, fd]>

=head2 run

    $server->run(port => 8080, workers => 4);

Start the HTTP server event loop.

B<Options:>

=over 4

=item port

Port to listen on. Default: C<8080>

=item workers

Number of worker processes. Default: C<1>

=back

=head1 FULL EXAMPLE

    use Hypersonic;
    use Hypersonic::Response 'res';

    my $server = Hypersonic->new(
        max_request_size => 16384,
        enable_security_headers => 1,
    );

    # Global middleware
    $server->before(sub {
        my ($req) = @_;
        # Log request
        warn $req->method . ' ' . $req->path . "\n";
        return;  # Continue
    });

    # Static route (runs once at compile time)
    $server->get('/health' => sub {
        '{"status":"ok"}'
    });

    # Dynamic route with path parameter
    $server->get('/users/:id' => sub {
        my ($req) = @_;
        my $id = $req->param('id');
        return res->json({ id => $id, name => "User $id" });
    });

    # POST with JSON body
    $server->post('/users' => sub {
        my ($req) = @_;
        my $data = $req->json;
        return res->status(201)->json({ created => $data->{name} });
    }, { parse_json => 1 });

    # Query parameters
    $server->get('/search' => sub {



( run in 0.538 second using v1.01-cache-2.11-cpan-56fb94df46f )