diff --git a/app/gui2/shared/util/detect.ts b/app/gui2/shared/util/detect.ts index 1973b0a92a1cd..ee27a551d850f 100644 --- a/app/gui2/shared/util/detect.ts +++ b/app/gui2/shared/util/detect.ts @@ -5,7 +5,4 @@ export const isNode = typeof global !== 'undefined' && (global as any)[Symbol.toStringTag] === 'global' -// Java environment is set up to have a `jvm: 'graalvm'` property. -export const isJvm = typeof global !== 'undefined' && (global as any)['jvm'] === 'graalvm' - export const isDevMode = process.env.NODE_ENV === 'development' diff --git a/app/gui2/vite.ydoc-server.config.ts b/app/gui2/vite.ydoc-server.config.ts index d9fa5676de44e..70472e2ee409a 100644 --- a/app/gui2/vite.ydoc-server.config.ts +++ b/app/gui2/vite.ydoc-server.config.ts @@ -30,6 +30,8 @@ export default defineConfig({ 'import.meta.vitest': false, // Single hardcoded usage of `global` in aws-amplify. 'global.TYPED_ARRAY_SUPPORT': true, + // One of the libraries refers to self in `self.fetch.bind(self)` + 'self': 'globalThis' }, build: { minify: false, // For debugging diff --git a/app/gui2/ydoc-server/server.ts b/app/gui2/ydoc-server/server.ts index 703a636eaf30a..9644caa2e0e9a 100644 --- a/app/gui2/ydoc-server/server.ts +++ b/app/gui2/ydoc-server/server.ts @@ -1,5 +1,13 @@ import { setupGatewayClient } from './ydoc' +declare global { + class WebSocketServer { + constructor(config: any) + onconnect: ((socket: any, url: any) => any) | null; + start(): void; + } +} + const wss = new WebSocketServer({ host: 'localhost', port: 1234 }) wss.onconnect = (socket, url) => setupGatewayClient(socket, "ws://localhost:8080", url) diff --git a/build.sbt b/build.sbt index eb865967f27e6..df38c81eca688 100644 --- a/build.sbt +++ b/build.sbt @@ -1125,7 +1125,8 @@ lazy val `polyglot-ydoc-server` = project .settings( crossPaths := false, autoScalaLibrary := false, - run / fork := true, + Compile / run / fork := true, + Test / fork := true, modulePath := { JPMSUtils.filterModulesFromUpdate( update.value, diff --git a/lib/java/polyglot-ydoc-server/src/main/java/org/enso/polyfill/encoding/EncodingPolyfill.java b/lib/java/polyglot-ydoc-server/src/main/java/org/enso/polyfill/encoding/EncodingPolyfill.java new file mode 100644 index 0000000000000..64825a86d83e3 --- /dev/null +++ b/lib/java/polyglot-ydoc-server/src/main/java/org/enso/polyfill/encoding/EncodingPolyfill.java @@ -0,0 +1,56 @@ +package org.enso.polyfill.encoding; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.enso.polyfill.Polyfill; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyExecutable; + +public final class EncodingPolyfill implements ProxyExecutable, Polyfill { + + private static final String TEXT_DECODER_DECODE = "text-decoder-decode"; + + private static final String ENCODING_POLYFILL_JS = "encoding-polyfill.js"; + + public EncodingPolyfill() { + } + + @Override + public void initialize(Context ctx) { + Source encodingPolyfillJs = Source + .newBuilder("js", EncodingPolyfill.class.getResource(ENCODING_POLYFILL_JS)) + .buildLiteral(); + + ctx.eval(encodingPolyfillJs).execute(this); + } + + @Override + public Object execute(Value... arguments) { + var command = arguments[0].asString(); + System.err.println(command + " " + Arrays.toString(arguments)); + + return switch (command) { + case TEXT_DECODER_DECODE -> { + var encoding = arguments[1].asString(); + var data = arguments[2].as(int[].class); + + var charset = encoding == null ? StandardCharsets.UTF_8 : Charset.forName(encoding); + // Convert unsigned Uint8Array to byte[] + var bytes = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + bytes[i] = (byte) data[i]; + } + + yield charset.decode(ByteBuffer.wrap(bytes)).toString(); + } + + default -> + throw new IllegalStateException(command); + }; + } +} diff --git a/lib/java/polyglot-ydoc-server/src/main/java/org/enso/polyfill/websocket/WebSocketPolyfill.java b/lib/java/polyglot-ydoc-server/src/main/java/org/enso/polyfill/websocket/WebSocketPolyfill.java index 7f93afd823262..3f1a71e35446a 100644 --- a/lib/java/polyglot-ydoc-server/src/main/java/org/enso/polyfill/websocket/WebSocketPolyfill.java +++ b/lib/java/polyglot-ydoc-server/src/main/java/org/enso/polyfill/websocket/WebSocketPolyfill.java @@ -12,6 +12,7 @@ import org.enso.polyfill.Polyfill; import org.enso.polyfill.crypto.CryptoPolyfill; +import org.enso.polyfill.encoding.EncodingPolyfill; import org.enso.polyfill.timers.TimersPolyfill; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Source; @@ -58,6 +59,9 @@ public void initialize(Context ctx) { var crypto = new CryptoPolyfill(); crypto.initialize(ctx); + var encoding = new EncodingPolyfill(); + encoding.initialize(ctx); + Source webSocketPolyfillJs = Source .newBuilder("js", WebSocketPolyfill.class.getResource(WEBSOCKET_POLYFILL_JS)) .buildLiteral(); diff --git a/lib/java/polyglot-ydoc-server/src/main/resources/org/enso/polyfill/encoding/encoding-polyfill.js b/lib/java/polyglot-ydoc-server/src/main/resources/org/enso/polyfill/encoding/encoding-polyfill.js new file mode 100644 index 0000000000000..7b13ac4870ed9 --- /dev/null +++ b/lib/java/polyglot-ydoc-server/src/main/resources/org/enso/polyfill/encoding/encoding-polyfill.js @@ -0,0 +1,18 @@ +(function (jvm) { + + class TextDecoder { + + constructor(encoding) { + if (typeof encoding === 'string') { + this._encoding = encoding; + } + } + + decode(arr) { + return jvm('text-decoder-decode', this._encoding, arr); + } + } + + globalThis.TextDecoder = TextDecoder; + +}) diff --git a/lib/java/polyglot-ydoc-server/src/test/java/org/enso/polyfill/EncodingPolyfillTest.java b/lib/java/polyglot-ydoc-server/src/test/java/org/enso/polyfill/EncodingPolyfillTest.java new file mode 100644 index 0000000000000..09b9e0ecdd735 --- /dev/null +++ b/lib/java/polyglot-ydoc-server/src/test/java/org/enso/polyfill/EncodingPolyfillTest.java @@ -0,0 +1,78 @@ +package org.enso.polyfill; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.enso.polyfill.encoding.EncodingPolyfill; +import org.graalvm.polyglot.Context; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class EncodingPolyfillTest { + + private Context context; + private ExecutorService executor; + + public EncodingPolyfillTest() { + } + + @Before + public void setup() throws Exception { + executor = Executors.newSingleThreadExecutor(); + var encoding = new EncodingPolyfill(); + var b = Context.newBuilder("js"); + + var chromePort = Integer.getInteger("inspectPort", -1); + if (chromePort > 0) { + b.option("inspect", ":" + chromePort); + } + + context = CompletableFuture + .supplyAsync(() -> { + var ctx = b.build(); + encoding.initialize(ctx); + return ctx; + }, executor) + .get(); + } + + @After + public void tearDown() { + executor.close(); + context.close(); + } + + @Test + public void textDecoderDecodeUtf8() throws Exception { + var code = """ + let decoder = new TextDecoder(); + var arr = new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33]); + decoder.decode(arr); + """; + + var result = CompletableFuture + .supplyAsync(() -> context.eval("js", code), executor) + .get(); + + Assert.assertEquals("Hello World!", result.as(String.class)); + } + + @Test + public void textDecoderDecodeWindows1251() throws Exception { + var code = """ + let decoder = new TextDecoder('windows-1251'); + var arr = new Uint8Array([207, 240, 232, 226, 229, 242, 44, 32, 236, 232, 240, 33]); + decoder.decode(arr); + """; + + var result = CompletableFuture + .supplyAsync(() -> context.eval("js", code), executor) + .get(); + + Assert.assertEquals("Привет, мир!", result.as(String.class)); + } + +}