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;
async sub fetch_user {
my ($self, $id) = @_;
my $row = await $self->db->query("SELECT ...", $id);
my $perms = await $self->perms->for($id);
return { %$row, perms => $perms };
}
# Caller MUST hold the returned Future:
my $f = $client->fetch_user(42);
my $user = await $f; # ok
# OR:
my $user = await $client->fetch_user(42); # also ok â await holds it
```
**Pitfalls:**
- **Returning a Future from `async sub` without `await`** double-wraps it. Final expression `return await $f`, not `return $f`.
- **`@_` is not preserved across `await` on Perl < 5.24.** Unpack args into lexicals at the top of the sub.
- **`await` inside `map`/`grep` does not work.** Convert to a `for` loop with an accumulator.
- **Caller drops the Future** â "Suspended async sub ⦠lost its returning future". The async sub's continuation is destroyed mid-flight. Always store the Future or `await` it.
- The async sub itself does not retain its returning Future. **Storing the Future in the calling object is the cure**, not adding `->retain` inside the async sub.
---
## Pattern 7 â Timeouts
```perl
# delay_future returns a Future that resolves with no value after N seconds
my $timer = $self->loop->delay_future(after => 5);
# Race the work against the timer
my $result = await Future->wait_any(
$work_future,
$timer->then_fail('timeout'),
);
```
( run in 3.959 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )