Hypersonic

 view release on metacpan or  search on metacpan

lib/Hypersonic.pm  view on Meta::CPAN

sub async_pool {
    my ($self, %opts) = @_;

    require Hypersonic::Future;
    require Hypersonic::Future::Pool;

    # Create a Pool instance (OO - allows multiple pools)
    my $pool = Hypersonic::Future::Pool->new(
        workers    => $opts{workers}    // 8,
        queue_size => $opts{queue_size} // 4096,
    );

    # Store in array for event loop registration
    push @{$self->{_async_pools} //= []}, $pool;

    # Mark that async pool is enabled - JIT code gen will include thread pool
    $self->{_async_enabled} = 1;

    # Return the Pool object (not $self anymore)
    return $pool;
}

# Compression configuration - JIT-compiled gzip compression in C
# Only loads compression module when called (JIT philosophy)
sub compress {
    my ($self, %opts) = @_;
    
    require Hypersonic::Compress;
    
    # Check for zlib
    unless (Hypersonic::Compress::check_zlib()) {
        warn "Warning: zlib not found, compression disabled\n";
        return $self;
    }
    
    # Configure compression
    my $config = Hypersonic::Compress->configure(%opts);
    
    # Mark that compression is enabled - JIT code gen will include zlib code
    $self->{_compression_enabled} = 1;
    $self->{_compression_config} = $config;
    
    return $self;
}

sub _add_route {
    my ($self, $method, $path, $handler, $opts) = @_;

    die "Path must start with /" unless $path =~ m{^/};
    die "Handler must be a code ref" unless ref($handler) eq 'CODE';

    # Check for dynamic option and feature flags
    my $dynamic = 0;
    my %features = (
        parse_query      => 0,  # Parse ?key=value query strings
        parse_headers    => 0,  # Parse HTTP headers
        parse_cookies    => 0,  # Parse Cookie header
        parse_json       => 0,  # Parse JSON body (requires Cpanel::JSON::XS)
        parse_form       => 0,  # Parse form-urlencoded body
        response_helpers => 0,  # JIT compile response helper methods
        streaming        => 0,  # Streaming response handler
        need_xs_builder  => 0,  # Handler receives XS::JIT::Builder
    );
    
    if (ref($opts) eq 'HASH') {
        $dynamic = $opts->{dynamic} ? 1 : 0;
        # Copy feature flags from options
        for my $feat (keys %features) {
            $features{$feat} = $opts->{$feat} ? 1 : 0 if exists $opts->{$feat};
        }
    }

    # Parse path parameters (supports multiple: /users/:user_id/posts/:post_id)
    my @params;
    my @segments = split '/', $path;
    shift @segments;  # Remove leading empty string
    
    for my $i (0 .. $#segments) {
        if ($segments[$i] =~ /^:(\w+)$/) {
            push @params, { name => $1, position => $i };
            $dynamic = 1;  # Path params imply dynamic
        }
    }

    # Streaming handlers are always dynamic
    if ($features{streaming}) {
        $dynamic = 1;
    }

    # need_xs_builder handlers are always dynamic (but handled specially at compile time)
    if ($features{need_xs_builder}) {
        $dynamic = 1;
    }

    push @{$self->{routes}}, {
        method          => $method,
        path            => $path,
        handler         => $handler,
        dynamic         => $dynamic,
        streaming       => $features{streaming},
        need_xs_builder => $features{need_xs_builder},
        params          => \@params,
        segments        => \@segments,
        features        => \%features,
        # Per-route middleware (optional)
        before    => $opts->{before} // [],
        after     => $opts->{after} // [],
    };

    return $self;
}

sub compile {
    my ($self) = @_;

    die "No routes defined" unless @{$self->{routes}} || @{$self->{static_dirs} // []};
    die "Already compiled" if $self->{compiled};

    # ============================================================
    # STATIC FILE PROCESSING - bake files into C at compile time
    # ============================================================
    if (my $static_dirs = $self->{static_dirs}) {
        $self->_compile_static_files($static_dirs);
    }

    # ============================================================
    # ROUTE ANALYSIS - determine what code to generate (JIT philosophy)
    # ============================================================
    my %analysis = (
        methods_used     => {},   # GET => 1, POST => 1, etc.
        has_dynamic      => 0,    # Any dynamic routes?
        has_static       => 0,    # Any static routes?
        has_path_params  => 0,    # Any routes with :param?
        has_body_access  => 0,    # Any routes that need body?
        route_count      => scalar(@{$self->{routes}}),
        all_same_prefix  => undef,  # Common prefix like /api/*
        single_method    => undef,  # Only one HTTP method used?
        # JIT feature flags - only generate code for features actually used
        needs_query      => 0,    # Any route needs query string parsing?
        needs_headers    => 0,    # Any route needs header access?
        needs_cookies    => 0,    # Any route needs cookie parsing?
        needs_json       => 0,    # Any route needs JSON body parsing?
        needs_form       => 0,    # Any route needs form data parsing?
        needs_response_helpers => 0,  # Any route needs response helper methods?
        needs_streaming  => 0,    # Any route uses streaming responses?
        needs_xs_builder => 0,    # Any route uses need_xs_builder?
        # Middleware flags - JIT: only generate middleware code if actually used
        has_global_before => scalar(@{$self->{before_middleware}}) > 0,
        has_global_after  => scalar(@{$self->{after_middleware}}) > 0,
        has_route_middleware => 0,  # Any route has before/after hooks?
        has_any_middleware   => 0,  # Global OR per-route middleware?
    );

    # First pass: collect method usage and route characteristics
    for my $route (@{$self->{routes}}) {
        $analysis{methods_used}{$route->{method}} = 1;

        if ($route->{dynamic}) {
            $analysis{has_dynamic} = 1;
            # Dynamic routes might need body access (POST/PUT/PATCH typically do)
            if ($route->{method} =~ /^(POST|PUT|PATCH)$/) {
                $analysis{has_body_access} = 1;
            }
            
            # JIT FEATURE DETECTION: Analyze handler code to detect what request
            # features it actually uses. This avoids generating unused parsing code.
            my $f = $route->{features} // {};
            
            # Explicit flags take precedence
            $analysis{needs_query}   = 1 if $f->{parse_query};
            $analysis{needs_headers} = 1 if $f->{parse_headers};
            $analysis{needs_cookies} = 1 if $f->{parse_cookies};
            $analysis{needs_json}    = 1 if $f->{parse_json};
            $analysis{needs_form}    = 1 if $f->{parse_form};
            $analysis{needs_response_helpers} = 1 if $f->{response_helpers};
            $analysis{needs_streaming} = 1 if $f->{streaming};
            $analysis{needs_xs_builder} = 1 if $f->{need_xs_builder};
            
            # Auto-detect by analyzing handler code
            my $handler_code = _deparse_handler($route->{handler});
            if ($handler_code) {
                # Look for $req->{query} or ->{query} access patterns
                $analysis{needs_query}   = 1 if $handler_code =~ /\{['"]*query['"]*\}/;
                $analysis{needs_headers} = 1 if $handler_code =~ /\{['"]*headers['"]*\}/;
                $analysis{needs_cookies} = 1 if $handler_code =~ /\{['"]*cookies['"]*\}/;
                $analysis{needs_json}    = 1 if $handler_code =~ /\{['"]*json['"]*\}/;
                $analysis{needs_form}    = 1 if $handler_code =~ /\{['"]*form['"]*\}/;
            }
        } else {
            $analysis{has_static} = 1;
        }

        if (@{$route->{params}}) {
            $analysis{has_path_params} = 1;
        }
        
        # Check for per-route middleware
        if (@{$route->{before}} || @{$route->{after}}) {
            $analysis{has_route_middleware} = 1;
        }
    }
    
    # Session support: force cookie parsing when sessions are enabled
    if ($self->{_session_enabled}) {
        $analysis{needs_cookies} = 1;
    }
    
    # Compression support: force header parsing to check Accept-Encoding
    if ($self->{_compression_enabled}) {
        $analysis{needs_headers} = 1;
    }

    # Async Pool support: enable thread pool integration.
    # Disabled on Windows - Pool uses pthread + eventfd, neither of
    # which exists there. (Future/Pool deserve a Win32 port; not in
    # the 0.14 minimum-viable Windows scope.)
    if ($self->{_async_enabled}) {
        if ($^O eq 'MSWin32') {
            warn "Hypersonic: async pool not supported on Windows; "
               . "ignoring.\n";
        } else {
            $analysis{needs_async_pool} = 1;
        }
    }

    # Classify middleware as builder (inline C) or Perl (call_sv)
    # Builder middleware has build_before/build_after methods and generates C at compile time
    my (@builder_before, @perl_before, @builder_after, @perl_after);
    for my $mw (@{$self->{before_middleware}}) {
        if (blessed($mw) && ($mw->can('build_before') || $mw->can('build_after'))) {
            push @builder_before, $mw;
        } else {
            push @perl_before, $mw;
        }
    }
    for my $mw (@{$self->{after_middleware}}) {

lib/Hypersonic.pm  view on Meta::CPAN

            }
            
            # 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};
            # Store arrays of middleware handlers per route (by handler_idx)
            push @route_before_mw, $route->{before};
            push @route_after_mw, $route->{after};
        }
        $self->{_route_before_mw} = \@route_before_mw;
        $self->{_route_after_mw} = \@route_after_mw;
    }

    # JIT: Store Perl-only middleware for runtime call_sv dispatch
    # Builder middleware is handled at compile time (inline C), not runtime
    if ($analysis->{has_global_before} || $analysis->{has_global_after}) {
        $self->{_perl_before_mw} = $analysis->{perl_before};
        $self->{_perl_after_mw}  = $analysis->{perl_after};
    }

    # Generate C code with pure C event loop
    my $c_code = $self->_generate_server_code(\@full_responses);

    # Compile via XS::JIT
    #
    # Derive a STABLE module id from a hash of the generated C source
    # (plus this dist's $VERSION and the target perl's archname/version
    # so a cache built for one perl is never loaded into another).
    #
    # Pre-0.18 we used 'Hypersonic::_Server_' . int(rand(100000)) which
    # gave a different module name on every fresh perl process; because
    # XS::JIT's on-disk cache lives at
    #   <cache_dir>/lib/auto/<safe_name>/<safe_name>.<dlext>
    # (see XS-JIT/lib/XS/JIT/xs_jit.c xs_jit_cache_path()), a different
    # $name on every run forced a full gcc/cc re-invocation EVERY time
    # the test suite or a user re-ran their server. On slow CPAN smoker
    # boxes that gcc invocation takes 30-60+ seconds per server, which
    # is what caused the SIGKILL cascade in CPAN tester reports for
    # 0.17 (t/0035-e2e-streaming.t, t/2012..t/2017, t/2102).
    #
    # Using a content hash means: identical route+option configurations
    # produce the same module name -> warm cache hit -> dlopen() of a
    # 100ms .so instead of a 30s gcc rebuild. The random fallback id is
    # retained for the degenerate case where Digest::MD5 isn't available.
    my $module_id;
    {
        my $hash_input = join("\0",
            $c_code,
            $VERSION,
            (defined $Config::Config{archname}
                ? $Config::Config{archname} : ''),
            $] || '',
        );
        if (eval { require Digest::MD5; 1 }) {
            # 16 hex chars (64 bits) is plenty of namespace for one
            # process and short enough to keep the on-disk file names
            # readable. md5_hex is core since perl 5.7.3.
            $module_id = substr(Digest::MD5::md5_hex($hash_input), 0, 16);
        } else {
            # No Digest::MD5? Fall back to the legacy random id. Cache
            # won't be reused across runs but at least nothing breaks.
            $module_id = $self->{id};
        }
    }
    my $module_name = 'Hypersonic::_Server_' . $module_id;
    
    # Build compile options - add TLS flags if enabled
    my %functions = (
        "${module_name}::run_event_loop" => {
            source       => 'hypersonic_run_event_loop',
            is_xs_native => 1,
        },
        "${module_name}::dispatch" => {
            source       => 'hypersonic_dispatch',
            is_xs_native => 1,
        },
    );

    # Add Stream and SSE XS functions if streaming is enabled
    if ($self->{route_analysis}{needs_streaming}) {
        %functions = (%functions, %{Hypersonic::Stream->get_xs_functions()});
        %functions = (%functions, %{Hypersonic::SSE->get_xs_functions()});
    }

    # Add WebSocket XS functions if WebSocket routes are registered
    if ($self->{route_analysis}{needs_websocket}) {
        require Hypersonic::WebSocket;
        %functions = (%functions, %{Hypersonic::WebSocket->get_xs_functions()});
    }

    # Add WebSocket Handler XS functions
    if ($self->{route_analysis}{needs_websocket_handler}) {
        require Hypersonic::WebSocket::Handler;
        %functions = (%functions, %{Hypersonic::WebSocket::Handler->get_xs_functions()});
    }

    # Add WebSocket Room XS functions
    if ($self->{route_analysis}{needs_websocket_rooms}) {
        require Hypersonic::WebSocket::Room;
        %functions = (%functions, %{Hypersonic::WebSocket::Room->get_xs_functions()});
    }

    # Add Future/Pool XS functions if async pool is enabled
    if ($self->{route_analysis}{needs_async_pool}) {
        require Hypersonic::Future;
        require Hypersonic::Future::Pool;
        %functions = (%functions, %{Hypersonic::Future->get_xs_functions()});
        %functions = (%functions, %{Hypersonic::Future::Pool->get_xs_functions()});
    }

    # Add need_xs_builder route additional XS functions (if any)
    # Note: The main handler functions are C functions called by call_xs_builder_handler,
    # NOT XS functions callable from Perl, so we don't register them.
    if (my $xsr = $self->{_xs_builder_routes}) {
        for my $entry (@$xsr) {
            my $result = $entry->{result};
            
            # Add any additional XS functions the handler defined
            if ($result->{xs_functions}) {
                %functions = (%functions, %{$result->{xs_functions}});
            }
        }
    }

    my %compile_opts = (
        code      => $c_code,
        name      => $module_name,
        cache_dir => $self->{cache_dir},
        functions => \%functions,
    );
    
    # Add OpenSSL flags for TLS support
    if ($self->{tls}) {
        $compile_opts{extra_cflags} = Hypersonic::TLS::get_extra_cflags();
        $compile_opts{extra_ldflags} = Hypersonic::TLS::get_extra_ldflags();
    }
    
    # Add nghttp2 flags for HTTP/2 support
    if ($self->{http2}) {
        require Hypersonic::Protocol::HTTP2;
        my $h2_cflags = Hypersonic::Protocol::HTTP2::get_extra_cflags();
        my $h2_ldflags = Hypersonic::Protocol::HTTP2::get_extra_ldflags();
        $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $h2_cflags";
        $compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $h2_ldflags";
    }
    
    # Add zlib flags for compression support
    if ($self->{_compression_enabled}) {
        require Hypersonic::Compress;
        my ($cflags, $ldflags) = Hypersonic::Compress::get_zlib_flags();
        $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $cflags";
        $compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $ldflags";
    }

    # Add pthread flags for async pool (thread pool)
    if ($self->{route_analysis}{needs_async_pool}) {
        $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " -pthread";
        $compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " -lpthread";
    }

    # Add event backend flags (e.g., io_uring needs -luring)
    if ($self->{_event_backend}) {
        my $backend = $self->{_event_backend};
        if ($backend->can('extra_cflags')) {
            my $ev_cflags = $backend->extra_cflags // '';
            $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $ev_cflags"
                if $ev_cflags;
        }
        if ($backend->can('extra_ldflags')) {
            my $ev_ldflags = $backend->extra_ldflags // '';
            $compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $ev_ldflags"
                if $ev_ldflags;
        }
    }

    # Windows: link Winsock so socket()/recv()/send()/etc. resolve in
    # the JIT-compiled .so. The select backend already adds this, but
    # belt-and-braces - the main compile uses these symbols too.
    if ($^O eq 'MSWin32') {
        my $ld = $compile_opts{extra_ldflags} // '';
        $compile_opts{extra_ldflags} = $ld . ' -lws2_32'
            unless $ld =~ /-lws2_32\b/;
    }

    # Emit a visible breadcrumb BEFORE the (potentially slow) gcc/cc
    # invocation so smoker logs never show "(child wrote no output)"
    # when wait_for_port times out mid-compile. Force a flush in case
    # something upstream disabled autoflush on STDERR.
    if ($ENV{HYPERSONIC_COMPILE_DIAG} || $ENV{AUTOMATED_TESTING}) {
        local $| = 1;
        print STDERR "# Hypersonic: compiling JIT module $module_name ...\n";
        eval { STDERR->flush; };
    }

    # The JIT boot xsub installs Hypersonic::Stream::* xsubs into
    # the same package that Hypersonic/Stream.pm already lives in,
    # which Perl reports as "Subroutine ... redefined". This is
    # expected (the .pm defines is_streaming_handler and the .so
    # provides the rest at compile time, or - on a second compile()
    # in the same process - it reinstalls them). Silence the noise.
    my $ok;
    {
        no warnings 'redefine';
        $ok = XS::JIT->compile(%compile_opts);
    }
    die "XS::JIT->compile failed for $module_name (check liburing/zlib/openssl "
      . "are installed and linkable; extra_ldflags='"
      . ($compile_opts{extra_ldflags} // '') . "')"
        unless $ok;

    # Store function references - confirm the boot xsub actually installed
    # them. If we somehow got past compile() with the .so loaded but the
    # symbols missing, fail loudly here rather than crash later in run().
    {
        no strict 'refs';
        $self->{run_loop_fn} = \&{"${module_name}::run_event_loop"};
        $self->{dispatch_fn} = \&{"${module_name}::dispatch"};
        die "XS::JIT loaded $module_name but ::dispatch is not defined "
          . "(stale cache_dir? link line was: ldflags='"
          . ($compile_opts{extra_ldflags} // '') . "')"
            unless defined &{"${module_name}::dispatch"};
    }

    # Mark Future/Pool as compiled if async pool is enabled
    # (prevents them from trying to compile separately)
    if ($self->{route_analysis}{needs_async_pool}) {
        $Hypersonic::Future::COMPILED = 1;
        $Hypersonic::Future::Pool::COMPILED = 1;
        # Register custom ops for Future after compilation
        Hypersonic::Future->_register_ops();
    }

    $self->{compiled} = 1;
    return $self;
}

sub _generate_server_code {
    my ($self, $full_responses) = @_;

    # Load event backend module
    require Hypersonic::Event;
    my $backend_name = $self->{event_backend} // Hypersonic::Event->best_backend;
    my $backend = Hypersonic::Event->backend($backend_name);

    # Store backend for use in event loop generation
    $self->{_event_backend} = $backend;
    $self->{_event_backend_name} = $backend_name;

    my $builder = XS::JIT::Builder->new;

    # C99 detection for inline keyword
    my $inline = Hypersonic::JIT::Util->inline_keyword;

    # Check if we have any dynamic routes
    my $has_dynamic = grep { $_->{dynamic} } @{$self->{routes}};

    # Common includes - portable across POSIX and Windows. On Windows
    # we substitute Winsock for netinet/socket/unistd, and define a

lib/Hypersonic.pm  view on Meta::CPAN

          ->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},
        });

        # SSE support - compile SSE methods when streaming is enabled
        require Hypersonic::SSE;
        Hypersonic::SSE->generate_c_code($builder, {
            max_sse_instances => $self->{max_connections},
        });
    }

    # WebSocket support - JIT: only generate when WebSocket routes exist
    if ($analysis->{needs_websocket}) {
        require Hypersonic::WebSocket;
        require Hypersonic::Protocol::WebSocket;
        require Hypersonic::Protocol::WebSocket::Frame;

        # Generate WebSocket frame encoding functions
        Hypersonic::Protocol::WebSocket::Frame->generate_c_code($builder, {
            max_connections => $self->{max_connections},
        });

        # Generate WebSocket connection management
        Hypersonic::WebSocket->generate_c_code($builder, {
            max_websockets => $self->{max_connections},
        });
    }

    # WebSocket Handler - JIT: connection registry, only when websocket routes exist
    if ($analysis->{needs_websocket_handler}) {
        require Hypersonic::WebSocket::Handler;
        Hypersonic::WebSocket::Handler->generate_c_code($builder, {
            max_connections => $self->{max_connections},
        });
    }

    # WebSocket Rooms - JIT: broadcast groups, only when explicitly enabled
    if ($analysis->{needs_websocket_rooms}) {
        require Hypersonic::WebSocket::Room;
        Hypersonic::WebSocket::Room->generate_c_code($builder, {
            max_rooms => $self->{max_rooms},
            max_clients_per_room => $self->{max_clients_per_room},
        });
    }

    # Future/Pool - JIT: async thread pool for blocking operations
    if ($analysis->{needs_async_pool}) {
        require Hypersonic::Future;
        require Hypersonic::Future::Pool;
        my $async_config = $self->{_async_config} // {};
        Hypersonic::Future->generate_c_code($builder, {
            max_futures => $async_config->{max_futures} // 65536,
        });
        Hypersonic::Future::Pool->generate_c_code($builder, {
            workers    => $async_config->{workers}    // 8,
            queue_size => $async_config->{queue_size} // 4096,
        });
    }

    # need_xs_builder routes - call handlers with fresh builder
    if ($analysis->{needs_xs_builder}) {
        my @xs_builder_routes;
        for my $i (0 .. $#{$self->{routes}}) {
            my $route = $self->{routes}[$i];
            next unless $route->{need_xs_builder};

lib/Hypersonic.pm  view on Meta::CPAN

                        ->raw($xsr->{code})
                        ->blank;
            }
        }
        
        # Store for function merging later
        $self->{_xs_builder_routes} = \@xs_builder_routes;
    }

    # JIT: WebSocket handler storage (independent of dynamic routes)
    if ($analysis->{needs_websocket}) {
        $builder->comment('WebSocket handler storage')
          ->line('static SV* g_websocket_handlers = NULL;')
          ->blank;
    }

    # Global storage for dynamic handler dispatch (only if needed)
    if ($has_dynamic) {
        $builder->comment('Storage for dynamic handler callbacks')
          ->line('static SV* g_handler_array = NULL;')
          ->line('static SV* g_server_obj = NULL;');

        # JIT: Only generate middleware storage if middleware is present
        if ($analysis->{has_any_middleware}) {
            $builder->line('static SV* g_before_middleware = NULL;')
              ->line('static SV* g_after_middleware = NULL;')
              ->line('static SV* g_route_before_middleware = NULL;')
              ->line('static SV* g_route_after_middleware = NULL;');
        }
        $builder->blank;

        # Generate param info table for named path parameters
        # Structure: { param_name, segment_position } per handler
        $builder->comment('Path parameter info per dynamic handler')
          ->line('typedef struct { const char* name; int position; } ParamInfo;')
          ->line('typedef struct { int count; ParamInfo params[8]; } RouteParamInfo;')
          ->blank;

        my @route_params = @{$self->{route_param_info} // []};
        my $handler_count = scalar @route_params;

        $builder->line("static RouteParamInfo g_route_params[$handler_count] = {");
        for my $i (0 .. $#route_params) {
            my $params = $route_params[$i] // [];
            my $count = scalar @$params;
            my @param_strs;
            for my $p (@$params) {
                push @param_strs, qq({ "$p->{name}", $p->{position} });
            }
            # Pad to 8 elements with {NULL, 0}
            my $padding = 8 - scalar(@param_strs);
            for (1 .. $padding) {
                push @param_strs, '{NULL, 0}';
            }
            my $params_str = join(', ', @param_strs);
            $builder->line("    { $count, { $params_str } },");
        }
        $builder->line('};')
          ->blank;

        # JIT: Streaming handler flags array (only if streaming is enabled)
        if ($analysis->{needs_streaming} && $self->{_streaming_flags}) {
            my @flags = @{$self->{_streaming_flags}};
            my $flags_str = join(', ', @flags);
            $builder->comment('Streaming handler flags - 1 = streaming, 0 = normal')
              ->line("static int g_streaming_handlers[$handler_count] = { $flags_str };")
              ->blank;
        }
    }

    # JIT: WebSocket route paths array (only if WebSocket routes exist)
    if ($analysis->{needs_websocket} && $self->{_websocket_paths}) {
        my @paths = @{$self->{_websocket_paths}};
        my $ws_count = scalar @paths;
        $builder->comment('WebSocket route paths');
        for my $i (0 .. $#paths) {
            my $escaped = _escape_c_string($paths[$i]);
            $builder->line(qq{static const char WS_PATH_$i\[] = "$escaped";});
        }
        $builder->line("static const char* g_ws_paths[$ws_count] = {");
        for my $i (0 .. $#paths) {
            my $comma = ($i < $#paths) ? ',' : '';
            $builder->line("    WS_PATH_$i$comma");
        }
        $builder->line('};')
          ->line("static const int g_ws_path_count = $ws_count;")
          ->blank;
    }

    # Emit FULL pre-computed HTTP responses (headers + body)
    for my $i (0 .. $#$full_responses) {
        my $resp = $full_responses->[$i];
        my $escaped = _escape_c_string($resp);
        my $len = length($resp);
        $builder->line("static const char RESP_$i\[] = \"$escaped\";")
          ->line("static const int RESP_${i}_LEN = $len;");
    }
    $builder->blank;

    # 404 response via Protocol module
    my $security_hdrs_404 = $self->{enable_security_headers} 
                          ? $self->_get_security_headers_string() 
                          : '';
    my $resp_404 = $PROTOCOL->build_404_response(
        security_headers => $security_hdrs_404,
    );
    
    my $escaped_404 = _escape_c_string($resp_404);
    $builder->line("static const char RESP_404[] = \"$escaped_404\";")
      ->line("static const int RESP_404_LEN = " . length($resp_404) . ";")
      ->blank;
    
    # Security headers constant for dynamic responses
    if ($self->{enable_security_headers} && $has_dynamic) {
        $builder->raw($self->_gen_security_headers_c_constant())
          ->blank;
    }

    # Generate dynamic handler caller if needed
    if ($has_dynamic) {
        $builder->raw($self->_gen_dynamic_handler_caller())
          ->blank;
    }

    # Generate XS builder route dispatcher if needed
    if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) {

lib/Hypersonic.pm  view on Meta::CPAN

      ->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,')

lib/Hypersonic.pm  view on Meta::CPAN

          ->line('    dSP;')
          ->line('    SSize_t len = av_len(handlers) + 1;')
          ->line('    SSize_t i;')
          ->line('    for (i = 0; i < len; i++) {')
          ->line('        SV** handler_sv = av_fetch(handlers, i, 0);')
          ->line('        if (!handler_sv || !SvROK(*handler_sv)) continue;')
          ->line('        PUSHMARK(SP);')
          ->line('        XPUSHs(req_ref);')
          ->line('        PUTBACK;')
          ->line('        int count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
          ->line('        SPAGAIN;')
          ->line('        if (SvTRUE(ERRSV)) {')
          ->line('            POPs;')
          ->line('            continue;')
          ->line('        }')
          ->line('        if (count == 1) {')
          ->line('            SV* result = POPs;')
          ->line('            PUTBACK;')
          ->line('            if (SvOK(result)) {')
          ->line('                return SvREFCNT_inc(result);')
          ->line('            }')
          ->line('        }')
          ->line('        PUTBACK;')
          ->line('    }')
          ->line('    return NULL;')
          ->line('}')
          ->blank;
    }
    
    # Main handler function
    $builder->comment('Call dynamic Perl handler and format HTTP response')
      ->line('static void call_dynamic_handler(pTHX_ int handler_idx, int client_fd,')
      ->line('                                  const char* method, int method_len,')
      ->line('                                  const char* path, int path_len,')
      ->line('                                  const char* body, int body_len,')
      ->line('                                  const char* raw_request, int raw_request_len,')
      ->line('                                  char** resp_out, int* resp_len_out) {')
      ->line('    dSP;')
      ->line('    int count;')
      ->line('    SV* result;')
      ->line('    STRLEN len;')
      ->line('    const char* body_str;')
      ->line('    int status = 200;')
      ->line('    const char* content_type = "text/plain";')
      ->line('    AV* handlers;')
      ->line('    SV** handler_sv;')
      ->line('    const char* query_start;')
      ->line('    int clean_path_len;')
      ->line('    AV* req;')
      ->line('    const char* segments[16];')
      ->line('    int seg_lens[16];')
      ->line('    int seg_count;')
      ->line('    AV* seg_av;')
      ->line('    int i;')
      ->line('    HV* params_hv;')
      ->line('    RouteParamInfo* param_info;')
      ->line('    SV* req_ref;')
      ->line('    SV* mw_result = NULL;')
      ->line('    int short_circuit = 0;')
      ->line('    HV* headers_hv = NULL;')
      ->line('    int is_streaming = 0;')
      ->line('    SV* stream_sv = NULL;')
      ->blank
      ->line('    if (!g_handler_array) {')
      ->line('        *resp_out = (char*)RESP_404;')
      ->line('        *resp_len_out = RESP_404_LEN;')
      ->line('        return;')
      ->line('    }')
      ->blank
      ->line('    handlers = (AV*)SvRV(g_handler_array);')
      ->line('    handler_sv = av_fetch(handlers, handler_idx, 0);')
      ->line('    if (!handler_sv || !SvROK(*handler_sv)) {')
      ->line('        *resp_out = (char*)RESP_404;')
      ->line('        *resp_len_out = RESP_404_LEN;')
      ->line('        return;')
      ->line('    }')
      ->blank;
    
    # Query string separation
    $builder->comment('Separate path from query string')
      ->line('    query_start = memchr(path, \'?\', path_len);')
      ->line('    clean_path_len = query_start ? (query_start - path) : path_len;')
      ->blank
      ->comment('Build array-based request object (JIT slots)')
      ->comment('Slot layout: METHOD=0, PATH=1, BODY=2, PARAMS=3, QUERY=4, QUERY_STRING=5,')
      ->comment('             HEADERS=6, COOKIES=7, JSON=8, FORM=9, SEGMENTS=10, ID=11')
      ->line('    req = newAV();')
      ->line('    av_extend(req, 11);')  # Pre-allocate 12 slots (0-11)
      ->line('    av_store(req, 0, newSVpvn(method, method_len));')       # SLOT_METHOD
      ->line('    av_store(req, 1, newSVpvn(path, clean_path_len));')     # SLOT_PATH
      ->line('    av_store(req, 2, newSVpvn(body, body_len));')           # SLOT_BODY
      ->blank;

    # Path segments and params - always needed
    $builder->comment('Parse path segments and named params')
      ->line('    seg_count = parse_path_segments(path, path_len, segments, seg_lens, 16);')
      ->line('    seg_av = newAV();')
      ->line('    for (i = 0; i < seg_count; i++) {')
      ->line('        av_push(seg_av, newSVpvn(segments[i], seg_lens[i]));')
      ->line('    }')
      ->line('    av_store(req, 10, newRV_noinc((SV*)seg_av));')  # SLOT_SEGMENTS
      ->blank
      ->comment('Build named params from route_param_info table')
      ->line('    params_hv = newHV();')
      ->line('    param_info = &g_route_params[handler_idx];')
      ->line('    for (i = 0; i < param_info->count && i < seg_count; i++) {')
      ->line('        int pos = param_info->params[i].position;')
      ->line('        if (pos < seg_count) {')
      ->line('            hv_store(params_hv, param_info->params[i].name,')
      ->line('                     strlen(param_info->params[i].name),')
      ->line('                     newSVpvn(segments[pos], seg_lens[pos]), 0);')
      ->line('        }')
      ->line('    }')
      ->line('    av_store(req, 3, newRV_noinc((SV*)params_hv));')  # SLOT_PARAMS
      ->line('    if (seg_count > 0) {')
      ->line('        av_store(req, 11, newSVpvn(segments[seg_count-1], seg_lens[seg_count-1]));')  # SLOT_ID
      ->line('    } else {')
      ->line('        av_store(req, 11, newSVpvn("", 0));')  # SLOT_ID (empty)
      ->line('    }')
      ->blank;
    

lib/Hypersonic.pm  view on Meta::CPAN

    $builder->comment('Bless array into Hypersonic::Request and call Perl handler')
      ->line('    req_ref = newRV_noinc((SV*)req);')
      ->line('    sv_bless(req_ref, gv_stashpv("Hypersonic::Request", GV_ADD));')
      ->line('    ENTER;')
      ->line('    SAVETMPS;');
    
    # JIT: Add middleware short-circuit variable only if middleware present
    # (mw_result and short_circuit already declared at function top)
    
    # JIT: Builder-based before middleware (inline C - no Perl calls)
    if ($analysis->{has_builder_before}) {
        my $ctx = {
            req_var     => 'req',
            req_ref_var => 'req_ref',
            slots       => $analysis->{middleware_slots},
        };
        $builder->blank
          ->comment('JIT: Builder before middleware (inline C - zero Perl overhead)');
        for my $mw (@{$analysis->{builder_before}}) {
            if ($mw->can('build_before')) {
                $mw->build_before($builder, $ctx);
            }
        }
    }

    # JIT: Call global before middleware (Perl coderefs via call_sv)
    if ($analysis->{has_global_before}) {
        $builder->blank
          ->comment('JIT: Call global before middleware (Perl)')
          ->line('    if (g_before_middleware && SvROK(g_before_middleware)) {')
          ->line('        AV* before_arr = (AV*)SvRV(g_before_middleware);')
          ->line('        mw_result = call_middleware_chain(aTHX_ before_arr, req_ref);')
          ->line('        if (mw_result) {')
          ->line('            result = mw_result;')
          ->line('            short_circuit = 1;')
          ->line('        }')
          ->line('    }');
    }
    
    # JIT: Call per-route before middleware
    if ($analysis->{has_route_middleware}) {
        $builder->blank
          ->comment('JIT: Call per-route before middleware')
          ->line('    if (!short_circuit && g_route_before_middleware && SvROK(g_route_before_middleware)) {')
          ->line('        AV* route_before = (AV*)SvRV(g_route_before_middleware);')
          ->line('        SV** handler_arr_ref = av_fetch(route_before, handler_idx, 0);')
          ->line('        if (handler_arr_ref && SvROK(*handler_arr_ref)) {')
          ->line('            AV* handler_arr = (AV*)SvRV(*handler_arr_ref);')
          ->line('            if (av_len(handler_arr) >= 0) {')
          ->line('                mw_result = call_middleware_chain(aTHX_ handler_arr, req_ref);')
          ->line('                if (mw_result) {')
          ->line('                    result = mw_result;')
          ->line('                    short_circuit = 1;')
          ->line('                }')
          ->line('            }')
          ->line('        }')
          ->line('    }');
    }
    
    # JIT: Streaming handler support
    if ($analysis->{needs_streaming}) {
        $builder->blank
          ->comment('JIT: Check if this is a streaming handler')
          ->line('    is_streaming = g_streaming_handlers[handler_idx];')
          ->if('is_streaming')
            ->comment('Create Hypersonic::Stream object for streaming handler')
            ->line('dSP;')
            ->line('PUSHMARK(SP);')
            ->line('XPUSHs(sv_2mortal(newSVpv("Hypersonic::Stream", 0)));')
            ->line('XPUSHs(sv_2mortal(newSVpv("fd", 0)));')
            ->line('XPUSHs(sv_2mortal(newSViv(client_fd)));')
            ->line('PUTBACK;')
            ->line('int stream_count = call_method("new", G_SCALAR);')
            ->line('SPAGAIN;')
            ->if('stream_count > 0')
              ->line('stream_sv = POPs;')
              ->line('SvREFCNT_inc(stream_sv);')
            ->endif
            ->line('PUTBACK;')
          ->endif;
    }

    # Call the main handler (conditionally if middleware present)
    if ($analysis->{has_any_middleware}) {
        $builder->blank
          ->comment('Call main handler (unless middleware short-circuited)')
          ->line('    if (!short_circuit) {')
          ->line('        PUSHMARK(SP);')
          ->line('        XPUSHs(req_ref);');

        # For streaming handlers with middleware
        if ($analysis->{needs_streaming}) {
            $builder->line('        if (is_streaming && stream_sv) XPUSHs(stream_sv);');
        }

        $builder->line('        PUTBACK;')
          ->line('        count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
          ->line('        SPAGAIN;')
          ->line('        if (count == 1) result = POPs;')
          ->line('        PUTBACK;')
          ->line('    }');
    } else {
        $builder->line('    PUSHMARK(SP);')
          ->line('    XPUSHs(sv_2mortal(req_ref));');

        # For streaming handlers without middleware
        if ($analysis->{needs_streaming}) {
            $builder->line('    if (is_streaming && stream_sv) XPUSHs(stream_sv);');
        }

        $builder->line('    PUTBACK;')
          ->line('    count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
          ->line('    SPAGAIN;');
    }
    
    # JIT: Call per-route after middleware
    if ($analysis->{has_route_middleware}) {
        $builder->blank
          ->comment('JIT: Call per-route after middleware')
          ->line('    if (g_route_after_middleware && SvROK(g_route_after_middleware)) {')
          ->line('        AV* route_after = (AV*)SvRV(g_route_after_middleware);')
          ->line('        SV** handler_arr_ref = av_fetch(route_after, handler_idx, 0);')
          ->line('        if (handler_arr_ref && SvROK(*handler_arr_ref)) {')
          ->line('            AV* handler_arr = (AV*)SvRV(*handler_arr_ref);')
          ->line('            if (av_len(handler_arr) >= 0) {')
          ->line('                SV* after_result = call_middleware_chain(aTHX_ handler_arr, req_ref);')
          ->line('                if (after_result) {')
          ->line('                    result = after_result;')
          ->line('                }')
          ->line('            }')
          ->line('        }')
          ->line('    }');
    }
    
    # JIT: Call global after middleware (Perl coderefs via call_sv)
    if ($analysis->{has_global_after}) {
        $builder->blank
          ->comment('JIT: Call global after middleware (Perl)')
          ->line('    if (g_after_middleware && SvROK(g_after_middleware)) {')
          ->line('        AV* after_arr = (AV*)SvRV(g_after_middleware);')
          ->line('        SV* after_result = call_middleware_chain(aTHX_ after_arr, req_ref);')
          ->line('        if (after_result) {')
          ->line('            result = after_result;')
          ->line('        }')
          ->line('    }');
    }

    # JIT: Builder-based after middleware (inline C - no Perl calls)
    if ($analysis->{has_builder_after}) {
        my $ctx = {
            req_var     => 'req',
            req_ref_var => 'req_ref',
            res_var     => 'result',
            slots       => $analysis->{middleware_slots},
        };
        $builder->blank
          ->comment('JIT: Builder after middleware (inline C - zero Perl overhead)');
        for my $mw (@{$analysis->{builder_after}}) {
            if ($mw->can('build_after')) {
                $mw->build_after($builder, $ctx);
            }
        }
    }

    # JIT: Streaming handlers - early return (response already sent via Stream)
    if ($analysis->{needs_streaming}) {
        $builder->blank
          ->comment('JIT: Streaming handlers return early - response sent via Stream object')
          ->if('is_streaming')
            ->line('if (stream_sv) SvREFCNT_dec(stream_sv);')
            ->line('FREETMPS;')
            ->line('LEAVE;')
            ->line('*resp_out = NULL;')
            ->line('*resp_len_out = -1;')
            ->comment('Signal streaming response - caller should not send')
            ->line('return;')
          ->endif;
    }

    $builder->blank
      ->line('    if (SvTRUE(ERRSV)) {')
      ->line('        static char error_resp[512];')
      ->line('        int err_len = snprintf(error_resp, sizeof(error_resp),')
      ->line('            "HTTP/1.1 500 Internal Server Error\\r\\n"')
      ->line('            "Content-Type: text/plain\\r\\n"')
      ->line('            "Content-Length: 21\\r\\n"')
      ->line('            "Connection: close\\r\\n\\r\\n"')
      ->line('            "Internal Server Error");')
      ->line('        *resp_out = error_resp;')
      ->line('        *resp_len_out = err_len;');
    
    # Middleware version already has result set, no-middleware version needs POPs
    if ($analysis->{has_any_middleware}) {
        $builder->line('    } else if (SvOK(result)) {')
          ->line('            HV* custom_headers = NULL;');
    } else {
        $builder->line('        POPs;')
          ->line('    } else if (count == 1) {')
          ->line('        result = POPs;')
          ->line('        if (SvOK(result)) {')
          ->line('            HV* custom_headers = NULL;');
    }

    $builder
      ->comment('            Handle arrayref [status, headers, body]')
      ->line('            if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVAV) {')
      ->line('                AV* arr = (AV*)SvRV(result);')
      ->line('                SV** status_sv = av_fetch(arr, 0, 0);')
      ->line('                SV** headers_sv = av_fetch(arr, 1, 0);')
      ->line('                SV** body_sv = av_fetch(arr, 2, 0);')
      ->line('                if (status_sv) status = (int)SvIV(*status_sv);')
      ->line('                if (body_sv) body_str = SvPV(*body_sv, len);')
      ->line('                else { body_str = ""; len = 0; }')
      ->line('                if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {')
      ->line('                    custom_headers = (HV*)SvRV(*headers_sv);')
      ->line('                    SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);')
      ->line('                    if (ct_sv && SvOK(*ct_sv)) {')
      ->line('                        STRLEN ct_len;')
      ->line('                        content_type = SvPV(*ct_sv, ct_len);')
      ->line('                    }')
      ->line('                }')
      ->line('            }')
      ->comment('            Handle hashref {status, headers, body}')
      ->line('            else if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVHV) {')
      ->line('                HV* hash = (HV*)SvRV(result);')
      ->line('                SV** status_sv = hv_fetch(hash, "status", 6, 0);')
      ->line('                SV** headers_sv = hv_fetch(hash, "headers", 7, 0);')
      ->line('                SV** body_sv = hv_fetch(hash, "body", 4, 0);')
      ->line('                if (status_sv) status = (int)SvIV(*status_sv);')
      ->line('                if (body_sv) body_str = SvPV(*body_sv, len);')
      ->line('                else { body_str = ""; len = 0; }')
      ->line('                if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {')
      ->line('                    custom_headers = (HV*)SvRV(*headers_sv);')
      ->line('                    SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);')
      ->line('                    if (ct_sv && SvOK(*ct_sv)) {')

lib/Hypersonic.pm  view on Meta::CPAN

=item $room->has($ws)

Check if a connection is in the room.

=item $room->count

Get number of connections in the room.

=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(
        c_helpers => sub {

lib/Hypersonic.pm  view on Meta::CPAN

    $f->done(@values);
    $f->fail($message, $category);

    # State checks
    $f->is_ready;     # True if done, failed, or cancelled
    $f->is_done;      # True if resolved with values
    $f->is_failed;    # True if rejected
    $f->is_cancelled; # True if cancelled

    # Get results
    my @values = $f->result;     # Returns result values
    my ($msg, $cat) = $f->failure;  # Returns error info

    # Chaining
    $f->then(sub { ... })
      ->catch(sub { ... })
      ->finally(sub { ... });

    # Callbacks
    $f->on_done(sub { my @vals = @_; ... });
    $f->on_fail(sub { my ($msg, $cat) = @_; ... });
    $f->on_ready(sub { ... });  # Called for any completion

    # Convergent futures
    Hypersonic::Future->needs_all($f1, $f2, $f3);  # All must succeed
    Hypersonic::Future->needs_any($f1, $f2);       # First success wins
    Hypersonic::Future->wait_all($f1, $f2, $f3);   # Wait for all (success or fail)
    Hypersonic::Future->wait_any($f1, $f2);        # Wait for first completion

See L<Hypersonic::Future> and L<Hypersonic::Future::Pool> for full documentation.

=head2 compile

    $server->compile();

Compile all registered routes into JIT'd native code. This:

=over 4

=item 1. Executes static handlers once to get response strings

=item 2. Analyzes which features each route needs (JIT philosophy)

=item 3. Generates C code with responses as static constants

=item 4. Generates dynamic handler caller with only needed parsing

=item 5. Compiles via XS::JIT

=back

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" });
    });



( run in 2.116 seconds using v1.01-cache-2.11-cpan-140bd7fdf52 )