Skip to content

Commit

Permalink
Avoid concurrent access to JavaScript objects
Browse files Browse the repository at this point in the history
  • Loading branch information
making committed Apr 9, 2024
1 parent 0a9f733 commit bd3c2d8
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 42 deletions.
114 changes: 73 additions & 41 deletions src/main/java/com/example/post/ssr/ReactRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.graalvm.polyglot.Context;
Expand All @@ -19,6 +23,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.core.NamedInheritableThreadLocal;
import org.springframework.core.NativeDetector;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
Expand All @@ -31,50 +36,35 @@
@Component
public class ReactRenderer implements AutoCloseable {

private final Context context;

private final Value render;
private final ThreadLocal<Value> renderHolder = new NamedInheritableThreadLocal<>("React Render");

private final String template;

private final ObjectMapper objectMapper;

private static final ConcurrentMap<String, File> fileMap = new ConcurrentHashMap<>();

private final List<Context> contexts = new ArrayList<>();

private static final Logger log = LoggerFactory.getLogger(ReactRenderer.class);

public ReactRenderer(ObjectMapper objectMapper) throws IOException {
this.objectMapper = objectMapper;
this.context = Context.newBuilder("js")
.allowIO(IOAccess.ALL)
.allowExperimentalOptions(true)
.option("js.esm-eval-returns-exports", "true")
.option("js.commonjs-require", "true")
.option("js.commonjs-require-cwd", getRoot("polyfill").getAbsolutePath())
.option("js.commonjs-core-modules-replacements",
"stream:stream-browserify,util:fastestsmallesttextencoderdecoder,buffer:buffer/")
.build();
this.context.eval("js", """
globalThis.Buffer = require('buffer').Buffer;
globalThis.URL = require('whatwg-url-without-unicode').URL;
globalThis.process = {
env: {
NODE_ENV: 'production'
}
};
globalThis.document = {};
global = globalThis;
""");
Path code = Paths.get(getRoot("server").getAbsolutePath(), "main-server.js");
Source source = Source.newBuilder("js", code.toFile()).mimeType("application/javascript+module").build();
Value exports = this.context.eval(source);
this.render = exports.getMember("render");
this.template = Files.readString(Paths.get(getRoot("META-INF/resources").getAbsolutePath(), "index.html"));
;
// pre-computing
getRoot("polyfill");
getRoot("server");
}

public String render(String url, Map<String, Object> input) {
Value render = this.renderHolder.get();
if (render == null) {
render = createRender();
renderHolder.set(render);
}
try {
String s = this.objectMapper.writeValueAsString(input);
Value executed = this.render.execute(url, s);
Value executed = render.execute(url, s);
Value head = executed.getMember("head");
Value html = executed.getMember("html");
return this.template
Expand All @@ -89,21 +79,63 @@ public String render(String url, Map<String, Object> input) {
}
}

static File getRoot(String root) throws IOException {
if (NativeDetector.inNativeImage()) {
// in native image
return copyResources(root).toFile();
}
ClassPathResource resource = new ClassPathResource(root);
if (resource.getURL().toString().startsWith("jar:")) {
// in jar file
return new FileSystemResource("./target/classes/" + root).getFile();
Value createRender() {
log.trace("createRender");
try {
Context context = Context.newBuilder("js")
.allowIO(IOAccess.ALL)
.allowExperimentalOptions(true)
.option("js.esm-eval-returns-exports", "true")
.option("js.commonjs-require", "true")
.option("js.commonjs-require-cwd", getRoot("polyfill").getAbsolutePath())
.option("js.commonjs-core-modules-replacements",
"stream:stream-browserify,util:fastestsmallesttextencoderdecoder,buffer:buffer/")
.build();
context.eval("js", """
globalThis.Buffer = require('buffer').Buffer;
globalThis.URL = require('whatwg-url-without-unicode').URL;
globalThis.process = {
env: {
NODE_ENV: 'production'
}
};
globalThis.document = {};
global = globalThis;
""");
Path code = Paths.get(getRoot("server").getAbsolutePath(), "main-server.js");
Source source = Source.newBuilder("js", code.toFile()).mimeType("application/javascript+module").build();
Value exports = context.eval(source);
this.contexts.add(context);
return exports.getMember("render");
}
else {
return resource.getFile();
catch (IOException e) {
throw new UncheckedIOException(e);
}
}

static File getRoot(String root) {
return fileMap.computeIfAbsent(root, key -> {
log.trace("computing getRoot({})", key);
try {
if (NativeDetector.inNativeImage()) {
// in native image
return copyResources(root).toFile();
}
ClassPathResource resource = new ClassPathResource(root);
if (resource.getURL().toString().startsWith("jar:")) {
// in jar file
return new FileSystemResource("./target/classes/" + root).getFile();
}
else {
return resource.getFile();
}
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}

private static Path copyResources(String root) {
try {
Path baseDir = Files.createTempDirectory("copied-");
Expand Down Expand Up @@ -140,7 +172,7 @@ private static Path copyResources(String root) {

@Override
public void close() {
this.context.close();
this.contexts.forEach(Context::close);
}

}
2 changes: 1 addition & 1 deletion src/test/java/com/example/post/ssr/SsrControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

@WebMvcTest(SsrController.class)
@WebMvcTest(controllers = SsrController.class, properties = "logging.level.com.example=trace")
@Import(ReactRenderer.class)
class SsrControllerTest {

Expand Down

0 comments on commit bd3c2d8

Please sign in to comment.