Skip to content

Commit 0aafc93

Browse files
kblokclaude
andauthored
Rename Extensions to RxExtensions, clarify FromEventBuffered's bufferSize risk (#2)
Extensions read too generically for a class this specific to RxSharp's own combinators. RxExtensions in the existing RxSharp.Extras namespace keeps the same one-name-for-the- whole-surface benefit without the genericness. Also expanded the bufferSize doc comment on FromEventBuffered: the default of 1 is a real footgun for a caller filtering for a specific match among payloads that could plausibly arrive in the pre-subscribe gap this method exists to cover - a matching payload can be silently evicted by a later non-matching one before anyone subscribes. Found via a puppeteer-sharp code review on the two call sites doing exactly that (WaitForTargetAsync/WaitForFrameAsync), which are being fixed to pass a larger bufferSize as a follow-up in that repo. Claude-Session: https://claude.ai/code/session_01K92eapPm8e7mX7puT4cF4s Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 3d81516 commit 0aafc93

11 files changed

Lines changed: 47 additions & 33 deletions

‎src/RxSharp/Extras/AssumeNeverEmits.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
namespace RxSharp.Extras;
44

55
/// <summary>Extension methods widening an error-only <see cref="Observable{T}"/> to another element type.</summary>
6-
public static partial class Extensions
6+
public static partial class RxExtensions
77
{
88
/// <summary>
99
/// Widens an <see cref="Observable{Unit}"/> that only ever calls <see cref="IObserver{T}.OnError"/> (such as
10-
/// <see cref="Extensions.FromCancellationToken"/> or <see cref="Extensions.Timeout"/>) so it
10+
/// <see cref="RxExtensions.FromCancellationToken"/> or <see cref="RxExtensions.Timeout"/>) so it
1111
/// type-checks anywhere an <see cref="Observable{T}"/> is expected - most commonly as one of the branches
1212
/// passed to <see cref="RaceOperator.RaceWith{T}"/> alongside a source that actually produces
1313
/// <typeparamref name="TResult"/> values. Mirrors how rxjs relies on TypeScript structurally accepting

‎src/RxSharp/Extras/FilterAsync.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace RxSharp.Extras;
44

55
/// <summary>Extension methods providing an async-predicate flavor of <c>Filter</c>.</summary>
6-
public static partial class Extensions
6+
public static partial class RxExtensions
77
{
88
/// <summary>
99
/// An operator supporting an async predicate, mirroring Puppeteer's own <c>filterAsync</c> helper (implemented,

‎src/RxSharp/Extras/FromCancellationToken.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace RxSharp.Extras;
22

33
/// <summary>Puppeteer-flavored combinators built on top of the core primitives — the C# analogues of the helpers Puppeteer itself layers on top of rxjs (see CLAUDE.md's "Puppeteer-essential surface").</summary>
4-
public static partial class Extensions
4+
public static partial class RxExtensions
55
{
66
/// <summary>
77
/// An observable that never emits and errors as soon as <paramref name="cancellationToken"/> is cancelled.

‎src/RxSharp/Extras/FromEventBuffered.cs‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace RxSharp.Extras;
44

55
/// <summary>
66
/// A handle to an eagerly-attached, buffered .NET event source created by
7-
/// <see cref="Extensions.FromEventBuffered{TDelegate, TEventArgs}"/>. Disposing detaches the underlying event handler.
7+
/// <see cref="RxExtensions.FromEventBuffered{TDelegate, TEventArgs}"/>. Disposing detaches the underlying event handler.
88
/// </summary>
99
/// <typeparam name="TEventArgs">The type of the event's payload.</typeparam>
1010
public sealed class BufferedEventSource<TEventArgs> : IDisposable
@@ -42,7 +42,7 @@ public void Dispose()
4242
}
4343

4444
/// <summary>Extension methods providing an eagerly-attached, buffered variant of <see cref="Observable.FromEvent{TEventArgs}"/>.</summary>
45-
public static partial class Extensions
45+
public static partial class RxExtensions
4646
{
4747
/// <summary>
4848
/// Like <see cref="Observable.FromEvent{TDelegate, TEventArgs}"/>, but attaches the underlying event handler
@@ -68,7 +68,14 @@ public static partial class Extensions
6868
/// <param name="addHandler">Called immediately, with the handler to add to the event.</param>
6969
/// <param name="removeHandler">Called on <see cref="BufferedEventSource{TEventArgs}.Dispose"/>, with the same handler, to remove it from the event.</param>
7070
/// <param name="conversion">Converts an <see cref="Action{TEventArgs}"/> callback into the event's actual delegate shape.</param>
71-
/// <param name="bufferSize">The maximum number of most-recent payloads kept for replay. Defaults to 1.</param>
71+
/// <param name="bufferSize">
72+
/// The maximum number of most-recent payloads kept for replay. Defaults to 1 - fine if the caller only
73+
/// cares about the single most recent payload, but a real risk if the caller filters for a specific match
74+
/// among possibly-several payloads that could arrive in the pre-subscribe gap this method exists to cover:
75+
/// with the default of 1, a matching payload can be silently evicted from the buffer by a later
76+
/// non-matching one before anyone subscribes to see it. Pass a larger value whenever more than one payload
77+
/// could plausibly arrive before subscription and only some of them are what the caller is looking for.
78+
/// </param>
7279
/// <returns>A handle exposing the buffered payloads as an observable, and detaching the handler on disposal.</returns>
7380
public static BufferedEventSource<TEventArgs> FromEventBuffered<TDelegate, TEventArgs>(
7481
Action<TDelegate> addHandler,
@@ -86,7 +93,14 @@ public static BufferedEventSource<TEventArgs> FromEventBuffered<TDelegate, TEven
8693
/// <typeparam name="TEventArgs">The type of the event's payload.</typeparam>
8794
/// <param name="addHandler">Called immediately, with the handler to add to the event.</param>
8895
/// <param name="removeHandler">Called on <see cref="BufferedEventSource{TEventArgs}.Dispose"/>, with the same handler, to remove it from the event.</param>
89-
/// <param name="bufferSize">The maximum number of most-recent payloads kept for replay. Defaults to 1.</param>
96+
/// <param name="bufferSize">
97+
/// The maximum number of most-recent payloads kept for replay. Defaults to 1 - fine if the caller only
98+
/// cares about the single most recent payload, but a real risk if the caller filters for a specific match
99+
/// among possibly-several payloads that could arrive in the pre-subscribe gap this method exists to cover:
100+
/// with the default of 1, a matching payload can be silently evicted from the buffer by a later
101+
/// non-matching one before anyone subscribes to see it. Pass a larger value whenever more than one payload
102+
/// could plausibly arrive before subscription and only some of them are what the caller is looking for.
103+
/// </param>
90104
/// <returns>A handle exposing the buffered payloads as an observable, and detaching the handler on disposal.</returns>
91105
public static BufferedEventSource<TEventArgs> FromEventBuffered<TEventArgs>(
92106
Action<EventHandler<TEventArgs>> addHandler,

‎src/RxSharp/Extras/RaceWithSignalAndTimer.cs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
namespace RxSharp.Extras;
44

55
/// <summary>Extension methods racing a single subscription against cancellation and a timeout.</summary>
6-
public static partial class Extensions
6+
public static partial class RxExtensions
77
{
88
/// <summary>
99
/// Races <paramref name="source"/> against cancellation and a timeout, whichever fires first. The
10-
/// non-retrying half of <see cref="Extensions.RetryAndRaceWithSignalAndTimer{T}(Observable{T}, TimeSpan, Func{Exception}, TimeSpan?, CancellationToken)"/>
10+
/// non-retrying half of <see cref="RxExtensions.RetryAndRaceWithSignalAndTimer{T}(Observable{T}, TimeSpan, Func{Exception}, TimeSpan?, CancellationToken)"/>
1111
/// - use this directly for a single wait (e.g. "wait for the next matching event") that doesn't need
1212
/// retrying, and reach for the retrying combinator when it does.
1313
/// </summary>
@@ -17,7 +17,7 @@ public static partial class Extensions
1717
/// <param name="causeFactory">
1818
/// Produces the exception used for both the cancellation and timeout branches. Defaults to
1919
/// <see cref="OperationCanceledException"/> for cancellation and <see cref="TimeoutException"/> for the timeout,
20-
/// via the defaults of <see cref="Extensions.FromCancellationToken"/> and <see cref="Extensions.Timeout"/> respectively.
20+
/// via the defaults of <see cref="RxExtensions.FromCancellationToken"/> and <see cref="RxExtensions.Timeout"/> respectively.
2121
/// Since one factory covers both branches, a caller needing to tell the two apart by exception type should
2222
/// pass <see langword="null"/> here (so each branch keeps its own distinct default type) and catch/rethrow
2323
/// as needed at the call site.
@@ -30,8 +30,8 @@ public static Observable<T> RaceWithSignalAndTimer<T>(
3030
Func<Exception>? causeFactory,
3131
CancellationToken cancellationToken)
3232
=> source.RaceWith(
33-
Extensions.FromCancellationToken(cancellationToken, causeFactory).AssumeNeverEmits<T>(),
34-
Extensions.Timeout(timeout, causeFactory).AssumeNeverEmits<T>());
33+
RxExtensions.FromCancellationToken(cancellationToken, causeFactory).AssumeNeverEmits<T>(),
34+
RxExtensions.Timeout(timeout, causeFactory).AssumeNeverEmits<T>());
3535

3636
/// <summary>
3737
/// Overload of <see cref="RaceWithSignalAndTimer{T}(Observable{T}, TimeSpan, Func{Exception}, CancellationToken)"/>

‎src/RxSharp/Extras/RetryAndRaceWithSignalAndTimer.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace RxSharp.Extras;
44

55
/// <summary>Extension methods composing retry, cancellation, and timeout into the single combinator Puppeteer's <c>Locator</c> actions rely on.</summary>
6-
public static partial class Extensions
6+
public static partial class RxExtensions
77
{
88
/// <summary>
99
/// The combinator behind Puppeteer's <c>Locator</c> actions (click/fill/hover/wait): retry the source
@@ -13,7 +13,7 @@ public static partial class Extensions
1313
/// that may not have rendered yet") while still giving up promptly, either because the caller cancelled it
1414
/// or because it took longer than <paramref name="timeout"/> — whichever happens first. Since the cancellation
1515
/// and timeout branches are <see cref="Observable{T}"/> sources that only ever error (see
16-
/// <see cref="Extensions.FromCancellationToken"/> and <see cref="Extensions.Timeout"/>), the only way
16+
/// <see cref="RxExtensions.FromCancellationToken"/> and <see cref="RxExtensions.Timeout"/>), the only way
1717
/// this combinator produces a value is if the retried <paramref name="source"/> itself emits one before either
1818
/// branch fires.
1919
/// </summary>
@@ -23,7 +23,7 @@ public static partial class Extensions
2323
/// <param name="causeFactory">
2424
/// Produces the exception used for both the cancellation and timeout branches. Defaults to
2525
/// <see cref="OperationCanceledException"/> for cancellation and <see cref="TimeoutException"/> for the timeout,
26-
/// via the defaults of <see cref="Extensions.FromCancellationToken"/> and <see cref="Extensions.Timeout"/> respectively.
26+
/// via the defaults of <see cref="RxExtensions.FromCancellationToken"/> and <see cref="RxExtensions.Timeout"/> respectively.
2727
/// </param>
2828
/// <param name="retryDelay">The delay between retry attempts. Defaults to 50 milliseconds.</param>
2929
/// <param name="cancellationToken">A token that, when cancelled, aborts the whole operation immediately.</param>

‎src/RxSharp/Extras/Timeout.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace RxSharp.Extras;
44

55
/// <summary>Extension methods providing a standalone, <see cref="TimeoutException"/>-throwing timer observable.</summary>
6-
public static partial class Extensions
6+
public static partial class RxExtensions
77
{
88
/// <summary>
99
/// An observable that never emits and errors after <paramref name="delay"/> elapses, or never errors at all if

‎src/RxSharp/RxSharp.csproj‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
<PackageProjectUrl>https://github.com/hardkoded/ReactiveExtensions-Sharp</PackageProjectUrl>
1717
<RepositoryUrl>https://github.com/hardkoded/ReactiveExtensions-Sharp</RepositoryUrl>
1818
<PackageReadmeFile>README.md</PackageReadmeFile>
19-
<PackageVersion>0.1.2</PackageVersion>
20-
<AssemblyVersion>0.1.2.0</AssemblyVersion>
21-
<FileVersion>0.1.2.0</FileVersion>
19+
<PackageVersion>0.1.3</PackageVersion>
20+
<AssemblyVersion>0.1.3.0</AssemblyVersion>
21+
<FileVersion>0.1.3.0</FileVersion>
2222
</PropertyGroup>
2323

2424
<PropertyGroup>

‎test/RxSharp.Tests/Extras/CancellationExtrasTests.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public void ShouldErrorImmediatelyIfTokenIsAlreadyCancelled()
1212
cts.Cancel();
1313

1414
Exception? received = null;
15-
Extensions.FromCancellationToken(cts.Token).Subscribe(onError: err => received = err);
15+
RxExtensions.FromCancellationToken(cts.Token).Subscribe(onError: err => received = err);
1616

1717
Assert.That(received, Is.InstanceOf<OperationCanceledException>());
1818
}
@@ -22,7 +22,7 @@ public void ShouldErrorAsSoonAsTheTokenIsCancelled()
2222
{
2323
using var cts = new CancellationTokenSource();
2424
Exception? received = null;
25-
Extensions.FromCancellationToken(cts.Token).Subscribe(onError: err => received = err);
25+
RxExtensions.FromCancellationToken(cts.Token).Subscribe(onError: err => received = err);
2626

2727
Assert.That(received, Is.Null);
2828

@@ -36,7 +36,7 @@ public void ShouldNeverEmitAValue()
3636
{
3737
using var cts = new CancellationTokenSource();
3838
var nextCalled = false;
39-
var subscription = Extensions.FromCancellationToken(cts.Token).Subscribe(_ => nextCalled = true);
39+
var subscription = RxExtensions.FromCancellationToken(cts.Token).Subscribe(_ => nextCalled = true);
4040

4141
subscription.Dispose();
4242
cts.Cancel();
@@ -52,7 +52,7 @@ public void ShouldUseTheCustomCauseFactory()
5252
var cause = new InvalidOperationException("custom cause");
5353

5454
Exception? received = null;
55-
Extensions.FromCancellationToken(cts.Token, () => cause).Subscribe(onError: err => received = err);
55+
RxExtensions.FromCancellationToken(cts.Token, () => cause).Subscribe(onError: err => received = err);
5656

5757
Assert.That(received, Is.SameAs(cause));
5858
}

‎test/RxSharp.Tests/Extras/FromEventBufferedExtrasTests.cs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public void ShouldAttachTheHandlerImmediatelyRatherThanAtSubscribeTime()
1717
{
1818
var manager = new FakeTargetManager();
1919

20-
using var source = Extensions.FromEventBuffered<string>(
20+
using var source = RxExtensions.FromEventBuffered<string>(
2121
h => manager.TargetCreated += h,
2222
h => manager.TargetCreated -= h);
2323

@@ -35,7 +35,7 @@ public void ShouldAttachTheHandlerImmediatelyRatherThanAtSubscribeTime()
3535
public void ShouldReplayBufferedValuesToALateSubscriberThenDeliverLiveValues()
3636
{
3737
var manager = new FakeTargetManager();
38-
using var source = Extensions.FromEventBuffered<string>(
38+
using var source = RxExtensions.FromEventBuffered<string>(
3939
h => manager.TargetCreated += h,
4040
h => manager.TargetCreated -= h);
4141

@@ -52,7 +52,7 @@ public void ShouldReplayBufferedValuesToALateSubscriberThenDeliverLiveValues()
5252
public void ShouldDeliverToEveryIndependentSubscriber()
5353
{
5454
var manager = new FakeTargetManager();
55-
using var source = Extensions.FromEventBuffered<string>(
55+
using var source = RxExtensions.FromEventBuffered<string>(
5656
h => manager.TargetCreated += h,
5757
h => manager.TargetCreated -= h);
5858

@@ -71,7 +71,7 @@ public void ShouldDeliverToEveryIndependentSubscriber()
7171
public void ShouldOnlyReplayUpToTheRequestedBufferSize()
7272
{
7373
var manager = new FakeTargetManager();
74-
using var source = Extensions.FromEventBuffered<string>(
74+
using var source = RxExtensions.FromEventBuffered<string>(
7575
h => manager.TargetCreated += h,
7676
h => manager.TargetCreated -= h,
7777
bufferSize: 2);
@@ -90,7 +90,7 @@ public void ShouldOnlyReplayUpToTheRequestedBufferSize()
9090
public void ShouldDetachTheHandlerOnDispose()
9191
{
9292
var manager = new FakeTargetManager();
93-
var source = Extensions.FromEventBuffered<string>(
93+
var source = RxExtensions.FromEventBuffered<string>(
9494
h => manager.TargetCreated += h,
9595
h => manager.TargetCreated -= h);
9696

@@ -106,7 +106,7 @@ public void ShouldDetachTheHandlerOnDispose()
106106
public void ShouldBeSafeToDisposeMoreThanOnce()
107107
{
108108
var manager = new FakeTargetManager();
109-
var source = Extensions.FromEventBuffered<string>(
109+
var source = RxExtensions.FromEventBuffered<string>(
110110
h => manager.TargetCreated += h,
111111
h => manager.TargetCreated -= h);
112112

0 commit comments

Comments
 (0)