micronaut-views icon indicating copy to clipboard operation
micronaut-views copied to clipboard

Support async views rendering

Open PepeBotella25 opened this issue 6 months ago • 0 comments

Feature description

Currently the Views rendering (ReactViewsRenderer::render) cannot be async, if happens that it's, it fails with io.netty.util.IllegalReferenceCountException: refCnt: 0.

In my case I'm using micronaut-views-react and my Jasascript waits for a Java Mono.

Despite the docs says that's not supported, I was able to workaround that by implementing my own ViewFilter where I create a Publisher from the Writable returned by ReactViewsRenderer::render, basically the idea is to wait until Writer::write gets called.

So I wonder whether something like this can be implemented to support async "rendering".

I copied the ViewsFilter (removing some logic I didn't need in my case), and "delay" the response until the Writer::write gets called.

It's only waiting for the first call to write, but leaving subscriber.onComplete(); only in flush and close should support multiple calls. As micronaut-views-react does not call neither flush nor close I had to call subscriber.onComplete(); in write too.

public final Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
			ServerFilterChain chain) {
		return Flux.from(chain.proceed(request))
				.switchMap(response -> {
					Object body = response.body();

					String view = viewsResolver.resolveView(request, response).orElse(null);
					if (view == null || !view.equals("App")) { // Only use this for my App View
						return Flux.just(response);
					}

					MediaType type = UTF8_HTML;
					Optional<ViewsRenderer> optionalViewsRenderer = viewsRendererLocator.resolveViewsRenderer(view, type.getName(), body);
					if(optionalViewsRenderer.isEmpty()) {
						return Flux.just(response);
					}

					ModelAndView<?> modelAndView = new ModelAndView<>(view, ((ModelAndView<?>) body).getModel().orElse(null));
					viewsModelDecorator.decorate(request, modelAndView);
					Writable writable = optionalViewsRenderer.get().render(view, modelAndView.getModel().orElse(null), request);

					return Flux.from((Publisher<String>) subscriber -> {
						subscriber.onSubscribe(new Subscription() {
							@Override
							public void request(long n)
							{
								try
								{
									writable.writeTo(new Writer() {

										@Override
										public void write(char[] buffer, int off, int len) throws IOException
										{
											subscriber.onNext(String.valueOf(buffer));
											subscriber.onComplete();
										}

										@Override
										public void flush() throws IOException
										{
											subscriber.onComplete();
										}

										@Override
										public void close() throws IOException
										{
											subscriber.onComplete();
										}
									});
								}
								catch (Exception e)
								{
									subscriber.onError(e);
								}
							}

							@Override
							public void cancel()
							{
								subscriber.onComplete();
							}
						});
					}).map(b -> response.body(b).contentType(type));
				});
	}

PepeBotella25 avatar Aug 14 '24 17:08 PepeBotella25