PAGI-Server

 view release on metacpan or  search on metacpan

t/http2/20-sse-transport.t  view on Meta::CPAN

        $loop->loop_once(0.02);
        my $buf = '';
        $client_sock->sysread($buf, 65536);
        $client->mem_recv($buf) if length($buf);
        my $out = $client->mem_send;
        $client_sock->syswrite($out) if length($out);
        last if $cond && $cond->();
    }
}

sub do_handshake {
    my ($client, $client_sock) = @_;
    $client->send_connection_preface;
    $client_sock->syswrite($client->mem_send);
    pump($client, $client_sock);
}

sub submit_sse_request {
    my ($client, $client_sock) = @_;
    $client->submit_request(
        method => 'GET', path => '/events', scheme => 'http', authority => 'localhost',
        headers => [['accept', 'text/event-stream']],
    );
    $client_sock->syswrite($client->mem_send);
}

subtest 'on_high_water and on_drain fire on an SSE-over-h2 stream' => sub {
    my ($hit_high, $hit_drain) = (0, 0);

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        await $receive->();

        my $t = $scope->{'pagi.transport'};
        $t->on_high_water(sub { $hit_high++ });
        $t->on_drain(sub     { $hit_drain++ });

        await $send->({ type => 'sse.start', status => 200 });
        # One 70 KB event: over the 64 KB high-water mark. The post-push poke
        # fires on_high_water before nghttp2 pulls; nghttp2 then drains the
        # per-stream queue below the 16 KB low mark, firing on_drain (deferred).
        await $send->({ type => 'sse.send', data => ('x' x (70 * 1024)) });
    };

    my ($conn, $stream_io, $client_sock, $server) = create_h2c_connection(app => $app);
    my $client = create_client;

    do_handshake($client, $client_sock);
    submit_sse_request($client, $client_sock);
    pump($client, $client_sock, sub { $hit_high && $hit_drain });

    ok($hit_high,  'on_high_water fired when the per-stream queue exceeded the high mark');
    ok($hit_drain, 'on_drain fired once nghttp2 drained the queue below the low mark');

    $stream_io->close_now;
    $loop->remove($server);
};

subtest 'SSE-over-h2 transport handle (and its $ss cycle) is collected at teardown' => sub {
    # The app weak-probes its OWN transport handle (race-free: the app always
    # runs), sends a few events, then returns. With the app coroutine complete
    # the scope is released, so the handle is held only by $ss->{transport_state}.
    # A client RST_STREAM drives _h2_on_close, which must delete that ref and
    # break the cycle -- otherwise the stream state leaks for the life of the
    # process (one per SSE request).
    my ($saw_handle, $probe);

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        await $receive->();

        my $t = $scope->{'pagi.transport'};
        $saw_handle = $t ? 1 : 0;
        weaken($probe = $t);

        await $send->({ type => 'sse.start', status => 200 });
        for my $i (1 .. 3) {
            await $send->({ type => 'sse.send', data => "event$i" });
        }
    };

    my ($conn, $stream_io, $client_sock, $server) = create_h2c_connection(app => $app);
    my $client = create_client;

    do_handshake($client, $client_sock);
    submit_sse_request($client, $client_sock);
    pump($client, $client_sock, sub { $saw_handle });

    ok($saw_handle, 'transport handle was attached to the SSE-over-h2 scope');

    # Client closes the SSE stream (stream id 1) -> server _h2_on_close.
    $client->submit_rst_stream(1, H2_CANCEL);
    $client_sock->syswrite($client->mem_send);
    pump($client, $client_sock);

    # Drive deferred teardown (loop->later) until the probe is collected.
    for (1 .. 200) {
        last unless defined $probe;
        $loop->loop_once(0.01);
    }

    is($probe, undef, 'transport handle (and its $ss cycle) collected after teardown; no leak');

    $stream_io->close_now;
    $loop->remove($server);
};

done_testing;



( run in 1.932 second using v1.01-cache-2.11-cpan-364913b4093 )