Spring Integration Channel works for first time, then does not the second time - spring

When using Spring Integration and Channel to another integration flow, it only works the first time.
Then after that it skips over the channel and returns.
From first integration flow:
.handle((p, h) -> {
System.out.println("Payload Before Channel" + p.toString());
return p;
})
.channel(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.getChannelName())
.handle((p, h) -> {
System.out.println("Payload After Channel" + p.toString());
return p;
})
Then on the next integration flow:
#Bean
public IntegrationFlow jamsSubmitJob() {
return IntegrationFlows.from(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.getChannelName())
.handle((p, h) -> {
try {
jamsToken = authMang.getJamsAuth().getTokenWithTokenType();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
JAMS_SUBMIT_JOB_INTGRTN
.info(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.toString() + " Integration called.");
JAMS_SUBMIT_JOB_INTGRTN.info(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.toString() + " Headers:= " + h);
JAMS_SUBMIT_JOB_INTGRTN.debug(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.toString() + " Payload:= " + p);
return p;
})
.handle((p, h) -> {
// hail mary to get new token
return MessageBuilder
.withPayload(p)
.removeHeaders("*")
.setHeader(HttpHeaders.AUTHORIZATION.toLowerCase(), jamsToken)
.setHeader(HttpHeaders.CONTENT_TYPE.toLowerCase(), "application/json")
.build();
})
.handle((p, h) -> {
JAMS_SUBMIT_JOB_INTGRTN
.info(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.toString() + " Submitting payload to JAMS:");
JAMS_SUBMIT_JOB_INTGRTN.info(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.toString() + " Headers:= " + h);
JAMS_SUBMIT_JOB_INTGRTN.info(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.toString() + " Payload:= " + p);
return p;
})
.handle(Http.outboundGateway(JAMS_SUBMIT_ENDPOINT)
.requestFactory(alliantPooledHttpConnection.get_httpComponentsClientHttpRequestFactory())
.httpMethod(HttpMethod.POST)
.expectedResponseType(String.class)
.extractPayload(true))
.logAndReply();
}
The behavior is that every other message gets through, basically skipping over the channel until the next time around. Strangely if I duplicate the jamsSubmitJob Bean, then it will work twice, then fail, then start over again.
Thanks!

This is a misunderstanding what is channel and what is a subscriber to that channel.
So, you have this:
.channel(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.getChannelName())
.handle((p, h) -> {
This way you declare a channel (if that does not exist in the application context yet) and subscriber to it.
Then you have this:
return IntegrationFlows.from(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.getChannelName())
.handle((p, h) -> {
This way you declare a channel (if that does not exist in the application context yet) and subscriber to it.
Did you notice a duplication in my explanation? I did that deliberately to take your attention to the problem.
So, if channel does not exist it is created. In the other place an existing object is used. In the end you end up with two subscribers to the same channel. The framework by default creates for us an instance of a DirectChannel, which come with a round-robin dispatching strategy. That means that the firsts message is going to a first subscriber, the second - to second, the third to the first and so on.
What you want is probably a request-reply pattern, and you better look into the .gateway(IntegrationNamesEnum.JAMS_SUBMIT_JOB_INTGRTN.getChannelName()) instead of .channel() in that your first IntegrationFlow.
See more info in docs:
https://docs.spring.io/spring-integration/docs/current/reference/html/core.html#channel-implementations-directchannel
https://docs.spring.io/spring-integration/docs/current/reference/html/dsl.html#java-dsl-channels
https://docs.spring.io/spring-integration/docs/current/reference/html/dsl.html#java-dsl-gateway

Related

How to return response immediate to client in spring flux by controlling the no of thread using ExecutorService and CompletableFuture?

I needed to call two downstream systems parallelly with non-blocking io from Spring flux-based my rest service API. But the first downstream system capacity is 10 requests at a time and the second downstream system is 100.
The first downstream system out is input to the second downstream system so I can make a more parallel request to the second system to expedite the process.
The second downstream system response is very large so unable to hold in memory to concrete all the response So immediate want to return the response to the client.
Ex workflow:
Sample Code:
#GetMapping(path = "/stream", produces = MediaType.APPLICATION_STREAM_JSON_VALUE)
public Flux<String> getstream() {
ExecutorService executor = Executors.newFixedThreadPool(10);
List<CompletableFuture> list = new ArrayList<>();
AtomicInteger ai = new AtomicInteger(1);
RestTemplate restTemplate = new RestTemplate();
for (int i = 0; i < 100; i++) {
CompletableFuture<Object> cff = CompletableFuture.supplyAsync(
() -> ai.getAndAdd(1) + " first downstream web service " +
restTemplate.getForObject("http://dummy.restapiexample.com/api/v1/employee/" + ai.get(), String.class)
).thenApplyAsync(v -> {
Random r = new Random();
Integer in = r.nextInt(1000);
return v + " second downstream web service " + in + " " + restTemplate.getForObject("http://dummy.restapiexample.com/api/v1/employee/" + ai.get() + 1, String.class) + " \n";
}, executor);
list.add(cff);
}
return Flux.fromStream(list.stream().map(m -> {
try {
return m.get().toString();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return "";
})
);
}
This code only working for the first five threads after I am getting a response all threads completed the process. But I needed to get a response immediately to the client once I am getting the response from the second downstream system.
Note: The above code is not implemented with a second level thread pool.
Thank you in advance.
If you're building non-blocking system using Spring-Webflux it's better to utilise capabilities of WebClient in your example. I've created a simple test application where the below code snippet worked for me:
private final WebClient w = WebClient.create("http://localhost:8080/call"); // web client for external system
#GetMapping(path = "/stream", produces = MediaType.APPLICATION_STREAM_JSON_VALUE)
public Flux<MyClass> getstream() {
return Flux
.range(0, 100) // prepare initial 100 requests
.window(10) // combine elements in batch of 10 (probably buffer will fit better, have a look)
// .delayElements(Duration.ofSeconds(5)) for testing purpose you can use this function as well
.doOnNext(flow -> log.info("Batch of 10 is ready")) // double check tells that batch is ready
.flatMap(flow -> flow
// perform an external async call for each element in batch of 10
// they will be executed sequentially but there will not be any performance issues because
// calls are async. If you wish you can add .parallel() to the flow to make it parallel
.flatMap(element -> w.get().exchange())
.map(r -> r.bodyToMono(MyClass.class))
)
// subscribe to each response and throw received element further to the stream
.flatMap(response -> Mono.create(s -> response.subscribe(s::success)))
.window(1000) // batch of 1000 is ready
.flatMap(flow -> flow
.flatMap(element -> w.get().exchange())
.map(r -> r.bodyToMono(MyClass.class))
)
.flatMap(response -> Mono.create(s -> response.subscribe(s::success)));
}
public static class MyClass {
public Integer i;
}
UPDATE:
I've prepared a small application to reproduce your case. You can find it in my repository.

CompletableFuture in case of if statment

I have 2 CompletableFutures and 3 methods that have to occur each after the other.
However, in some cases the second method isn't necessary.
CompletableFuture<Boolean> cf1 = supplyASync.(() ->( doSomething1()));
CompletableFuture<Boolean> cf2 = new CompletableFuture<Boolean>();
if(someThing){
cf2 = cf1.thenApplyAsync(previousResult -> doSomething2());
}
else{
cf2 = cf1;
}
if (SomeThing2) {
cf2.thenApplyAsync(previousResult ->doSomeThing3());
}
Bassicly what I'm trying to do is that doSomething2 will run after doSomething1 (if needed), but anyway that doSomeThing3 will execute after the first or both of them.
Is it the right way to do it ? Or is there better way ?
The code is being executed in sequence, that is first completable future 1, then completable future 2 (if applies) and finally task 3.
By this reason, you can use a single CompletableFuture:
CompletableFuture<Boolean> cf = CompletableFuture.supplyAsync(() -> {
if (doSomething1()) {
doSomething2();
}
doSomething3();
// return something
});
And handling the boolean results would be something like:
CompletableFuture<Boolean> cf = CompletableFuture.supplyAsync(() -> {
boolean result;
if (result = doSomething1()) {
result = doSomething2();
}
return result && doSomething3();
});
// handle future as you need, e.g. cf.get() to wait the future to end (after task 3)

[InvalidStateError: "setRemoteDescription needs to called before addIceCandidate" code: 11

I create a simple video calling app by using web Rtc and websockets.
But when i run the code, the following error occured.
DOMException [InvalidStateError: "setRemoteDescription needs to called before addIceCandidate"
code: 11
I don't know how to resolve this error.
Here is my code below:
enter code here
var localVideo;
var remoteVideo;
var peerConnection;
var uuid;
var localStream;
var peerConnectionConfig = {
'iceServers': [
{'urls': 'stun:stun.services.mozilla.com'},
{'urls': 'stun:stun.l.google.com:19302'},
]
};
function pageReady() {
uuid = uuid();
console.log('Inside Page Ready');
localVideo = document.getElementById('localVideo');
remoteVideo = document.getElementById('remoteVideo');
serverConnection = new WebSocket('wss://' + window.location.hostname +
':8443');
serverConnection.onmessage = gotMessageFromServer;
var constraints = {
video: true,
audio: true,
};
if(navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia(constraints)
.then(getUserMediaSuccess).catch(errorHandler);
}else
{
alert('Your browser does not support getUserMedia API');
}
}
function getUserMediaSuccess(stream) {
localStream = stream;
localVideo.src = window.URL.createObjectURL(stream);
}
function start(isCaller) {
console.log('Inside isCaller');
peerConnection = new RTCPeerConnection(peerConnectionConfig);
peerConnection.onicecandidate = gotIceCandidate;
peerConnection.onaddstream = gotRemoteStream;
peerConnection.addStream(localStream);
if(isCaller) {
console.log('Inside Caller to create offer');
peerConnection.createOffer().
then(createdDescription).catch(errorHandler);
}
}
function gotMessageFromServer(message) {
console.log('Message from Server');
if(!peerConnection)
{
console.log('Inside !Peer Conn');
start(false);
}
var signal = JSON.parse(message.data);
// Ignore messages from ourself
if(signal.uuid == uuid) return;
if(signal.sdp) {
console.log('Inside SDP');
peerConnection.setRemoteDescription(new
RTCSessionDescription(signal.sdp)).then(function() {
// Only create answers in response to offers
if(signal.sdp.type == 'offer') {
console.log('Before Create Answer');
peerConnection.createAnswer().then(createdDescription)
.catch(errorHandler);
}
}).catch(errorHandler);
} else if(signal.ice) {
console.log('Inside Signal Ice');
peerConnection.addIceCandidate(new
RTCIceCandidate(signal.ice)).catch(errorHandler);
}
}
function gotIceCandidate(event) {
console.log('Inside Got Ice Candi');
if(event.candidate != null) {
serverConnection.send(JSON.stringify({'ice': event.candidate,
'uuid': uuid}));
}
}
function createdDescription(description) {
console.log('got description');
peerConnection.setLocalDescription(description).then(function() {
console.log('Inside Setting ');
serverConnection.send(JSON.stringify({'sdp':
peerConnection.localDescription, 'uuid': uuid}));
}).catch(errorHandler);
}
function gotRemoteStream(event) {
console.log('got remote stream');
remoteVideo.src = window.URL.createObjectURL(event.stream);
}
function errorHandler(error) {
console.log(error);
}
// Taken from http://stackoverflow.com/a/105074/515584
// Strictly speaking, it's not a real UUID, but it gets the job done here
function uuid() {
function s4() {
return Math.floor((1 + Math.random()) *
0x10000).toString(16).substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() +
s4() + s4();
}
This is my code, I don't know how to arrange the addIceCandidate and addRemoteDescription function.
You need to make sure that
peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice))
is called after description is set.
You have sitution where you receive ice candidate and try to add it to peerConnection before peerConnection has completed with setting description.
I had similar situation, and I created array for storing candidates that arrived before setting description is completed, and a variable that checks if description is set. If description is set, I would add candidates to peerConnection, otherwise I would add them to array. (when you set your variable to true, you can also go through array and add all stored candidates to peerConnection.
The way WebRTC works (as much as i understand) is you have to make two peers to have a deal to how to communicate eachother in the order of give an offer to your peer get your peers answer and select an ICE candidate to communicate on then if you want to send your media streams for an video conversation
for you to have a good exampe to look on how to implemet those funcitons and in which order you can visit https://github.com/alexan1/SignalRTC he has a good understading of how to do this.
you might already have a solution to your problem at this time but im replying in case you do not.
EDIT: As I have been told, this solution is an anti-pattern and you should NOT implement it this way. For more info on how I solved it while still keeping a reasonable flow, follow this answer and comment section: https://stackoverflow.com/a/57257449/779483
TLDR: Instead of calling addIceCandidate as soon as the signaling information arrives, add the candidates to a queue. After calling setRemoteDescription, go through candidates queue and call addIceCandidate on each one.
--
From this answer I learned that we have to call setRemoteDescription(offer) before we add the Ice Candidates data.
So, expanding on #Luxior answer, I did the following:
When signaling message with candidate arrives:
Check if remote was set (via a boolean flag, ie: remoteIsReady)
If it was, call addIceCandidate
If it wasn't, add to a queue
After setRemoteDescription is called (in answer signal or answer client action):
Call a method to go through the candidates queue and call addIceCandidate on each one.
Set boolean flag (remoteIsReady) to true
Empty queue

Chaining several CompletionStage only if a condition is achieved

I have several CompletionStage methods that I'd like to chain. The problem is that the result of the first one will determine if the next ones should be executed. Right now the only way to achieve this seems to be passing "special" arguments to next CompletionStage so it doesn't execute the full code. For example:
public enum SomeResult {
RESULT_1,
RESULT_2,
RESULT_3
}
public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {
return CompletableFuture.supplyAsync(() -> {
// loooooong operation
if (someCondition)
return validValue;
else
return null;
}).thenCompose(result -> {
if (result != null)
return someMethodThatReturnsACompletionStage(result);
else
return CompletableFuture.completedFuture(null);
}).thenApply(result -> {
if (result == null)
return ChainingResult.RESULT_1;
else if (result.someCondition())
return ChainingResult.RESULT_2;
else
return ChainingResult.RESULT_3;
});
}
Since the whole code depends on the first someCondition (if it's false then the result will be RESULT_1, if not then the whole code should be executed) this construction looks a bit ugly to me. Is there any way to decide if 2nd (thenCompose(...)) and 3rd (thenApply(...)) methods should be executed?
You can do it like this:
public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {
CompletableFuture<SomeResult> shortCut = new CompletableFuture<>();
CompletableFuture<ResultOfFirstOp> withChain = new CompletableFuture<>();
CompletableFuture.runAsync(() -> {
// loooooong operation
if (someCondition)
withChain.complete(validValue);
else
shortCut.complete(SomeResult.RESULT_1);
});
return withChain
.thenCompose(result -> someMethodThatReturnsACompletionStage(result))
.thenApply(result ->
result.someCondition()? SomeResult.RESULT_2: SomeResult.RESULT_3)
.applyToEither(shortCut, Function.identity());
}
Instead of one CompletableFuture we create two, representing the different execution paths we might take. The loooooong operation is submitted as runnable then and will deliberately complete one of these CompletableFuture. The followup stages are chained to the stage representing the fulfilled condition, then both execution paths join at the last applyToEither(shortCut, Function.identity()) step.
The shortCut future has already the type of the final result and will be completed with the RESULT_1, the result of your nullpassing path, which will cause the immediate completion of the entire operation. If you don’t like the dependency between the first stage and the actual result value of the short-cut, you can retract it like this:
public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {
CompletableFuture<Object> shortCut = new CompletableFuture<>();
CompletableFuture<ResultOfFirstOp> withChain = new CompletableFuture<>();
CompletableFuture.runAsync(() -> {
// loooooong operation
if (someCondition)
withChain.complete(validValue);
else
shortCut.complete(null);
});
return withChain
.thenCompose(result -> someMethodThatReturnsACompletionStage(result))
.thenApply(result ->
result.someCondition()? SomeResult.RESULT_2: SomeResult.RESULT_3)
.applyToEither(shortCut.thenApply(x -> SomeResult.RESULT_1), Function.identity());
}
If your third step wasn’t exemplary but looks exactly like shown in the question, you could merge it with the code path joining step:
public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {
CompletableFuture<ResultOfSecondOp> shortCut = new CompletableFuture<>();
CompletableFuture<ResultOfFirstOp> withChain = new CompletableFuture<>();
CompletableFuture.runAsync(() -> {
// loooooong operation
if (someCondition)
withChain.complete(validValue);
else
shortCut.complete(null);
});
return withChain
.thenCompose(result -> someMethodThatReturnsACompletionStage(result))
.applyToEither(shortCut, result -> result==null? SomeResult.RESULT_1:
result.someCondition()? SomeResult.RESULT_2: SomeResult.RESULT_3);
}
then we only skip the second step, the someMethodThatReturnsACompletionStage invocation, but that can still stand for a long chain of intermediate steps, all skipped without the need to roll out a manual skipping via nullcheck.
For the sake of completeness I'm adding a new answer
Although the solution proposed by #Holger works great it's kinda strange to me. The solution I've been using involves separating different flows in different method calls and chaining them with thenCompose:
public enum SomeResult {
RESULT_1,
RESULT_2,
RESULT_3
}
public CompletionStage<SomeResult> someMethod(SomeArgument someArgument) {
return CompletableFuture.supplyAsync(() -> {
// loooooong operation
if (someCondition)
return operateWithValidValue(value);
else
return CompletableFuture.completedValue(ChainingResult.RESULT_1);
})
.thenCompose(future -> future);
public CompletionStage<SomeResult> operateWithValidValue(... value) {
// more loooong operations...
if (someCondition)
return CompletableFuture.completedValue(SomeResult.RESULT_2);
else
return doFinalOperation(someOtherValue);
}
public CompletionStage<SomeResult> doFinalOperation(... value) {
// more loooong operations...
if (someCondition)
return CompletableFuture.completedValue(SomeResult.RESULT_2);
else
return CompletableFuture.completedValue(SomeResult.RESULT_3);
}
NOTE: I've changed the algorithm from the question in sake of a more complete answer
All long operations could be potentially wrapped inside another CompletableFuture.supplyAsync with little effort
If you have to check only for null values you can solve using Optional. For example you should do:
public Bar execute(String id) {
return this.getFooById(id)
.thenCompose(this::checkFooPresent)
.thenCompose(this::doSomethingElse)
.thenCompose(this::doSomethingElseMore)
.thenApply(rankRes -> new Bar(foo));
}
private Optional<Foo> getFooById(String id) {
// some better logic to retrieve foo
return Optional.ofNullable(foo);
}
private CompletableFuture<Foo> checkFooPresent(Optional<Foo> optRanking) {
CompletableFuture<Foo> future = new CompletableFuture();
optRanking.map(future::complete).orElseGet(() -> future.completeExceptionally(new Exception("Foo not present")));
return future;
}
The checkFooPresent() receives an Optional, and if its value is null it complete exceptionally the CompletableFuture.
Obviously you need to manage that exception, but if you have previously setted an ExceptionHandler or something similar it should come for free.

Approach to filtering and validating return values with rxjs

So here is the scenario I am attempting to figure out how to implement using rxjs:
Load some set of metadata from a file/database/etc. Each element in the metadata has an id and additional information - like the location of the actual data. Currently, I am loading all of this metadata at the start of the application, asynchronously. After this data is loaded the Observable calls complete. Eventually I may add a refresh capability
At some later point in the application, I will need to load specific sets of data based upon what is available in the metadata. I am currently attempting to do this with a function like fetchData(ids:string[]):Observable. This is where I am unclear about how to proceed under the rxjs paradigm. I am equally unsure of what to do with requesting a single item using a function like fetchDatum(id:string):Observable
I can of course use filter to operate only on those IMetdata items emitted from the IMetadata Observable that match one of the names in the list - but I also need to confirm that ALL requested items are found in the IMetadata Observable emissions, and if not I need to error.
So if someone requests the IMetadata with id = "Bob" - but there is no such IMetadata emitted from the source Observable, then it needs to error. Or if they request { "Shirley", "Rex", "Samantha" } and there is no data for "Rex" then it should error.
I've considered using a Rx.Subject here, but from what I've read that is generally undesirable under the rxjs paradigm. Please advise on what approaches would work for this scenario under the rxjs paradigm. Thanks!
Here's the solution I came up with. This function creates an Observable that relies upon a IBufferEvaluator to tell it what to do with each item that is emitted by the source Observable. It can APPEND the item to the buffer, SKIP the emitted item, CLEAR the buffer, FLUSH the buffer to the subscriber, etc. Let me know if you find a better way to do this, especially if its an out-of-the-box rxjs solution. Thanks.
import Rx from 'rxjs/Rx';
export enum BufferAction {
APPEND, /** Append the current emission to the buffer and continue **/
SKIP, /** Do nothing, ignoring the current emission if applicable **/
FLUSH, /** This will ignore the current emission, if applicable, and flush the existing buffer contents */
CLEAR, /** Clear the buffer contents. Ignore the current emission, if applicable */
COMPLETE, /** Mark the Observable as Complete. The buffer will be cleared upon completion. **/
APPEND_THEN_FLUSH, /** Append the current emission to the buffer prior to flushing it **/
APPEND_THEN_COMPLETE, /** Append the current emission to the buffer and then complete **/
CLEAR_THEN_APPEND, /** Clear the buffer contents and then append the current emission to it */
FLUSH_THEN_APPEND, /** Flush the buffer contents and then append the current emission to it */
FLUSH_THEN_COMPLETE, /** Flush the buffer contents and then mark the Observable as complete */
APPEND_FLUSH_COMPLETE /** Append the current emission, flush the buffer, and then complete */
}
export function bufferActionToString(action: BufferAction):string
{
switch(action)
{
case BufferAction.APPEND: return "APPEND";
case BufferAction.SKIP: return "SKIP";
case BufferAction.FLUSH: return "FLUSH";
case BufferAction.CLEAR: return "CLEAR";
case BufferAction.COMPLETE: return "COMPLETE";
case BufferAction.APPEND_THEN_FLUSH: return "APPEND_THEN_FLUSH";
case BufferAction.APPEND_THEN_COMPLETE: return "APPEND_THEN_COMPLETE";
case BufferAction.CLEAR_THEN_APPEND: return "CLEAR_THEN_APPEND";
case BufferAction.FLUSH_THEN_APPEND: return "FLUSH_THEN_APPEND";
case BufferAction.FLUSH_THEN_COMPLETE: return "FLUSH_THEN_COMPLETE";
case BufferAction.APPEND_FLUSH_COMPLETE: return "APPEND_FLUSH_COMPLETE";
default: return "Unrecognized Buffer Action [" + action + "]";
}
}
export interface IBufferEvaluator<T>
{
evalOnNext(next:T, buffer: T[]):BufferAction;
evalOnComplete(buffer: T[]):BufferAction;
}
/** bufferWithEval.ts
* An Operator that buffers the emissions from the source Observable. As each emission is recieved,
* it and the buffered emissions are evaluated to determine what BufferAction to APPEND. You can APPEND
* the current emission value to the end of the buffered emissions, you can FLUSH the buffered emissions
* before or after appending the current emission value, you can SKIP the current emission value and then
* (optionally) FLUSH the buffer, and you can CLEAR the buffer before or after appending the current emission.
*
* The evalOnNext and evalOnComplete are expected to return a BufferAction to indicate
* which action to take. If no evalOnNext is supplied, it will default to APPENDing each emission. The evalOnComplete
* will default to FLUSH_THEN_COMPLETE. If evalOnNext or evalOnComplete throw an exception, the Observable will emit
* the exception and cease.
*/
export function bufferWithEval<T>
( source: Rx.Observable<T>,
evaluatorFactory?: () => IBufferEvaluator<T>
) : Rx.Observable<T[]>
{
/** if no evaluatorFactory supplied, use the default evaluatorFactory **/
if(!evaluatorFactory)
{
evaluatorFactory = () => {
return {
evalOnNext : function(next: T, buffer: T[]) { return BufferAction.APPEND; },
evalOnComplete : function(buffer: T[]) { return BufferAction.FLUSH; }
};
}
}
return new Rx.Observable<T[]>((subscriber: Rx.Subscriber<T[]>) =>
{
var _buffer = new Array<T>();
var _evaluator = evaluatorFactory();
var _subscription: Rx.Subscription = null;
function append(next: T)
{
_buffer.push(next);
}
function flush()
{
try
{
subscriber.next(_buffer);
}
finally
{
// Ignore any exceptions that come from subscriber.next()
clear();
}
}
function clear()
{
_buffer = new Array<T>();
}
function next(next: T)
{
try
{
var action = _evaluator.evalOnNext(next, _buffer.slice(0));
switch(action)
{
case BufferAction.APPEND: { append(next); break; }
case BufferAction.SKIP: { break; }
case BufferAction.FLUSH: { flush(); break; }
case BufferAction.CLEAR: { clear(); break; }
case BufferAction.COMPLETE: { complete(); break; }
case BufferAction.APPEND_THEN_FLUSH: { append(next); flush(); break; }
case BufferAction.APPEND_THEN_COMPLETE: { append(next); complete(); break; }
case BufferAction.APPEND_FLUSH_COMPLETE: { append(next); flush(); complete(); break; }
case BufferAction.CLEAR_THEN_APPEND: { clear(); append(next); break; }
case BufferAction.FLUSH_THEN_APPEND: { flush(); append(next); break; }
case BufferAction.FLUSH_THEN_COMPLETE: { flush(); complete(); break; }
default: throw new Error("next(): Invalid BufferAction '" + bufferActionToString(action) + "'");
}
}
catch(e)
{
error(e);
}
}
function complete()
{
try
{
var action = _evaluator.evalOnComplete(_buffer.slice(0));
switch(action)
{
case BufferAction.FLUSH_THEN_COMPLETE:
case BufferAction.FLUSH: { flush(); }
case BufferAction.CLEAR:
case BufferAction.COMPLETE: { break; }
case BufferAction.APPEND:
case BufferAction.APPEND_THEN_FLUSH:
case BufferAction.APPEND_THEN_COMPLETE:
case BufferAction.APPEND_FLUSH_COMPLETE:
case BufferAction.SKIP:
case BufferAction.CLEAR_THEN_APPEND:
case BufferAction.FLUSH_THEN_APPEND:
default: throw new Error("complete(): Invalid BufferAction '" + bufferActionToString(action) + "'");
}
clear();
subscriber.complete();
_subscription.unsubscribe();
}
catch(e)
{
error(e);
}
}
function error(err: any)
{
try
{
subscriber.error(err);
}
finally
{
_subscription.unsubscribe();
}
}
_subscription = source.subscribe(next, error, complete);
return _subscription;
});
}

Resources