Skip to content

Commit 8c9db62

Browse files
committed
Add stream and create methods for creating non-blocking observables
1 parent 509303b commit 8c9db62

File tree

10 files changed

+391
-56
lines changed

10 files changed

+391
-56
lines changed

src/main/groovy/grails/rx/web/Rx.groovy

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import grails.web.databinding.DataBindingUtils
55
import grails.web.mapping.mvc.exceptions.CannotRedirectException
66
import groovy.transform.CompileDynamic
77
import groovy.transform.CompileStatic
8+
import org.grails.plugins.rx.web.NewObservableResult
89
import org.grails.plugins.rx.web.ObservableResult
10+
import org.grails.plugins.rx.web.StreamingNewObservableResult
911
import org.grails.plugins.rx.web.StreamingObservableResult
1012
import org.grails.plugins.rx.web.result.*
1113
import org.grails.web.converters.Converter
@@ -238,7 +240,7 @@ class Rx {
238240
}
239241

240242
/**
241-
* Return an observable with the given timeout. In the event the timeout is reached
243+
* Return an observable with the given timeout to be used with the container. In the event the timeout is reached
242244
* the containers onTimeout event handler will be invoked and an error response returned
243245
*
244246
* @param observable The observable
@@ -250,6 +252,33 @@ class Rx {
250252
return new ObservableResult<T>(observable, timeout, unit)
251253
}
252254

255+
/**
256+
* Create a new observable result for the given closure. The closure should accept an argument of type rx.Subscriber
257+
* @param callable The closure
258+
* @return The observable result
259+
*/
260+
static <T> NewObservableResult<T> create(@DelegatesTo(Subscriber) Closure<T> callable) {
261+
return new NewObservableResult<T>(callable)
262+
}
263+
264+
/**
265+
* Create a new observable result for the given closure. The closure should accept an argument of type rx.Subscriber
266+
* @param callable The closure
267+
* @return The observable result
268+
*/
269+
static <T> NewObservableResult<T> create(Long timeout, TimeUnit unit, @DelegatesTo(Subscriber) Closure<T> callable) {
270+
return new NewObservableResult<T>(callable, timeout, unit)
271+
}
272+
273+
/**
274+
* Create a new observable result for the given closure. The closure should accept an argument of type rx.Subscriber
275+
* @param callable The closure
276+
* @return The observable result
277+
*/
278+
static <T> NewObservableResult<T> create(Long timeout, @DelegatesTo(Subscriber) Closure<T> callable) {
279+
return new NewObservableResult<T>(callable, timeout)
280+
}
281+
253282
/**
254283
* Start a streaming Server-Send event response for the given observable
255284
*
@@ -276,6 +305,39 @@ class Rx {
276305
streamingObservableResult.eventName = eventName
277306
return streamingObservableResult
278307
}
308+
309+
/**
310+
* Start a streaming Server-Send event response for the given closure which is converted to an asynchronous task
311+
*
312+
* @param timeout The timeout
313+
* @param unit The timeout unit
314+
* @param callable The closure, it should accept a single argument which is the rx.Subscriber instance
315+
* @return An observable result
316+
*/
317+
static <T> StreamingNewObservableResult<T> stream(Long timeout, TimeUnit unit, @DelegatesTo(Subscriber) Closure callable) {
318+
return new StreamingNewObservableResult<T>(callable, timeout, unit)
319+
}
320+
321+
/**
322+
* Start a streaming Server-Send event response for the given closure which is converted to an asynchronous task
323+
*
324+
* @param timeout The timeout in milliseconds
325+
* @param callable The closure, it should accept a single argument which is the rx.Subscriber instance
326+
* @return An observable result
327+
*/
328+
static <T> StreamingNewObservableResult<T> stream(Long timeout, @DelegatesTo(Subscriber) Closure callable) {
329+
return new StreamingNewObservableResult<T>(callable, timeout)
330+
}
331+
332+
/**
333+
* Start a streaming Server-Send event response for the given closure which is converted to an asynchronous task
334+
*
335+
* @param callable The closure, it should accept a single argument which is the rx.Subscriber instance
336+
* @return An observable result
337+
*/
338+
static <T> StreamingNewObservableResult<T> stream(@DelegatesTo(Subscriber) Closure callable) {
339+
return new StreamingNewObservableResult<T>(callable, -1L)
340+
}
279341
/**
280342
* Executes a forward
281343
*
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.grails.plugins.rx.web
2+
3+
import groovy.transform.CompileStatic
4+
import rx.Observable
5+
import rx.Subscriber
6+
7+
import java.util.concurrent.TimeUnit
8+
9+
/**
10+
* Creates a new observable from the given closure
11+
*
12+
* @author Graeme Rocher
13+
* @since 6.0
14+
*/
15+
@CompileStatic
16+
class NewObservableResult<T> extends TimeoutResult {
17+
18+
final Closure<T> callable
19+
20+
NewObservableResult(Closure<T> callable, Long timeout = null, TimeUnit unit = TimeUnit.MILLISECONDS) {
21+
super(timeout, unit)
22+
this.callable = callable
23+
def parameterTypes = this.callable.parameterTypes
24+
boolean isSubscriber = parameterTypes.length == 1 && Subscriber.isAssignableFrom(parameterTypes[0])
25+
if(!isSubscriber) {
26+
throw new IllegalArgumentException("Passed closure must accept argument of type rx.Subscriber")
27+
}
28+
}
29+
30+
}

src/main/groovy/org/grails/plugins/rx/web/ObservableResult.groovy

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,17 @@ import java.util.concurrent.TimeUnit
1010
* @since 6.0
1111
*/
1212
@CompileStatic
13-
class ObservableResult<T> {
13+
class ObservableResult<T> extends TimeoutResult {
1414

1515
/**
1616
* The observable
1717
*/
1818
final Observable<T> observable
1919

20-
/**
21-
* The timeout, null indicates use the default container timeout
22-
*/
23-
final Long timeout
24-
25-
/**
26-
* The time unit
27-
*/
28-
final TimeUnit unit
2920

3021
ObservableResult(Observable<T> observable, Long timeout = null, TimeUnit unit = TimeUnit.MILLISECONDS) {
22+
super(timeout, unit)
3123
this.observable = observable
32-
this.timeout = timeout
33-
this.unit = unit
3424
}
3525

36-
/**
37-
* The timeout, null indicates use the default container timeout
38-
*/
39-
Long timeoutInMillis() {
40-
if(timeout != null) {
41-
return unit.toMillis(timeout)
42-
}
43-
return null
44-
}
4526
}

src/main/groovy/org/grails/plugins/rx/web/RxResultSubscriber.groovy

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.grails.web.util.GrailsApplicationAttributes
1717
import org.grails.web.util.WebUtils
1818
import org.springframework.http.HttpStatus
1919
import org.springframework.web.context.request.RequestContextHolder
20+
import org.springframework.web.context.request.async.WebAsyncUtils
2021
import rx.Subscriber
2122

2223
import javax.servlet.AsyncEvent
@@ -80,6 +81,8 @@ class RxResultSubscriber extends Subscriber implements AsyncListener {
8081
*/
8182
boolean serverSendEvents = false
8283

84+
boolean asyncComplete = false
85+
8386
/**
8487
* The server send event name
8588
*/
@@ -102,6 +105,13 @@ class RxResultSubscriber extends Subscriber implements AsyncListener {
102105

103106
@Override
104107
void onNext(Object o) {
108+
if(asyncComplete) {
109+
if( !isUnsubscribed() ) {
110+
unsubscribe()
111+
}
112+
return
113+
}
114+
105115
if(o instanceof RxResult) {
106116
// if the object emitted is an RxResult handle it accordingly
107117
if(o instanceof ForwardResult) {
@@ -135,6 +145,7 @@ class RxResultSubscriber extends Subscriber implements AsyncListener {
135145
if(isServerSendEvents()) {
136146
writer.write("\n\n")
137147
}
148+
response.flushBuffer()
138149

139150
}
140151
}
@@ -208,7 +219,7 @@ class RxResultSubscriber extends Subscriber implements AsyncListener {
208219

209220
@Override
210221
void onError(Throwable e) {
211-
if(!asyncContext.response.isCommitted()) {
222+
if(!asyncComplete && !asyncContext.response.isCommitted()) {
212223
// if an error occurred and the response has not yet been commited try and handle it
213224
def httpServletResponse = (HttpServletResponse) asyncContext.response
214225
// first check if the exception resolver and resolve a model and view
@@ -227,14 +238,21 @@ class RxResultSubscriber extends Subscriber implements AsyncListener {
227238
sendDefaultError(e, httpServletResponse)
228239
}
229240
}
230-
else {
231-
log.error("Async Dispatch Error: ${e.message}", e)
241+
else if(!asyncComplete) {
242+
if(e != null) {
243+
log.error("Async Dispatch Error: ${e.message}", e)
244+
}
245+
else {
246+
log.debug("Async timeout occurred")
247+
}
248+
asyncContext.request.removeAttribute(WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE)
232249
asyncContext.complete()
233250
}
234251
}
235252

236253
@Override
237254
void onComplete(AsyncEvent event) throws IOException {
255+
asyncComplete = true
238256
if(!isUnsubscribed()) {
239257
unsubscribe()
240258
}
@@ -245,6 +263,7 @@ class RxResultSubscriber extends Subscriber implements AsyncListener {
245263
if(!isUnsubscribed()) {
246264
unsubscribe()
247265
onError(event.throwable)
266+
asyncComplete = true
248267
}
249268
}
250269

@@ -253,6 +272,7 @@ class RxResultSubscriber extends Subscriber implements AsyncListener {
253272
if(!isUnsubscribed()) {
254273
unsubscribe()
255274
onError(event.throwable)
275+
asyncComplete = true
256276
}
257277
}
258278

src/main/groovy/org/grails/plugins/rx/web/RxResultTransformer.groovy

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.springframework.web.context.request.async.AsyncWebRequest
1616
import org.springframework.web.context.request.async.WebAsyncManager
1717
import org.springframework.web.context.request.async.WebAsyncUtils
1818
import rx.Observable
19+
import rx.Subscriber
1920

2021
import javax.servlet.AsyncContext
2122
import javax.servlet.ServletResponse
@@ -37,6 +38,7 @@ class RxResultTransformer implements ActionResultTransformer {
3738
/**
3839
* For handling exceptions
3940
*/
41+
public static final String CONTENT_TYPE_EVENT_STREAM = "text/event-stream"
4042
@Autowired(required = false)
4143
GrailsExceptionResolver exceptionResolver
4244

@@ -50,20 +52,22 @@ class RxResultTransformer implements ActionResultTransformer {
5052
UrlConverter urlConverter
5153

5254
Object transformActionResult(GrailsWebRequest webRequest, String viewName, Object actionResult, boolean isRender = false) {
53-
ObservableResult observableResult = null
54-
if(actionResult instanceof ObservableResult) {
55-
observableResult = ((ObservableResult)actionResult)
56-
actionResult = observableResult.observable
55+
TimeoutResult timeoutResult = null
56+
if(actionResult instanceof TimeoutResult) {
57+
timeoutResult = ((TimeoutResult)actionResult)
58+
if(timeoutResult instanceof ObservableResult) {
59+
actionResult = ((ObservableResult)timeoutResult).observable
60+
}
5761
}
5862

59-
if(actionResult instanceof Observable) {
60-
// handle RxJava Observables
61-
Observable observable = (Observable)actionResult
6263

64+
boolean isObservable = actionResult instanceof Observable
65+
if(isObservable || (actionResult instanceof NewObservableResult)) {
6366
// tell Grails not to render the view by convention
6467
HttpServletRequest request = webRequest.getCurrentRequest()
6568
HttpServletResponse response = webRequest.getCurrentResponse()
6669

70+
6771
webRequest.setRenderView(false)
6872

6973
// Create the Async web request and register it with the WebAsyncManager so Spring is aware
@@ -80,33 +84,52 @@ class RxResultTransformer implements ActionResultTransformer {
8084
asyncWebRequest.startAsync()
8185
request.setAttribute(GrailsApplicationAttributes.ASYNC_STARTED, true)
8286
GrailsAsyncContext asyncContext = new GrailsAsyncContext(asyncWebRequest.asyncContext, webRequest)
83-
final boolean isStreaming = observableResult instanceof StreamingObservableResult
84-
if(observableResult != null) {
85-
def timeout = observableResult.timeoutInMillis()
87+
final boolean isStreaming = timeoutResult instanceof StreamingResult
88+
if(timeoutResult != null) {
89+
def timeout = timeoutResult.timeoutInMillis()
8690
if(timeout != null) {
8791
asyncContext.setTimeout(timeout)
8892
}
8993
if(isStreaming) {
90-
response.setContentType("text/event-stream");
94+
response.setContentType(CONTENT_TYPE_EVENT_STREAM);
9195
response.flushBuffer()
9296
}
9397
}
94-
// in a separate thread register the observable subscriber
95-
asyncContext.start {
96-
RxResultSubscriber subscriber = new RxResultSubscriber(
97-
asyncContext,
98-
exceptionResolver,
99-
linkGenerator,
100-
webRequest.controllerClass,
101-
(Controller)webRequest.attributes.getController(request)
102-
)
103-
subscriber.isRender = isRender
104-
subscriber.urlConverter = urlConverter
105-
if(isStreaming) {
106-
subscriber.serverSendEvents = true
107-
subscriber.serverSendEventName = ((StreamingObservableResult)observableResult).eventName
98+
99+
RxResultSubscriber subscriber = new RxResultSubscriber(
100+
asyncContext,
101+
exceptionResolver,
102+
linkGenerator,
103+
webRequest.controllerClass,
104+
(Controller)webRequest.attributes.getController(request)
105+
)
106+
subscriber.isRender = isRender
107+
subscriber.urlConverter = urlConverter
108+
if(isStreaming) {
109+
subscriber.serverSendEvents = true
110+
subscriber.serverSendEventName = ((StreamingResult)timeoutResult).eventName
111+
}
112+
113+
// handle RxJava Observables
114+
if(isObservable) {
115+
116+
Observable observable = (Observable)actionResult
117+
118+
// in a separate thread register the observable subscriber
119+
asyncContext.start {
120+
observable.subscribe(subscriber)
108121
}
109-
observable.subscribe(subscriber)
122+
}
123+
else {
124+
NewObservableResult newObservableResult = (NewObservableResult)actionResult
125+
Observable newObservable = Observable.create( { Subscriber newSub ->
126+
asyncContext.start {
127+
Closure callable = newObservableResult.callable
128+
callable.setDelegate(newSub)
129+
callable.call(newSub)
130+
}
131+
} as Observable.OnSubscribe)
132+
newObservable.subscribe(subscriber)
110133
}
111134
// return null indicating that the request thread should be returned to the thread pool
112135
// async request processing will take over
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.grails.plugins.rx.web
2+
3+
import groovy.transform.CompileStatic
4+
import groovy.transform.InheritConstructors
5+
6+
/**
7+
* A new observable that is streaming
8+
*
9+
* @author Graeme Rocher
10+
* @since 1.0
11+
*/
12+
@CompileStatic
13+
@InheritConstructors
14+
class StreamingNewObservableResult<T> extends NewObservableResult<T> implements StreamingResult {
15+
}

src/main/groovy/org/grails/plugins/rx/web/StreamingObservableResult.groovy

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,5 @@ import groovy.transform.InheritConstructors
1111
*/
1212
@CompileStatic
1313
@InheritConstructors
14-
class StreamingObservableResult<T> extends ObservableResult<T> {
15-
/**
16-
* The event name, defaults to null
17-
*/
18-
String eventName
14+
class StreamingObservableResult<T> extends ObservableResult<T> implements StreamingResult{
1915
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy