Selkie.git | t/ | 75-store-async-drain.rakutest


use Test;
use lib 'lib';

use Selkie::Store;

# register-fx('async') must track every Promise it spawns so
# App.shutdown can drain in-flight workers before tearing notcurses
# down. Without this, a worker thread completing post-stop dispatches
# into handlers whose native deps are gone — segfault or NPE.

plan 4;

subtest "async fx returns success via dispatch" => {
    plan 1;
    my $s = Selkie::Store.new;
    my $seen;
    $s.register-handler('done', -> $store, %p { $seen = %p<result>; () });
    $s.register-handler('start', -> $store, %p {
        ('async' => {
            work       => -> { 42 },
            on-success => 'done',
        }),
    });
    $s.dispatch('start');
    $s.tick;
    $s.drain-async(:timeout(2));
    # The on-success dispatch was queued — tick again to deliver it.
    $s.tick;
    is $seen, 42, "async work completed and dispatched on-success";
};

subtest "drain-async waits for in-flight work to complete" => {
    plan 2;
    my $s = Selkie::Store.new;
    my $delivered;
    $s.register-handler('done', -> $store, %p { $delivered = True; () });
    $s.register-handler('start', -> $store, %p {
        ('async' => {
            work       => -> { sleep 0.3; 'ok' },
            on-success => 'done',
        }),
    });

    my $t0 = now;
    $s.dispatch('start');
    $s.tick;
    $s.drain-async(:timeout(5));
    my $elapsed = now - $t0;

    ok $elapsed >= 0.25, "drain blocked until work finished ({$elapsed.fmt('%.2f')}s)";
    # Work completed before drain returned, so flushing one more tick
    # delivers the on-success dispatch.
    $s.tick;
    ok $delivered, "on-success was delivered before drain returned";
};

subtest "drain-async respects timeout when work hangs" => {
    plan 1;
    my $s = Selkie::Store.new;
    $s.register-handler('start', -> $store, %p {
        ('async' => {
            work => -> { sleep 5 },
        }),
    });
    $s.dispatch('start');
    $s.tick;

    my $t0 = now;
    $s.drain-async(:timeout(0.5));
    my $elapsed = now - $t0;
    ok $elapsed < 1.5,
        "drain returned within timeout window ({$elapsed.fmt('%.2f')}s)";
};

subtest "shutting-down flag prevents new async dispatches" => {
    plan 2;
    my $s = Selkie::Store.new;
    $s.drain-async;
    ok $s.shutting-down, "shutting-down flag set after drain";

    my $fired = False;
    $s.register-handler('start', -> $store, %p {
        ('async' => {
            work       => -> { $fired = True },
        }),
    });
    $s.dispatch('start');
    $s.tick;
    # The async fx should have no-op'd because we're shutting down.
    sleep 0.05;
    nok $fired, "post-shutdown async dispatch was suppressed";
};