Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/fable-library-py/fable_library/mailbox_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,21 @@ def post_and_async_reply[Reply](self, build_message: Callable[[AsyncReplyChannel
"""

result: Reply | None = None
# Track whether `reply` was actually called, since the reply value itself
# may legitimately be `None` (e.g. `AsyncReplyChannel[None]` for unit).
replied = False
continuation: Continuations[Any] | None = (
None # This is the continuation for the `done` callback of the awaiting poster.
)

def check_completion() -> None:
if result is not None and continuation is not None:
if replied and continuation is not None:
continuation[0](result)

def reply_callback(res: Reply):
nonlocal result
nonlocal result, replied
result = res
replied = True
check_completion()

reply_channel: AsyncReplyChannel[Reply] = AsyncReplyChannel(reply_callback)
Expand Down
18 changes: 12 additions & 6 deletions src/fable-library-ts/MailboxProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ class MailboxQueue<Msg> {
}
}

public tryGet() {
public tryGet(): { value: Msg } | undefined {
if (this.firstAndLast) {
const value = this.firstAndLast[0].value;
if (this.firstAndLast[0].next) {
this.firstAndLast = [this.firstAndLast[0].next, this.firstAndLast[1]];
} else {
delete this.firstAndLast;
}
return value;
// Wrap the value so falsy messages (0, false, "", null, undefined/unit)
// can be distinguished from an empty queue.
return { value };
}
return void 0;
}
Expand Down Expand Up @@ -64,11 +66,11 @@ export class MailboxProcessor<Msg> {

function __processEvents<Msg>($this: MailboxProcessor<Msg>) {
if ($this.continuation) {
const value = $this.messages.tryGet();
if (value) {
const dequeued = $this.messages.tryGet();
if (dequeued !== void 0) {
const cont = $this.continuation;
delete $this.continuation;
cont(value);
cont(dequeued.value);
}
}
}
Expand Down Expand Up @@ -97,15 +99,19 @@ export function postAndAsyncReply<Reply, Msg>(
buildMessage: (c: AsyncReplyChannel<Reply>) => Msg,
) {
let result: Reply;
// Use an explicit flag because the reply value itself may be undefined
// (e.g. AsyncReplyChannel<unit>), which must still complete the async.
let replied = false;
let continuation: Continuation<Reply>;
function checkCompletion() {
if (result !== void 0 && continuation !== void 0) {
if (replied && continuation !== void 0) {
continuation(result);
}
}
const reply = {
reply: (res: Reply) => {
result = res;
replied = true;
checkCompletion();
},
};
Expand Down
16 changes: 16 additions & 0 deletions tests/Beam/MailboxProcessorTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ let ``test MailboxProcessor.postAndAsyncReply works with falsy values`` () =
equal 0 resp
} |> Async.StartImmediate

[<Fact>]
let ``test MailboxProcessor.postAndAsyncReply completes with unit reply`` () =
async {
let mutable ran = false
let agent = MailboxProcessor<AsyncReplyChannel<unit>>.Start(fun inbox ->
let rec loop () = async {
let! chan = inbox.Receive()
chan.Reply()
return! loop ()
}
loop ())
do! agent.PostAndAsyncReply(fun ch -> ch)
ran <- true
equal true ran
} |> Async.StartImmediate

// Message ordering

[<Fact>]
Expand Down
34 changes: 34 additions & 0 deletions tests/Js/Main/AsyncTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,40 @@ let tests =
equal 0 resp
}

testCaseAsync "MailboxProcessor delivers falsy messages in order" <| fun () -> // See #1588
async {
let mutable collected = []
let agent = MailboxProcessor<int>.Start(fun inbox ->
let rec loop () = async {
let! msg = inbox.Receive()
if msg = -1 then ()
else
collected <- msg :: collected
return! loop ()
}
loop ())
for msg in [1; 0; 2; 0; 3] do
agent.Post(msg)
agent.Post(-1)
do! Async.Sleep 200
equal [1; 0; 2; 0; 3] (List.rev collected)
}

testCaseAsync "MailboxProcessor.postAndAsyncReply completes with unit reply" <| fun () ->
async {
let mutable ran = false
let agent = MailboxProcessor<AsyncReplyChannel<unit>>.Start(fun inbox ->
let rec loop () = async {
let! chan = inbox.Receive()
chan.Reply()
return! loop ()
}
loop ())
do! agent.PostAndAsyncReply(fun ch -> ch)
ran <- true
equal true ran
}

testCase "Async try .. with returns correctly from 'with' branch" <| fun () ->
let work = async {
try
Expand Down
16 changes: 16 additions & 0 deletions tests/Python/TestMailboxProcessor.fs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ let ``test MailboxProcessor.postAndAsyncReply works with falsy values`` () =
equal 0 resp
} |> Async.StartImmediate

[<Fact>]
let ``test MailboxProcessor.postAndAsyncReply completes with unit reply`` () =
async {
let mutable ran = false
let agent = MailboxProcessor<AsyncReplyChannel<unit>>.Start(fun inbox ->
let rec loop () = async {
let! chan = inbox.Receive()
chan.Reply()
return! loop ()
}
loop ())
do! agent.PostAndAsyncReply(fun ch -> ch)
ran <- true
equal true ran
} |> Async.StartImmediate

// Message ordering

[<Fact>]
Expand Down
Loading