-
-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
270 additions
and
39 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
namespace R3; | ||
|
||
public static partial class Observable | ||
{ | ||
public static Observable<Unit> IntervalFrame(int periodFrame, CancellationToken cancellationToken = default) | ||
{ | ||
return TimerFrame(periodFrame, periodFrame, cancellationToken); | ||
} | ||
|
||
public static Observable<Unit> IntervalFrame(int periodFrame, FrameProvider frameProvider, CancellationToken cancellationToken = default) | ||
{ | ||
return TimerFrame(periodFrame, periodFrame, frameProvider, cancellationToken); | ||
} | ||
|
||
public static Observable<Unit> TimerFrame(int dueTimeFrame, CancellationToken cancellationToken = default) | ||
{ | ||
return new TimerFrame(dueTimeFrame, null, ObservableSystem.DefaultFrameProvider, cancellationToken); | ||
} | ||
|
||
public static Observable<Unit> TimerFrame(int dueTimeFrame, int periodFrame, CancellationToken cancellationToken = default) | ||
{ | ||
return new TimerFrame(dueTimeFrame, periodFrame, ObservableSystem.DefaultFrameProvider, cancellationToken); | ||
} | ||
|
||
public static Observable<Unit> TimerFrame(int dueTimeFrame, FrameProvider frameProvider, CancellationToken cancellationToken = default) | ||
{ | ||
return new TimerFrame(dueTimeFrame, null, frameProvider, cancellationToken); | ||
} | ||
|
||
public static Observable<Unit> TimerFrame(int dueTimeFrame, int periodFrame, FrameProvider frameProvider, CancellationToken cancellationToken = default) | ||
{ | ||
return new TimerFrame(dueTimeFrame, periodFrame, frameProvider, cancellationToken); | ||
} | ||
} | ||
|
||
internal sealed class TimerFrame(int dueTimeFrame, int? periodFrame, FrameProvider frameProvider, CancellationToken cancellationToken) : Observable<Unit> | ||
{ | ||
protected override IDisposable SubscribeCore(Observer<Unit> observer) | ||
{ | ||
dueTimeFrame = dueTimeFrame.Normalize(); | ||
periodFrame = periodFrame?.Normalize(); | ||
|
||
CancellableFrameRunnerWorkItemBase<Unit> runner = (periodFrame == null) | ||
? new SingleTimerFrameRunnerWorkItem(dueTimeFrame, observer, cancellationToken) | ||
: new MultiTimerFrameRunnerWorkItem(dueTimeFrame, periodFrame.Value, observer, cancellationToken); | ||
frameProvider.Register(runner); | ||
return runner; | ||
} | ||
|
||
class SingleTimerFrameRunnerWorkItem(int dueTimeFrame, Observer<Unit> observer, CancellationToken cancellationToken) | ||
: CancellableFrameRunnerWorkItemBase<Unit>(observer, cancellationToken) | ||
{ | ||
int currentFrame; | ||
|
||
protected override bool MoveNextCore() | ||
{ | ||
if (++currentFrame == dueTimeFrame) | ||
{ | ||
observer.OnNext(default); | ||
observer.OnCompleted(); | ||
Dispose(); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} | ||
|
||
class MultiTimerFrameRunnerWorkItem(int dueTimeFrame, int periodFrame, Observer<Unit> observer, CancellationToken cancellationToken) | ||
: CancellableFrameRunnerWorkItemBase<Unit>(observer, cancellationToken) | ||
{ | ||
int currentFrame; | ||
bool isPeriodPhase; | ||
|
||
protected override bool MoveNextCore() | ||
{ | ||
// initial phase | ||
if (!isPeriodPhase) | ||
{ | ||
if (++currentFrame == dueTimeFrame) | ||
{ | ||
observer.OnNext(default); | ||
isPeriodPhase = true; | ||
currentFrame = 0; | ||
} | ||
return true; | ||
} | ||
|
||
// period phase | ||
if (++currentFrame == periodFrame) | ||
{ | ||
observer.OnNext(default); | ||
currentFrame = 0; | ||
} | ||
return true; | ||
} | ||
} | ||
} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
namespace R3.Internal; | ||
|
||
// when Canceled, publish OnCompleted. | ||
internal abstract class CancellableFrameRunnerWorkItemBase<T> : IFrameRunnerWorkItem, IDisposable | ||
{ | ||
protected readonly Observer<T> observer; | ||
CancellationTokenRegistration cancellationTokenRegistration; | ||
bool isDisposed; | ||
|
||
public CancellableFrameRunnerWorkItemBase(Observer<T> observer, CancellationToken cancellationToken) | ||
{ | ||
this.observer = observer; | ||
|
||
if (cancellationToken.CanBeCanceled) | ||
{ | ||
this.cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state => | ||
{ | ||
var s = (CancellableFrameRunnerWorkItemBase<T>)state!; | ||
s.observer.OnCompleted(); | ||
s.Dispose(); | ||
}, this); | ||
} | ||
} | ||
|
||
public bool MoveNext(long frameCount) | ||
{ | ||
if (isDisposed) | ||
{ | ||
return false; | ||
} | ||
|
||
return MoveNextCore(); | ||
} | ||
|
||
protected abstract bool MoveNextCore(); | ||
|
||
public void Dispose() | ||
{ | ||
if (!isDisposed) | ||
{ | ||
isDisposed = true; | ||
cancellationTokenRegistration.Dispose(); | ||
DisposeCore(); | ||
} | ||
} | ||
|
||
protected virtual void DisposeCore() { } | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace R3.Internal; | ||
|
||
internal static class FrameCountExtensions | ||
{ | ||
// 0 is invalid, 1 is valid. | ||
public static int Normalize(this int frameCount) | ||
{ | ||
return frameCount > 0 ? frameCount : 1; | ||
} | ||
} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
namespace R3.Tests.FactoryTests; | ||
|
||
public class TimerFrameTest | ||
{ | ||
|
||
[Fact] | ||
public void TimerSingle() | ||
{ | ||
{ | ||
var fakeTime = new ManualFrameProvider(); | ||
var list = Observable.TimerFrame(0, fakeTime).ToLiveList(); | ||
fakeTime.Advance(1); | ||
list.AssertIsCompleted(); | ||
list.AssertEqual([Unit.Default]); | ||
} | ||
{ | ||
var fakeTime = new ManualFrameProvider(); | ||
var list = Observable.TimerFrame(1, fakeTime).ToLiveList(); | ||
fakeTime.Advance(1); | ||
list.AssertIsCompleted(); | ||
list.AssertEqual([Unit.Default]); | ||
} | ||
{ | ||
var fakeTime = new ManualFrameProvider(); | ||
var list = Observable.TimerFrame(2, fakeTime).ToLiveList(); | ||
fakeTime.Advance(2); | ||
list.AssertIsCompleted(); | ||
list.AssertEqual([Unit.Default]); | ||
} | ||
} | ||
|
||
[Fact] | ||
public void TimerSingle2() | ||
{ | ||
var fakeTime = new ManualFrameProvider(); | ||
|
||
var list = Observable.TimerFrame(5, fakeTime).ToLiveList(); | ||
|
||
fakeTime.Advance(4); | ||
list.AssertIsNotCompleted(); | ||
|
||
fakeTime.Advance(1); | ||
list.AssertIsCompleted(); | ||
list.AssertEqual(new[] { Unit.Default }); | ||
} | ||
|
||
[Fact] | ||
public void TimerMulti() | ||
{ | ||
var cts = new CancellationTokenSource(); | ||
var fakeTime = new ManualFrameProvider(); | ||
|
||
var list = Observable.TimerFrame(5, 8, fakeTime, cts.Token).ToLiveList(); | ||
|
||
fakeTime.Advance(4); | ||
list.AssertIsNotCompleted(); | ||
|
||
fakeTime.Advance(1); | ||
list.AssertIsNotCompleted(); | ||
list.AssertEqual([Unit.Default]); | ||
|
||
fakeTime.Advance(7); | ||
list.AssertEqual([Unit.Default]); | ||
|
||
fakeTime.Advance(1); | ||
list.AssertEqual([Unit.Default, Unit.Default]); | ||
|
||
fakeTime.Advance(8); | ||
list.AssertEqual([Unit.Default, Unit.Default, Unit.Default]); | ||
|
||
cts.Cancel(); | ||
list.AssertIsCompleted(); | ||
} | ||
|
||
[Fact] | ||
public void Interval() | ||
{ | ||
var cts = new CancellationTokenSource(); | ||
var fakeTime = new ManualFrameProvider(); | ||
|
||
var list = Observable.IntervalFrame(5, fakeTime, cts.Token).ToLiveList(); | ||
|
||
fakeTime.Advance(4); | ||
list.AssertIsNotCompleted(); | ||
|
||
fakeTime.Advance(1); | ||
list.AssertIsNotCompleted(); | ||
list.AssertEqual([Unit.Default]); | ||
|
||
fakeTime.Advance(4); | ||
list.AssertEqual([Unit.Default]); | ||
|
||
fakeTime.Advance(1); | ||
list.AssertEqual([Unit.Default, Unit.Default]); | ||
|
||
fakeTime.Advance(5); | ||
list.AssertEqual([Unit.Default, Unit.Default, Unit.Default]); | ||
|
||
cts.Cancel(); | ||
list.AssertIsCompleted(); | ||
} | ||
} |