App-Raider

 view release on metacpan or  search on metacpan

.claude/skills/perl-io-async-future/SKILL.md  view on Meta::CPAN

        service => $self->{port},
    )->on_fail(sub {
        my $err = shift;
        $self->{_connect_future}->fail("connect: $err")
            unless $self->{_connect_future}->is_ready;
    });

    $self->{_connect_future} = $self->loop->new_future;
    return await $self->{_connect_future};
}
```

**Why both futures?** `_tcp_connect_future` resolves when the *socket* is up. `_connect_future` resolves when your *protocol handshake* is done. The handshake completes inside `on_read`, so you need a separate Future to await.

Clean up `_tcp_connect_future` once the handshake succeeds (`delete $self->{_tcp_connect_future}`).

---

## Pattern 3 — Reconnect Without Losing Futures

The reconnect path is where Future-lifetime bugs cluster. Wrong:

```perl
# ❌ BUG: $f is local to the closure, GC'd as soon as _reconnect returns
sub _reconnect {
    my $self = shift;
    my $f = $self->loop->delay_future(after => 2)->then(sub { $self->connect });
}
```

Right:

```perl
sub _reconnect_attempt {
    my ($self) = @_;
    return if $self->{_connected};

    weaken(my $weak = $self);

    $self->{_reconnect_future} = $self->loop
        ->delay_future(after => $self->{reconnect_wait})
        ->then(sub {
            my $self = $weak or return Future->done;
            return Future->done if $self->{_connected};
            return $self->connect;
        })
        ->on_done(sub {
            my $self = $weak or return;
            delete $self->{_reconnect_future};
        })
        ->on_fail(sub {
            my $self = $weak or return;
            delete $self->{_reconnect_future};
            $self->_reconnect_attempt;        # try again
        });
}
```

**Rules:**
- Store the *whole chain* on the object (`$self->{_reconnect_future}`), not just the leaf.
- `weaken` `$self` inside callbacks — otherwise the chain holds the object alive forever.
- Always `delete $self->{_reconnect_future}` in both `on_done` and `on_fail`, or you'll guard out future reconnects with `return if $self->{_reconnect_future}`.
- On disconnect, **cancel the old `_connect_future`** so its async sub unwinds:
  ```perl
  if (my $f = delete $self->{_connect_future}) {
      $f->fail("disconnected: $reason") unless $f->is_ready;
  }
  ```

---

## Pattern 4 — Future Composition

```perl
# Sequential dependency (then = "after this, do that")
$f1->then(sub { do_b(@_) })           # done → done branch
   ->else(sub { recover(@_) })        # fail → recovery branch
   ->then(sub { do_c(@_) });

# Parallel, all must succeed (fails fast, cancels siblings)
my $f = Future->needs_all($fa, $fb, $fc);
my @results = await $f;

# Race — first one wins, losers are cancelled
my $f = Future->wait_any($work, $self->loop->delay_future(after => 5)
                                   ->then_fail('timeout'));

# Observe without composing (does NOT chain — return value ignored)
$f->on_done(sub { warn "done: @_" });
$f->on_fail(sub { warn "failed: $_[0]" });
```

**`then` vs `on_done`:**
- `then` returns a *new* Future; the callback returns a Future to chain. Use for control flow.
- `on_done` returns the *same* Future; callback return value is discarded. Use for side-effects/observation.

**Protective composition** (Future ≥ 0.51): pass extra siblings via `also =>` to keep them alive without making cancellation propagate:
```perl
Future->needs_all($f1, also => $f2, $f3);
```

---

## Pattern 5 — `->retain` for Fire-and-Forget

When you legitimately want to start an op and not wait, but still need it to *finish*:

```perl
$self->_log_async($msg)
    ->on_fail(sub { warn "log failed: $_[0]" })
    ->retain;
```

`->retain` parks the Future on an internal global until it completes, then drops it. Without `retain`, the chain has no holder and gets GC'd before it runs. Use this only when you genuinely don't need the result — otherwise hold the Future yourself...

---

## Pattern 6 — Future::AsyncAwait (`async`/`await`)

```perl
use Future::AsyncAwait;



( run in 1.213 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )