Add AssumeNeverEmits, RaceWithSignalAndTimer, and FromEventBuffered - #1
Merged
Conversation
Three additions motivated by puppeteer-sharp's WaitForTargetAsync/WaitForFrameAsync, promoting patterns that already existed as private, duplicated workarounds: - AssumeNeverEmits: promotes the NeverReached helper RetryAndRaceWithSignalAndTimer already had privately (widening an error-only Observable<Unit> to any T, since C# has no bottom type for RaceWith to accept the way TypeScript's Observable<never> does). RetryAndRaceWithSignalAndTimer now uses the public version instead of its own copy. - RaceWithSignalAndTimer: the non-retrying half of RetryAndRaceWithSignalAndTimer, for a single wait (e.g. "wait for the next matching event") that doesn't need retrying. RetryAndRaceWithSignalAndTimer is now built on top of it (Retry().RaceWithSignalAndTimer()). - FromEventBuffered: an eagerly-attaching, buffered variant of FromEvent. Ordinary FromEvent is cold - nothing attaches to the underlying event until Subscribe is called. That's fine in upstream rxjs, where single-threaded run-to-completion semantics mean nothing can fire between attaching a handler and checking some existing state for an already-matching item. .NET has no such guarantee (event delivery can run on another thread), so that gap is a real, provable race - FromEventBuffered attaches immediately and buffers into a ReplaySubject so nothing fired in that gap is lost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K92eapPm8e7mX7puT4cF4s
Every Extras method previously lived in its own single-method static class (CancellationExtras, TimeoutExtras, FilterAsyncExtras, ...), each named after its one method - fine for the extension methods (FilterAsync, RaceWithSignalAndTimer, etc.), which already show up via dot-completion on Observable<T> with just `using RxSharp.Extras;`, but a real discoverability tax on the three factory-style methods (FromCancellationToken, Timeout, FromEventBuffered) that have no natural `this Observable<T>` receiver to hang off of: a caller had no way to guess which class held which method. All seven are now one `public static partial class PuppeteerExtras`, split across the same per-concern files as before via `partial`. (Named PuppeteerExtras rather than plain Extras: CA1724 flags a type named the same as its containing namespace, and every file's own doc comments already frame these as puppeteer-sharp's real motivation.) Two colliding private `DefaultCause` helpers (one per branch's default exception factory) got distinct names now that they're members of the same type. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K92eapPm8e7mX7puT4cF4s
PuppeteerExtras tied the name too closely to one consumer for what's meant to be a general-purpose combinator surface. Extensions in the existing RxSharp.Extras namespace reads the same way as e.g. Observable in the RxSharp namespace. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K92eapPm8e7mX7puT4cF4s
kblok
added a commit
to hardkoded/puppeteer-sharp
that referenced
this pull request
Jul 27, 2026
Bumps to RxSharp 0.1.2 (pending publish - see hardkoded/ReactiveExtensions-Sharp#1), which adds three primitives that let these two methods read much closer to upstream's merge(...).pipe(filterAsync(predicate), raceWith(...)) shape instead of the ReplaySubject + manual predicate-in-the-handler version from the previous commit: - FromEventBuffered eagerly attaches the raw event handler (instead of a plain Subject fed by a raw handler) and exposes it as a proper Observable, so predicate filtering can happen declaratively downstream via .Filter(predicate) instead of inside the handler. - RaceWithSignalAndTimer replaces the hand-built cancellation/timeout race branches in WaitForTargetAsync. - The private NeverReached helper duplicated in both Browser.cs and CdpPage.cs is gone, replaced by RxSharp's own AssumeNeverEmits. Same external behavior as before - same exception types/messages. Verified with 8 repeated reruns of the two tests that first exposed the ReplaySubject race, confirming FromEventBuffered doesn't reintroduce it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K92eapPm8e7mX7puT4cF4s
kblok
added a commit
to hardkoded/puppeteer-sharp
that referenced
this pull request
Jul 27, 2026
RxSharp's FromEventBufferedExtras/TimeoutExtras/CancellationExtras/etc are now one public static partial class PuppeteerExtras - see hardkoded/ReactiveExtensions-Sharp#1 for why. Mechanical follow-up: this repo's two call sites. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K92eapPm8e7mX7puT4cF4s
kblok
added a commit
to hardkoded/puppeteer-sharp
that referenced
this pull request
Jul 27, 2026
PuppeteerExtras -> Extensions, see hardkoded/ReactiveExtensions-Sharp#1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K92eapPm8e7mX7puT4cF4s
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivated by puppeteer-sharp's
WaitForTargetAsync/WaitForFrameAsync— all three promote patterns that already existed as private, duplicated workarounds in consumer code.AssumeNeverEmits — promotes the
NeverReachedhelperRetryAndRaceWithSignalAndTimeralready had privately: widens an error-onlyObservable<Unit>to anyT, since C# has no bottom type forRaceWithto accept the way TypeScript'sObservable<never>does.RetryAndRaceWithSignalAndTimernow uses the public version instead of its own copy.RaceWithSignalAndTimer — the non-retrying half of
RetryAndRaceWithSignalAndTimer, for a single wait (e.g. "wait for the next matching event") that doesn't need retrying.RetryAndRaceWithSignalAndTimeris now built on top of it (Retry().RaceWithSignalAndTimer()).FromEventBuffered — an eagerly-attaching, buffered variant of
FromEvent. OrdinaryFromEventis cold — nothing attaches to the underlying event untilSubscribeis called. That's fine in upstream rxjs, where single-threaded run-to-completion semantics mean nothing can fire between attaching a handler and checking some existing state for an already-matching item. .NET has no such guarantee (event delivery can run on another thread), so that gap is a real, provable race —FromEventBufferedattaches immediately and buffers into aReplaySubjectso nothing fired in that gap is lost. (This closes a real bug found while porting puppeteer-sharp'sWaitForTargetAsync: a plainSubjectbridge dropped a matchingTargetCreatedevent that fired in exactly that gap, hanging every page creation until timeout.)Also: consolidated every Extras class (the three above plus the pre-existing
CancellationExtras/TimeoutExtras/FilterAsyncExtras/RetryAndRaceWithSignalAndTimerExtras) into a singlepublic static partial class Extensions(in the existingRxSharp.Extrasnamespace), split across the same per-concern files viapartial. Extension methods (FilterAsync,RaceWithSignalAndTimer, ...) never needed the class name typed out —using RxSharp.Extras;already surfaces them via dot-completion — but the three factory-style methods (no naturalthis Observable<T>receiver) gave callers no way to guess which class held which method. One name fixes that for zero cost to the extension methods.All 839 tests pass (825 existing + 14 new).
🤖 Generated with Claude Code
https://claude.ai/code/session_01K92eapPm8e7mX7puT4cF4s