Hypersonic

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

	- MAJOR: Hypersonic compile() now derives the JIT module id
	  from a Digest::MD5 hash of the generated C source instead
	  of int(rand(100000)). 
	- t/1007-future-pool-chaining.t should no longer flake on slow
	  smokers 
	- t/2012..t/2017 (UA async future / run-poll / callback /
	  start-request / state-machine / helpers) all converted to
	  HypersonicTest::spawn_server + wait_for_port.

0.17    2026-05-26
	- Fix CPAN-tester bailouts on t/0035-e2e-streaming.t (perl
	  5.12..5.42 on the k93msid smoker): bumped wait_for_port
	  default from 5s to 60s and t/0035 to 120s so slow debugging
	  perls have time to JIT-compile the largest test server
	  (regular + streaming + 2 SSE + 2 WebSocket routes)
	- HypersonicTest spawn_server now sets HYPERSONIC_COMPILE_DIAG
	  so the child prints a 'compiling JIT module ...' breadcrumb
	  before the slow gcc invocation; wait_for_port diag is no
	  longer empty when the timeout hits mid-compile
	- Hypersonic compile() wraps XS::JIT->compile() in
	  'no warnings "redefine"' to silence the
	  'Subroutine Hypersonic::Stream::* redefined at
	   Hypersonic.pm line 881' noise when the boot xsub installs
	  its xsubs into the same package as Hypersonic/Stream.pm

Changes  view on Meta::CPAN

	- Fix Hypersonic::SSE call_method() return handling: SvTRUE(POPs)
	- Bump XS::JIT to 0.22

0.11    2026-01-31
	- Fix can_link() to properly pass headers to Devel::CheckLib
	- Fix can_link() test to work reliably with compiler builtins (libm sin)
	- Add missing C includes to UA.pm for socket/networking types
	- Attempt a portable strcasestr implementation for non-GNU systems
	- Fix compilation failures on Linux (missing headers, GNU extensions)
	- Fix WebSocket g_websocket_handlers not declared when no dynamic routes
	- Fix WebSocket requires Stream module (set needs_streaming for WebSocket)
	- Fix WebSocket test to handle frame arriving with HTTP handshake

0.10    2026-01-29
	- RequestID Middleware
	- More C89 compliance refactoring

0.09    2026-01-29
	- Fix io_uring backend to check for liburing library (not just headers)
	- Add event backend ldflags to XS compile options
	- Fix test race conditions with active port probing instead of sleep()

MANIFEST  view on Meta::CPAN

t/0009-security.t
t/0010-tls.t
t/0011-request-features.t
t/0012-response.t
t/0013-request.t
t/0014-middleware.t
t/0015-static.t
t/0016-session.t
t/0017-compression.t
t/0018-http2.t
t/0019-streaming.t
t/0020-event.t
t/0021-event-role.t
t/0022-event-kqueue.t
t/0023-event-epoll.t
t/0024-event-poll.t
t/0025-event-select.t
t/0026-event-iouring.t
t/0027-chunked-encoding.t
t/0027-event-iocp.t
t/0028-event-eventports.t
t/0028-http2-streaming.t
t/0029-sse.t
t/0030-websocket-handshake.t
t/0031-websocket-framing.t
t/0032-websocket-api.t
t/0035-e2e-streaming.t
t/0040-server-reliability.t
t/1001-future-basic.t
t/1002-future-convergent.t
t/1003-future-pool-basic.t
t/1004-future-pool-submit.t
t/1005-future-pool-concurrent.t
t/1006-future-pool-errors.t
t/1007-future-pool-chaining.t
t/1008-future-pool-shutdown.t
t/1009-future-data-structures.t
t/2001-ua-foundation.t
t/2002-ua-http11.t
t/2003-ua-response.t
t/2004-ua-streaming.t
t/2005-ua-tls.t
t/2006-ua-pool.t
t/2007-ua-http2.t
t/2008-ua-sse.t
t/2009-ua-websocket.t
t/2010-ua-async-api.t
t/2011-ua-blocking-methods.t
t/2012-ua-async-future.t
t/2013-ua-run-poll.t
t/2014-ua-callback.t

lib/Hypersonic.pm  view on Meta::CPAN


    # 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};
        }
    }

lib/Hypersonic.pm  view on Meta::CPAN

    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;

lib/Hypersonic.pm  view on Meta::CPAN

        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}}) {

lib/Hypersonic.pm  view on Meta::CPAN

            # 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['"]*\}/;

lib/Hypersonic.pm  view on Meta::CPAN

            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}) {

lib/Hypersonic.pm  view on Meta::CPAN

    #
    # 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,

lib/Hypersonic.pm  view on Meta::CPAN

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

lib/Hypersonic.pm  view on Meta::CPAN

    # 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} // '') . "')"

lib/Hypersonic.pm  view on Meta::CPAN

        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;

lib/Hypersonic.pm  view on Meta::CPAN

            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) {

lib/Hypersonic.pm  view on Meta::CPAN

        
        $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;

lib/Hypersonic.pm  view on Meta::CPAN

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

lib/Hypersonic.pm  view on Meta::CPAN

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

lib/Hypersonic.pm  view on Meta::CPAN

    }

    # 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

lib/Hypersonic.pm  view on Meta::CPAN

        $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"')

lib/Hypersonic.pm  view on Meta::CPAN

            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).

lib/Hypersonic.pm  view on Meta::CPAN


        $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

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

lib/Hypersonic.pm  view on Meta::CPAN

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]>

lib/Hypersonic/Protocol/HTTP1.pm  view on Meta::CPAN

    return $text{$code} // 'Unknown';
}

# Get status text (class method for external use)
sub status_text {
    my ($class, $code) = @_;
    return _status_text($code);
}

# ============================================================
# Chunked Transfer Encoding (HTTP/1.1 streaming)
# ============================================================

# Generate C code for chunked response headers
sub gen_chunked_start {
    my ($class, $builder) = @_;
    
    $builder->comment('Send HTTP/1.1 headers with chunked encoding')
      ->line('static void send_chunked_headers(int fd, int status, const char* content_type) {')
      ->line('    char headers[2048];')
      ->line('    const char* status_str = "OK";')

lib/Hypersonic/Protocol/HTTP2.pm  view on Meta::CPAN

      ->line('}')
      ->blank;
    
    return $builder;
}

# ============================================================
# HTTP/2 Streaming Support (Phase 3)
# ============================================================

# Generate streaming headers without END_STREAM
sub gen_stream_headers {
    my ($class, $builder) = @_;
    
    $builder->comment('HTTP/2 Streaming: Send HEADERS without END_STREAM (allows more DATA)')
      ->line('static int h2_stream_headers(nghttp2_session* session, int32_t stream_id,')
      ->line('                              int status, const char* content_type) {')
      ->line('    char status_str[4];')
      ->line('    snprintf(status_str, sizeof(status_str), "%d", status);')
      ->line('    ')
      ->line('    nghttp2_nv hdrs[] = {')

lib/Hypersonic/Protocol/HTTP2.pm  view on Meta::CPAN

      ->line('                                     stream_id, NULL, hdrs, 2, NULL);')
      ->line('    if (rv < 0) return rv;')
      ->line('    ')
      ->line('    return nghttp2_session_send(session);')
      ->line('}')
      ->blank;
    
    return $builder;
}

# Generate streaming data chunk sender
sub gen_stream_data {
    my ($class, $builder) = @_;
    
    $builder->comment('HTTP/2 Streaming: Chunk provider for streaming DATA frames')
      ->line('typedef struct {')
      ->line('    const uint8_t* data;')
      ->line('    size_t length;')
      ->line('    size_t pos;')
      ->line('} H2ChunkProvider;')
      ->blank
      ->line('static ssize_t h2_chunk_read_cb(nghttp2_session* session,')
      ->line('                                 int32_t stream_id,')
      ->line('                                 uint8_t* buf, size_t length,')
      ->line('                                 uint32_t* data_flags,')

lib/Hypersonic/Protocol/HTTP2.pm  view on Meta::CPAN

      ->line('    int32_t conn_window = nghttp2_session_get_remote_window_size(session);')
      ->line('    int32_t stream_window = nghttp2_session_get_stream_remote_window_size(')
      ->line('        session, stream_id);')
      ->line('    return conn_window < stream_window ? conn_window : stream_window;')
      ->line('}')
      ->blank;
    
    return $builder;
}

# Generate XS wrappers for HTTP/2 streaming from Perl
sub gen_stream_xs_wrappers {
    my ($class, $builder) = @_;
    
    $builder->comment('XS wrappers for HTTP/2 streaming from Perl');
    
    # h2_stream_start(session_ptr, stream_id, status, content_type)
    $builder->xs_function('hypersonic_h2_stream_start')
      ->xs_preamble
      ->check_items(4, 4, 'session_ptr, stream_id, status, content_type')
      ->line('nghttp2_session* session = (nghttp2_session*)SvUV(ST(0));')
      ->line('int32_t stream_id = (int32_t)SvIV(ST(1));')
      ->line('int status = (int)SvIV(ST(2));')
      ->line('STRLEN ct_len;')
      ->line('const char* content_type = SvPV(ST(3), ct_len);')

lib/Hypersonic/Protocol/HTTP2.pm  view on Meta::CPAN

      ->line('nghttp2_session* session = (nghttp2_session*)SvUV(ST(0));')
      ->line('int32_t stream_id = (int32_t)SvIV(ST(1));')
      ->line('int rv = h2_stream_end(session, stream_id);')
      ->line('XSRETURN_IV(rv);')
      ->xs_end
      ->blank;
    
    return $builder;
}

# Generate all HTTP/2 streaming code
sub generate_streaming {
    my ($class, $builder, $opts) = @_;
    
    $class->gen_stream_headers($builder);
    $class->gen_stream_data($builder);
    $class->gen_stream_end($builder);
    $class->gen_flow_control($builder);
    $class->gen_stream_xs_wrappers($builder);
    
    return $builder;
}

lib/Hypersonic/SSE.pm  view on Meta::CPAN

package Hypersonic::SSE;
use strict;
use warnings;
use 5.010;

# Hypersonic::SSE - High-level Server-Sent Events API
#
# Wraps the streaming infrastructure to provide a clean SSE interface.
# Automatically handles headers, event formatting, and keepalives.
# Uses JIT-compiled XS for performance.

our $VERSION = '0.19';

use constant {
    STATE_INIT     => 0,
    STATE_STARTED  => 1,
    STATE_FINISHED => 2,
};
use constant MAX_SSE_INSTANCES => 65536;
use constant DEFAULT_KEEPALIVE => 30;

use Hypersonic::Protocol::SSE;

=head1 NAME

Hypersonic::SSE - Server-Sent Events streaming interface

=head1 SYNOPSIS

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

        my $sse = Hypersonic::SSE->new($stream);

        $sse->event(
            type => 'message',
            data => 'Hello World!',
        );

        $sse->event(
            type => 'update',
            data => '{"count": 42}',
            id   => '123',
        );

        $sse->close();
    }, { streaming => 1 });

=head1 DESCRIPTION

Hypersonic::SSE provides a high-level API for sending Server-Sent Events.
It wraps a Hypersonic::Stream object and handles SSE-specific formatting,
headers, and keepalives.

=cut

# ============================================================

lib/Hypersonic/SSE.pm  view on Meta::CPAN

        
        for my $id (($last_id + 1) .. 100) {
            $sse->event(
                type => 'update',
                data => "Event $id",
                id   => $id,
            );
        }
        
        $sse->close();
    }, { streaming => 1 });

=head1 SEE ALSO

L<Hypersonic::Stream>, L<Hypersonic::Protocol::SSE>

=head1 AUTHOR

Hypersonic Contributors

=cut

lib/Hypersonic/Stream.pm  view on Meta::CPAN

our $VERSION = '0.19';

use constant {
    STATE_INIT     => 0,
    STATE_STARTED  => 1,
    STATE_FINISHED => 2,
    STATE_ABORTED  => 3,
};
use constant MAX_STREAMS => 65536;

# Class method for streaming handler detection (only Perl code needed)
sub is_streaming_handler {
    my ($class, $handler, $opts) = @_;
    return 1 if $opts && $opts->{streaming};
    my $proto = prototype($handler);
    return 1 if defined $proto && $proto =~ /stream/i;
    eval {
        require B::Deparse;
        my $deparser = B::Deparse->new('-p', '-sC');
        my $code = $deparser->coderef2text($handler);
        return 1 if $code =~ /\$stream\s*->/;
    };
    return 0;
}

lib/Hypersonic/Stream.pm  view on Meta::CPAN


sub gen_xs_headers {
    my ($class, $builder) = @_;
    
    $builder->xs_function('xs_stream_headers')
      ->xs_preamble
      ->line('int fd = SvIV(SvRV(ST(0)));')
      ->line('StreamState* s = &stream_registry[fd];')
      ->blank
      ->if('s->state != STREAM_STATE_INIT')
        ->line('croak("Cannot set headers after streaming started");')
      ->endif
      ->blank
      ->if('items >= 2')
        ->line('s->status = SvIV(ST(1));')
      ->endif
      ->blank
      ->line('s->extra_headers[0] = \'\\0\';')
      ->if('items >= 3 && SvROK(ST(2)) && SvTYPE(SvRV(ST(2))) == SVt_PVHV')
        ->line('HV* hv = (HV*)SvRV(ST(2));')
        ->line('int extra_pos = 0;')

lib/Hypersonic/Stream.pm  view on Meta::CPAN

    
    $builder->xs_function('xs_stream_content_type')
      ->xs_preamble
      ->if('items != 2')
        ->line('croak("Usage: $stream->content_type(type)");')
      ->endif
      ->line('int fd = SvIV(SvRV(ST(0)));')
      ->line('StreamState* s = &stream_registry[fd];')
      ->blank
      ->if('s->state != STREAM_STATE_INIT')
        ->line('croak("Cannot set content_type after streaming started");')
      ->endif
      ->blank
      ->line('STRLEN len;')
      ->line('const char* ct = SvPV(ST(1), len);')
      ->if('len < sizeof(s->content_type)')
        ->line('memcpy(s->content_type, ct, len);')
        ->line('s->content_type[len] = \'\\0\';')
      ->endif
      ->blank
      ->line('ST(0) = ST(0);')

t/0019-streaming.t  view on Meta::CPAN

    
    my $stream = Hypersonic::Stream->new(fd => 200);
    
    is($stream->state, 0, 'state starts at INIT');
    ok(!$stream->is_finished, 'not finished initially');
};

# ============================================================
# Test 8-10: Streaming handler detection
# ============================================================
subtest 'is_streaming_handler with explicit flag' => sub {
    plan tests => 2;
    
    my $handler = sub { };
    
    ok(
        Hypersonic::Stream->is_streaming_handler($handler, { streaming => 1 }),
        'explicit streaming => 1 detected'
    );
    
    ok(
        !Hypersonic::Stream->is_streaming_handler($handler, { streaming => 0 }),
        'explicit streaming => 0 not detected'
    );
};

subtest 'is_streaming_handler with no options' => sub {
    plan tests => 1;
    
    my $handler = sub { my ($req) = @_; return { status => 200 }; };
    
    ok(
        !Hypersonic::Stream->is_streaming_handler($handler, {}),
        'regular handler not detected as streaming'
    );
};

subtest 'is_streaming_handler with code analysis' => sub {
    plan tests => 1;
    
    # Handler that uses $stream->
    my $handler = sub {
        my ($req, $stream) = @_;
        $stream->write("test");
    };
    
    # Note: code analysis may or may not detect this depending on B::Deparse
    ok(1, 'code analysis attempted');
};

# ============================================================
# Test 11-14: Route registration with streaming flag
# ============================================================
subtest 'Route with streaming flag' => sub {
    plan tests => 4;
    
    my $app = Hypersonic->new();
    
    $app->get('/stream' => sub {
        my ($req, $stream) = @_;
        $stream->write("test");
        $stream->end();
    }, { streaming => 1 });
    
    my $route = $app->{routes}[0];
    
    ok($route->{streaming}, 'route has streaming flag');
    ok($route->{dynamic}, 'streaming route is dynamic');
    is($route->{features}{streaming}, 1, 'features has streaming');
    is($route->{path}, '/stream', 'path correct');
};

subtest 'Route without streaming flag' => sub {
    plan tests => 2;
    
    my $app = Hypersonic->new();
    
    $app->get('/normal' => sub {
        return { status => 200, body => 'hello' };
    });
    
    my $route = $app->{routes}[0];
    
    ok(!$route->{streaming}, 'regular route has no streaming flag');
    is($route->{features}{streaming}, 0, 'features has streaming = 0');
};

subtest 'Route analysis detects streaming' => sub {
    plan tests => 2;
    
    my $app = Hypersonic->new();
    
    $app->get('/stream' => sub {
        my ($req, $stream) = @_;
        $stream->write("data");
        $stream->end();
    }, { streaming => 1 });
    
    my %analysis = (needs_streaming => 0);
    for my $route (@{$app->{routes}}) {
        if ($route->{features}{streaming}) {
            $analysis{needs_streaming} = 1;
        }
    }
    
    ok($analysis{needs_streaming}, 'analysis detects streaming routes');
    
    # Non-streaming app
    my $app2 = Hypersonic->new();
    $app2->get('/normal' => sub { return 'hello' });
    
    %analysis = (needs_streaming => 0);
    for my $route (@{$app2->{routes}}) {
        if ($route->{features}{streaming}) {
            $analysis{needs_streaming} = 1;
        }
    }
    
    ok(!$analysis{needs_streaming}, 'analysis does not detect streaming for normal routes');
};

subtest 'Multiple routes mixed streaming' => sub {
    plan tests => 3;
    
    my $app = Hypersonic->new();
    
    $app->get('/normal' => sub { return 'hello' });
    $app->get('/stream' => sub { }, { streaming => 1 });
    $app->post('/data' => sub { }, { dynamic => 1 });
    
    my @streaming = grep { $_->{streaming} } @{$app->{routes}};
    my @dynamic = grep { $_->{dynamic} } @{$app->{routes}};
    
    is(scalar(@streaming), 1, 'one streaming route');
    is(scalar(@dynamic), 2, 'two dynamic routes (streaming is dynamic)');
    is($app->{routes}[1]{streaming}, 1, 'correct route is streaming');
};

# ============================================================
# Test 15: Code generation produces valid C
# ============================================================
subtest 'Stream code generation' => sub {
    plan tests => 6;
    
    my $builder = XS::JIT::Builder->new;
    

t/0019-streaming.t  view on Meta::CPAN

    like($code, qr/STREAM_STATE_STARTED/, 'defines STREAM_STATE_STARTED');
    like($code, qr/STREAM_STATE_FINISHED/, 'defines STREAM_STATE_FINISHED');
    like($code, qr/StreamState/, 'defines StreamState struct');
    like($code, qr/stream_registry/, 'has stream registry');
    like($code, qr/xs_stream_new/, 'has xs_stream_new');
};

# ============================================================
# Test 16-18: Hypersonic integration
# ============================================================
subtest 'Hypersonic compile with streaming route' => sub {
    plan tests => 2;
    
    my $app = Hypersonic->new();
    
    $app->get('/events' => sub {
        my ($req, $stream) = @_;
        $stream->write("event: test\n");
        $stream->end();
    }, { streaming => 1 });
    
    $app->get('/' => sub { return 'hello' });
    
    eval { $app->compile() };
    
    ok(!$@, 'compile succeeds with streaming route') or diag($@);
    ok($app->{route_analysis}{needs_streaming}, 'analysis has needs_streaming flag');
};

subtest 'Hypersonic compile without streaming route' => sub {
    plan tests => 2;
    
    my $app = Hypersonic->new();
    
    $app->get('/' => sub { return 'hello' });
    $app->get('/about' => sub { return 'about' });
    
    eval { $app->compile() };
    
    ok(!$@, 'compile succeeds without streaming route') or diag($@);
    ok(!$app->{route_analysis}{needs_streaming}, 'analysis has no needs_streaming flag');
};

subtest 'Feature flags in analysis' => sub {
    plan tests => 3;
    
    my $app = Hypersonic->new();
    
    $app->get('/stream' => sub { }, { streaming => 1 });
    $app->get('/json' => sub { }, { dynamic => 1, parse_json => 1 });
    
    my %analysis = (
        needs_streaming => 0,
        needs_json => 0,
    );
    
    for my $route (@{$app->{routes}}) {
        my $f = $route->{features} // {};
        $analysis{needs_streaming} = 1 if $f->{streaming};
        $analysis{needs_json} = 1 if $f->{parse_json};
    }
    
    ok($analysis{needs_streaming}, 'streaming detected');
    ok($analysis{needs_json}, 'json detected');
    
    # Verify they're independent
    my $app2 = Hypersonic->new();
    $app2->get('/stream' => sub { }, { streaming => 1 });
    
    %analysis = (needs_streaming => 0, needs_json => 0);
    for my $route (@{$app2->{routes}}) {
        my $f = $route->{features} // {};
        $analysis{needs_streaming} = 1 if $f->{streaming};
        $analysis{needs_json} = 1 if $f->{parse_json};
    }
    
    ok(!$analysis{needs_json}, 'json not detected when not used');
};

done_testing();

t/0027-chunked-encoding.t  view on Meta::CPAN

    
    require XS::JIT::Builder;
    my $builder = XS::JIT::Builder->new;
    
    Hypersonic::Protocol::HTTP1->gen_chunked_start($builder);
    my $code = $builder->code;
    
    like($code, qr/void send_chunked_headers\(/, 'function defined');
    like($code, qr/Transfer-Encoding: chunked/, 'chunked header included');
    like($code, qr/Content-Type: %s/, 'content-type placeholder');
    like($code, qr/Connection: keep-alive/, 'keep-alive for streaming');
    like($code, qr/send\(fd, headers/, 'sends headers');
};

subtest 'gen_chunked_write uses writev' => sub {
    plan tests => 4;
    
    require XS::JIT::Builder;
    my $builder = XS::JIT::Builder->new;
    
    Hypersonic::Protocol::HTTP1->gen_chunked_write($builder);

t/0027-chunked-encoding.t  view on Meta::CPAN

    my $final = Hypersonic::Protocol::HTTP1->build_final_chunk();
    
    # "0" CRLF CRLF (we don't send trailers)
    is($final, "0\r\n\r\n", 'final chunk correct');
    is(length($final), 5, 'final chunk is 5 bytes');
};

# ============================================================
# Test 14-15: Integration with Hypersonic
# ============================================================
subtest 'Hypersonic compiles streaming routes' => sub {
    plan tests => 3;
    
    my $app = Hypersonic->new();
    
    $app->get('/events' => sub {
        my ($req, $stream) = @_;
        $stream->content_type('text/event-stream');
        $stream->write("data: hello\n\n");
        $stream->end();
    }, { streaming => 1 });
    
    $app->get('/' => sub { return 'hello' });
    
    eval { $app->compile() };
    ok(!$@, 'compile succeeds') or diag($@);
    
    ok($app->{route_analysis}{needs_streaming}, 'streaming detected');
    ok($app->{compiled}, 'app is compiled');
};

subtest 'Generated code includes chunked functions' => sub {
    plan tests => 3;
    
    require XS::JIT::Builder;
    my $builder = XS::JIT::Builder->new;
    
    # Generate all HTTP1 chunked code

t/0028-http2-streaming.t  view on Meta::CPAN

}

# ============================================================
# Test 4: Check nghttp2 availability
# ============================================================
my $has_nghttp2 = Hypersonic::Protocol::HTTP2->check_nghttp2();
ok(defined $has_nghttp2, 'nghttp2 detection works');
diag($has_nghttp2 ? 'nghttp2 found' : 'nghttp2 not available');

# ============================================================
# Test 5-9: JIT code generation for HTTP/2 streaming
# ============================================================
SKIP: {
    skip 'nghttp2 not available', 5 unless $has_nghttp2;
    
    require XS::JIT::Builder;
    
    subtest 'gen_stream_headers generates correct code' => sub {
        plan tests => 4;
        
        my $builder = XS::JIT::Builder->new;

t/0028-http2-streaming.t  view on Meta::CPAN

};

subtest 'Stream defaults to http1' => sub {
    plan tests => 1;
    
    my $stream = Hypersonic::Stream->new(fd => 1);
    is($stream->protocol, 'http1', 'default protocol is http1');
};

# ============================================================
# Test 13-15: generate_streaming method
# ============================================================
SKIP: {
    skip 'nghttp2 not available', 3 unless $has_nghttp2;
    
    subtest 'generate_streaming generates all functions' => sub {
        plan tests => 5;
        
        require XS::JIT::Builder;
        my $builder = XS::JIT::Builder->new;
        
        Hypersonic::Protocol::HTTP2->generate_streaming($builder);
        my $code = $builder->code;
        
        like($code, qr/h2_stream_headers/, 'has h2_stream_headers');
        like($code, qr/h2_stream_data/, 'has h2_stream_data');
        like($code, qr/h2_stream_end/, 'has h2_stream_end');
        like($code, qr/h2_can_send/, 'has h2_can_send');
        like($code, qr/hypersonic_h2_stream/, 'has XS wrappers');
    };
    
    subtest 'Stream.generate_c_code with http2 option' => sub {

t/0028-http2-streaming.t  view on Meta::CPAN


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

        Hypersonic::Stream->generate_c_code($builder, {
            max_connections => 100,
            http2           => 1,
        });
        my $code = $builder->code;

        # HTTP/1.1 streaming code (always present)
        like($code, qr/stream_start|stream_write_chunk/, 'has HTTP/1.1 streaming code');

        # Stream registry tracks http2 flag for future HTTP/2 support
        like($code, qr/int http2/, 'has http2 flag in StreamState');
    };
    
    subtest 'Stream.generate_c_code without http2 option' => sub {
        plan tests => 2;
        
        require XS::JIT::Builder;
        my $builder = XS::JIT::Builder->new;
        
        Hypersonic::Stream->generate_c_code($builder, {
            max_connections => 100,
            http2           => 0,
        });
        my $code = $builder->code;
        
        # HTTP/1.1 streaming code
        like($code, qr/stream_start|stream_write_chunk/, 'has HTTP/1.1 streaming code');
        
        # No HTTP/2 streaming code
        unlike($code, qr/h2_stream_headers/, 'no HTTP/2 streaming code');
    };
}

# ============================================================
# Test 16-18: Integration with Hypersonic
# ============================================================
SKIP: {
    skip 'nghttp2 not available', 3 unless $has_nghttp2;
    
    # Check for TLS test certificates
    my $cert_file = 't/certs/server.crt';
    my $key_file = 't/certs/server.key';
    my $has_certs = (-f $cert_file && -f $key_file);
    
    SKIP: {
        skip 'TLS certificates not found', 1 unless $has_certs;
        
        subtest 'Hypersonic compile with http2 and streaming' => sub {
            plan tests => 3;
            
            my $app = Hypersonic->new(
                http2    => 1,
                tls      => 1,
                cert     => $cert_file,
                key      => $key_file,
            );
            
            $app->get('/events' => sub {
                my ($req, $stream) = @_;
                $stream->write("data: test\n\n");
                $stream->end();
            }, { streaming => 1 });
            
            $app->get('/' => sub { return { status => 200, body => 'hello' } });
            
            eval { $app->compile() };
            ok(!$@, 'compile succeeds') or diag($@);
            ok($app->{http2}, 'http2 enabled');
            ok($app->{route_analysis}{needs_streaming}, 'streaming detected');
        };
    }
    
    SKIP: {
        skip 'TLS certificates not found', 1 unless $has_certs;
        
        subtest 'Hypersonic compile with http2, no streaming' => sub {
            plan tests => 3;
            
            my $app = Hypersonic->new(
                http2    => 1,
                tls      => 1,
                cert     => $cert_file,
                key      => $key_file,
            );
            
            $app->get('/' => sub { return { status => 200, body => 'hello' } });
            
            eval { $app->compile() };
            ok(!$@, 'compile succeeds') or diag($@);
            ok($app->{http2}, 'http2 enabled');
            ok(!$app->{route_analysis}{needs_streaming}, 'no streaming detected');
        };
    }
    
    subtest 'Hypersonic without http2 still compiles streaming' => sub {
        plan tests => 3;
        
        my $app = Hypersonic->new(http2 => 0);
        
        $app->get('/stream' => sub {
            my ($req, $stream) = @_;
            $stream->write("chunk");
            $stream->end();
        }, { streaming => 1 });
        
        $app->get('/' => sub { return 'hello' });
        
        eval { $app->compile() };
        ok(!$@, 'compile succeeds') or diag($@);
        ok(!$app->{http2}, 'http2 disabled');
        ok($app->{route_analysis}{needs_streaming}, 'streaming detected');
    };
}

done_testing();

t/0035-e2e-streaming.t  view on Meta::CPAN

    $server->get('/' => sub { 'OK' });

    # Streaming route - sends multiple chunks
    $server->get('/stream' => sub {
        my ($req, $stream) = @_;
        $stream->headers(200, { 'Content-Type' => 'text/plain' });
        $stream->write("chunk1\n");
        $stream->write("chunk2\n");
        $stream->write("chunk3\n");
        $stream->end();
    }, { streaming => 1 });

    # SSE route - sends server-sent events
    $server->get('/sse' => sub {
        my ($req, $stream) = @_;
        require Hypersonic::SSE;
        my $sse = Hypersonic::SSE->new($stream);
        $sse->event(type => 'greeting', data => 'Hello SSE!');
        $sse->event(type => 'update', data => 'First update', id => '1');
        $sse->event(type => 'update', data => "Multi\nLine\nData", id => '2');
        $sse->data('simple data');
        $sse->comment('test comment');
        $sse->retry(5000);
        $sse->close();
    }, { streaming => 1 });

    # SSE with keepalive test
    $server->get('/sse-keepalive' => sub {
        my ($req, $stream) = @_;
        require Hypersonic::SSE;
        my $sse = Hypersonic::SSE->new($stream, keepalive => 1);
        $sse->event(data => 'start');
        $sse->keepalive();
        $sse->event(data => 'end');
        $sse->close();
    }, { streaming => 1 });

    # WebSocket echo route
    $server->websocket('/ws-echo' => sub {
        my ($ws) = @_;
        $ws->on(message => sub {
            my ($data) = @_;
            $ws->send("echo: $data");
        });
        $ws->on(close => sub {
            # Connection closed

t/0035-e2e-streaming.t  view on Meta::CPAN

        $ws->on(open => sub {
            $ws->send("Welcome!");
        });
    });

    $server->compile();
    $server->run(port => $port, workers => 1);
});

# Parent - wait for server to start. This test compiles the largest
# JIT module of the suite (regular + streaming + 2 SSE routes + 2
# websocket routes); on smokers with -O0 -g debugging perls the gcc
# invocation alone can take 30-60s, which is why earlier 5s/10s
# timeouts produced the "child wrote no output" bailouts on CPAN
# testers (host k93msid, perl 5.12 .. 5.42).
wait_for_port($port, { pid => $pid, log => $log, tries => 600, sleep => 0.2 })
    or BAIL_OUT("server child failed to bind port $port (see diag above)");

# ============================================================
# Test helpers
# ============================================================

t/0035-e2e-streaming.t  view on Meta::CPAN

        my $buf;
        my $bytes = sysread($sock, $buf, 4096);
        last unless $bytes;
        $response .= $buf;
    }

    close($sock);
    return $response;
}

# Read streaming response incrementally
sub http_streaming_request {
    my ($method, $path, %opts) = @_;
    my $timeout = $opts{timeout} // 5;
    my $headers = $opts{headers} // {};

    my $sock = IO::Socket::INET->new(
        PeerAddr => '127.0.0.1',
        PeerPort => $port,
        Proto    => 'tcp',
        Timeout  => $timeout,
    );

t/0035-e2e-streaming.t  view on Meta::CPAN

    like($resp, qr/200 OK/, 'Returns 200');
};

# ============================================================
# Test 2: Streaming response with multiple chunks
# ============================================================
subtest 'Streaming response (chunked)' => sub {
    plan tests => 6;

    my $resp = http_request('GET', '/stream');
    ok($resp, 'Got streaming response');
    like($resp, qr/HTTP\/1\.1 200 OK/, 'Status 200');
    like($resp, qr/Transfer-Encoding: chunked/i, 'Chunked encoding header');
    like($resp, qr/chunk1/, 'Contains chunk1');
    like($resp, qr/chunk2/, 'Contains chunk2');
    like($resp, qr/chunk3/, 'Contains chunk3');
};

# ============================================================
# Test 3: SSE response format
# ============================================================

t/2103-ua-minimal-compile.t  view on Meta::CPAN


        $server->compile();
        $server->run(port => $PORT, workers => 1);
    });

    # tries => 50 was 10s, which timed out on the ANDK k93msid
    # DEBUGGING perl 5.24.0 smoker (Hypersonic 0.18 report
    # 404e86a8) with `Child server still alive but not listening on
    # port 34861`. Raise to 600 (120s) and let HypersonicTest's
    # 60s floor + PERL_TEST_TIME_OUT_FACTOR scaling handle the rest.
    # This is the same budget t/0035-e2e-streaming.t uses.
    wait_for_port($PORT, { pid => $server_pid, log => $server_log, tries => 600, sleep => 0.2 })
        or die "Server failed to start";
    return 1;
}

sub stop_test_server {
    if ($server_pid) {
        kill('TERM', $server_pid);
        waitpid($server_pid, 0);
    }

t/lib/HypersonicTest.pm  view on Meta::CPAN

# wait_for_port($port [, $opts]) -> 1 / 0
#
# Probes 127.0.0.1:$port until something accepts or we give up. On
# timeout, if $opts->{log} is given, diag()s the child's captured
# output; if $opts->{pid} is given, diag()s the child's exit status.
sub wait_for_port {
    my ($port, $opts) = @_;
    $opts //= {};
    # Default: 60 seconds (300 tries x 0.2s). The JIT compile of a
    # full Hypersonic server (all event backends + TLS + HTTP/2 +
    # WebSocket + SSE + streaming) can take >30s on a debugging-perl
    # smoke host (gcc -O0 -g), and on older perls/hosts even longer.
    # The previous 5s default caused t/0035-e2e-streaming.t bailouts
    # in CPAN tester reports on the k93msid host for perl 5.12..5.42.
    my $max_tries = $opts->{tries} // 300;
    my $sleep     = $opts->{sleep} // 0.2;

    # Enforce a MINIMUM total wait of 60 wallclock seconds regardless
    # of what the caller asked for. Most test files in this dist pass
    # `tries => 50` which (at 0.2s/try) is only 10s; that's nowhere
    # near enough for the JIT compile on a slow/DEBUGGING smoker.
    # Also scale by PERL_TEST_TIME_OUT_FACTOR (some smokers set this
    # to 3 to indicate "I'm a slow box, give tests 3x the budget").



( run in 0.490 second using v1.01-cache-2.11-cpan-140bd7fdf52 )