Skip to content

feat: add Actor.callAsync — request-reply returning Async<'Reply>#32

Merged
dbrattli merged 1 commit into
mainfrom
feat/actor-call-async
Jun 14, 2026
Merged

feat: add Actor.callAsync — request-reply returning Async<'Reply>#32
dbrattli merged 1 commit into
mainfrom
feat/actor-call-async

Conversation

@dbrattli

Copy link
Copy Markdown
Collaborator

Summary

Adds Actor.callAsync — a request-reply that returns Async<'Reply> so it can be let!-bound from inside an async { } context on all targets. This is the reverse companion to the rc.10 Async→ActorOp bridge:

  • rc.10 let you do! an Async inside actor { }.
  • callAsync lets you let! an actor call inside async { }.

Downstream, Fable.Reactive's mapActor operator does let! result = Actor.call actor x inside an async { } observer, which fails to compile on BEAM because call returns the CPS ActorOp, not Async. callAsync fixes that.

Implementation (src/Fable.Actor/Actor.fs)

Added immediately after call in both platform branches, mirroring the call/callWithTimeout ordering so the branches stay structurally in sync:

  • BEAM (#if FABLE_COMPILER_BEAM): wraps the CPS call in an async, runs its Run continuation, and returns the captured reply. BEAM processes block natively, so the continuation fires synchronously once the reply arrives.
  • non-BEAM (#else): ActorOp = Async, so callAsync is a direct alias for call.

Purely additive and backward-compatible — call/callWithTimeout and all existing tests are untouched.

Tests

Adds actor_call_async_test (test/ActorTest.fs, registered in Program.fs): from inside an async { } block it does let! reply = Actor.callAsync server GetCount against a counter server started with Actor.start, and asserts the reply. The server is a distinct process from the caller, so callAsync does not deadlock on BEAM.

just test — all three targets green:

  • .NET: 28/28
  • Python: 28/28
  • BEAM: 28/28

Formatted with dotnet fantomas src test (max line length 120). Changelog bump intentionally omitted (handled automatically at release).

🤖 Generated with Claude Code

Reverse companion to the rc.10 Async→ActorOp bridge: rc.10 let you do!
an Async inside actor { }; callAsync lets you let! an actor call inside
async { } on all targets.

On BEAM, call is CPS (ActorOp = { Run: ('T -> unit) -> unit }) and cannot
be let!-bound in async { }. callAsync wraps the CPS call in an async and
captures the reply — the Run continuation fires synchronously once the
reply arrives, since BEAM processes block natively. On non-BEAM targets
ActorOp = Async, so callAsync is a direct alias for call.

Purely additive; existing call/callWithTimeout and all tests unchanged.
Downstream, Fable.Reactive's mapActor operator needs this for BEAM
support (it does let! result = call inside an async { } observer).

Adds actor_call_async_test covering let! callAsync from inside async { }.
Green on .NET, Python, and BEAM (28/28 each).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dbrattli
dbrattli merged commit a1b8252 into main Jun 14, 2026
2 checks passed
@dbrattli
dbrattli deleted the feat/actor-call-async branch June 14, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant