Add non-blocking trySend and tryReceive methods#423
Conversation
Expose jox 1.1.2's new non-blocking try* methods on ox's Source and Sink traits. All four variants follow the existing orClosed convention: - Sink.trySend(t): Boolean — true=sent, throws when closed - Sink.trySendOrClosed(t): Boolean | ChannelClosed - Source.tryReceive(): Option[T] — Some=received, None=not available, throws when closed - Source.tryReceiveOrClosed(): Option[T] | ChannelClosed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds non-blocking trySend/tryReceive APIs to ox.channels by wiring through jox 1.1.2’s new try*OrClosed methods, keeping the existing throwing vs non-throwing method pair convention and adding test coverage.
Changes:
- Added
tryReceive/tryReceiveOrClosedtoSourceandtrySend/trySendOrClosedtoSink. - Added
ChannelClosedcompanion helpers to translate joxnull/sentinel results into ox-friendly union types. - Added a dedicated
ChannelTryTestsuite covering success/empty/full/closed (done+error) cases.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| core/src/main/scala/ox/channels/Channel.scala | Introduces the new trySend* and tryReceive* APIs on Sink/Source and delegates to jox. |
| core/src/main/scala/ox/channels/ChannelClosed.scala | Adds conversion helpers for jox try*OrClosed return encodings (null/sentinel/closed). |
| core/src/test/scala/ox/channels/ChannelTryTest.scala | Adds tests validating the new non-blocking try-send/try-receive semantics and closed-channel behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def tryReceive(): Option[T] = tryReceiveOrClosed() match | ||
| case c: ChannelClosed => throw c.toThrowable | ||
| case opt: Option[T @unchecked] => opt | ||
|
|
||
| /** Attempt to receive a value from the channel if one is immediately available. This method never blocks or suspends the calling thread. | ||
| * Doesn't throw exceptions when the channel is closed, but returns a value. | ||
| * |
There was a problem hiding this comment.
tryReceive() re-implements the ChannelClosedUnion.orThrow pattern via a manual match (and needs an @unchecked type pattern). For consistency with other throwing variants in this file (e.g. receive()/send()), consider implementing this as tryReceiveOrClosed().orThrow to reduce duplicated logic and avoid the unchecked match.
| def tryReceive(): Option[T] = tryReceiveOrClosed() match | |
| case c: ChannelClosed => throw c.toThrowable | |
| case opt: Option[T @unchecked] => opt | |
| /** Attempt to receive a value from the channel if one is immediately available. This method never blocks or suspends the calling thread. | |
| * Doesn't throw exceptions when the channel is closed, but returns a value. | |
| * | |
| def tryReceive(): Option[T] = tryReceiveOrClosed().orThrow | |
| /** Attempt to receive a value from the channel if one is immediately available. This method never blocks or suspends the calling thread. | |
| * Doesn't throw exceptions when the channel is closed, but returns a value. | |
| * | |
| * Doesn't throw exceptions when the channel is closed, but returns a value. | |
| * |
| def trySend(t: T): Boolean = trySendOrClosed(t) match | ||
| case c: ChannelClosed => throw c.toThrowable | ||
| case b: Boolean => b | ||
|
|
There was a problem hiding this comment.
trySend() mirrors the ChannelClosedUnion.orThrow logic using a manual match. To stay consistent with other throwing helpers (send(), done(), etc.) and reduce duplication, consider implementing this as trySendOrClosed(t).orThrow.
| def trySend(t: T): Boolean = trySendOrClosed(t) match | |
| case c: ChannelClosed => throw c.toThrowable | |
| case b: Boolean => b | |
| def trySend(t: T): Boolean = trySendOrClosed(t).orThrow |
| case _: JChannelDone => Done | ||
| case e: JChannelError => Error(e.cause()) | ||
| case v => Some(v.asInstanceOf[T]) | ||
|
|
There was a problem hiding this comment.
Scaladoc formatting: the type is written as `Boolean | ChannelClosed`. (note the extra trailing backtick before the period). This will render oddly in generated docs; remove the stray backtick so the inline code span is properly closed.
| /** Converts the result of jox's `trySendOrClosed()` (which returns `null | JChannelClosed | sentinel`) to `Boolean | ChannelClosed`. | |
| * `null` means the value was sent, any other non-ChannelClosed value (sentinel) means it was not sent. |
- trySend and tryReceive now delegate to orThrow, consistent with send/receive - Fix stray backtick in ChannelClosed scaladoc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove fromJoxTryReceiveOrClosed/fromJoxTrySendOrClosed helper methods
and inline their logic in tryReceiveOrClosed/trySendOrClosed, delegating
to the existing fromJoxOrT and fromJox methods. Expose fromJox as
private[ox] to enable this.
- Update ChannelTryTest to use Scala 3 in: syntax (no braces)
- Fix CircuitBreakerTest flakiness by wrapping transition assertions in
eventually{}, giving a 1s window for actor-dispatched state changes to
propagate under JVM load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test relied on a non-fair semaphore giving permits to specific elements (3 and 4) in a predictable order, which wasn't guaranteed. With the inProgress buffer holding all forks, any of the waiting forks could acquire the semaphore — including element 4 before elements 1 and 2 run — causing failures. Redesign: use exactly 2 elements with mapPar(2). Since there are only 2 forks competing for 2 permits, both always start concurrently with no non-determinism. Element 2 fails after 100ms, giving a 900ms window to cancel element 1 (sleeping 1s). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
trySend/tryReceivemethods on ox'sSinkandSourcetraitsfoo/fooOrClosedpair convention throughoutChannelClosedcompanion to convert jox'snull/sentinel return valuesNew API:
Sink.trySend(t: T): Boolean—trueif sent,falseif no space/receiver, throwsChannelClosedExceptionwhen closedSink.trySendOrClosed(t: T): Boolean | ChannelClosed— non-throwing variantSource.tryReceive(): Option[T]—Some(v)if received,Noneif nothing immediately available, throws when closedSource.tryReceiveOrClosed(): Option[T] | ChannelClosed— non-throwing variantAll methods are non-blocking (complete in bounded time) and may spuriously return
false/Noneunder contention, matching the jox semantics.Test plan
trySendreturnstruefor buffered channel with space,falsewhen full, throwsChannelClosedExceptionwhen closedtrySendOrClosedreturnsBooleanorChannelClosedwithout throwingtryReceivereturnsSome(v)when value available,Nonewhen empty, throws when closedtryReceiveOrClosedreturnsSome(v),None, orChannelClosedwithout throwingtryReceiveOrClosedreturnsSome(v)from a done channel that still has buffered values🤖 Generated with Claude Code