Skip to content

Conversation

@4e6
Copy link
Contributor

@4e6 4e6 commented Dec 8, 2025

Pull Request Description

Important Notes

Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

  • The documentation has been updated, if necessary.
  • Screenshots/screencasts have been attached, if there are any visual changes. For interactive or animated visual changes, a screencast is preferred.
  • All code follows the
    Scala,
    Java,
    TypeScript,
    and
    Rust
    style guides. In case you are using a language not listed above, follow the Rust style guide.
  • Unit tests have been written where possible.
  • If meaningful changes were made to logic or tests affecting Enso Cloud integration in the libraries,
    or the Snowflake database integration, a run of the Extra Tests has been scheduled.
    • If applicable, it is suggested to paste a link to a successful run of the Extra Tests.

@4e6 4e6 self-assigned this Dec 8, 2025
@4e6 4e6 added the CI: No changelog needed Do not require a changelog entry for this PR. label Dec 8, 2025
@4e6 4e6 force-pushed the wip/db/14142-ydoc-websocket branch 2 times, most recently from 7356274 to 4371781 Compare December 8, 2025 18:03
@@ -0,0 +1,3 @@
module org.enso.ydoc.api {
exports org.enso.ydoc.api;
Copy link
Member

@JaroslavTulach JaroslavTulach Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • There is no need for a new API module.
  • there already is YdocServerApi
  • please put the new interfaces there as inner classes of YdocServerApi.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Possibly you can move YdocServerApi to the new org.enso.ydoc.api module.
  • but the whole API needs to be co-located

package org.enso.ydoc.api;

public class NoOpMessageCallbacks implements MessageCallbacks {
public static final NoOpMessageCallbacks INSTANCE = new NoOpMessageCallbacks();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Please don't put NoOpMessagesCallbacks implementation class into the API
  • remove it
  • if you have to have it, then encapsulate it
  • put public static final MessageCallbacks NO_OP into MessageCallbacks and make the implementation class package private

Copy link
Member

@JaroslavTulach JaroslavTulach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ideally HostAccess is configured internally
  • and both tests and production code just work with the default configuration
  • possible trick to achieve that is to crate a proxy around interfaces passed to JavaScript

return this;
}

public Builder hostAccessBuilder(HostAccess.Builder hostAccessBuilder) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Why is this a builder?
  • Isn't just HostAccess instance good enough?
Suggested change
public Builder hostAccessBuilder(HostAccess.Builder hostAccessBuilder) {
public Builder hostAccess(HostAccess hostAccessBuilder) {
  • without WebEnvironment.defaultHostAccess being public, one might need a builder
  • but having both builder and WebEnvironment.defaultHostAccess seems unnecessary

WebEnvironment.defaultHostAccess
// allowImplementations is required to call methods on JS objects from Java, i.e. to
// call methods on `YjsChannel` object returned from JS
.allowImplementations(YjsChannel.class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • allowing implementation of a well known interface is OK
  • but opening up allowAccess to a random callbacks.getClass() class is a security flaw
  • please create a proxy around callbacks and expose its methods
    • preferrably via @HostAccess.Explicit
    • or via allowAccess(ProxyMessageCallbacks.getDeclararedMethod("")) but while hardcoding ProxyMessageCallbacks class

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current state with:

.allowImplementations(YjsChannel.class)
.allowPublicAccess(true);

is certainly better than the original one. Thanks.

Still, it be good to remove .allowPublicAccess(true) and use @HostAccess.Explicit annotation instead.

@@ -0,0 +1,3 @@
module org.enso.ydoc.api {
exports org.enso.ydoc.api;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Possibly you can move YdocServerApi to the new org.enso.ydoc.api module.
  • but the whole API needs to be co-located

@4e6 4e6 force-pushed the wip/db/14142-ydoc-websocket branch from 71e9b86 to 8713e6f Compare December 13, 2025 16:18
for (const item of items) {
// Only notify handlers if the message is from another sender
if (item.senderId !== this.senderId) {
this.notifyHandlers(item.payload)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • As of 8713e6f we are probably dealing with ever growing Y.Array
  • somehow we need to find out that the "message has been delivered" to all handlers
    • that's kind of hard in a distributed framework
    • where handlers can appear and diappear
  • one solution would be to add receiverId to item
    • deliver the message only when senderId matches receiverId
    • e.g. if (item.receiverId === this.senderId)`
    • if there was only one receiver of a message
    • then it'd be the receiver's responsibility to remove the item from array

@4e6 4e6 force-pushed the wip/db/14142-ydoc-websocket branch 2 times, most recently from f122b95 to 9625b1f Compare December 29, 2025 11:49
Copy link
Member

@JaroslavTulach JaroslavTulach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It all looks quite decent. Few comments inline.

/**
* A bidirectional communication channel backed by Y.Array.
*
* This class allows multiple parties to send and receive messages through a shared
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • really "multiple parties"?
  • shouldn't the YjsChannel be just bidirectional?

import java.util.ServiceLoader;
import org.enso.ydoc.api.MessageCallbacks;

public abstract class YdocServerApi {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we put YdocServerApi into org.enso.ydoc.api module as well? As this comment suggests?


@Test
public void onConnectSend() throws Exception {
var res = new AtomicReference<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I'd slightly prefer you to create CountingTest class and register its instance instead
  • annotate its set method with @HostAccess.Explicit

@@ -25,21 +28,22 @@ public static void main(String[] args) throws Exception {
var then = System.currentTimeMillis();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Is this public static void main still useful for anything?
  • Shouldn't it be removed?

WebEnvironment.defaultHostAccess
// allowImplementations is required to call methods on JS objects from Java, i.e. to
// call methods on `YjsChannel` object returned from JS
.allowImplementations(YjsChannel.class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current state with:

.allowImplementations(YjsChannel.class)
.allowPublicAccess(true);

is certainly better than the original one. Thanks.

Still, it be good to remove .allowPublicAccess(true) and use @HostAccess.Explicit annotation instead.

var bindings = ctx.getBindings("js");
bindings.putMember("YDOC_HOST", hostname);
bindings.putMember("YDOC_PORT", port);
bindings.putMember("YDOC_MESSAGE_CALLBACKS", callbacks);
Copy link
Member

@JaroslavTulach JaroslavTulach Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • registering non-primitive type as global symbol seems strange
  • with YDOC_HOST one could post it as environment variable, but
  • one cannot register callbacks via environment variable
  • why don't create a initializeYdocServer function
  • and call it here directly with proper arguments?


configureAllDebugLogs(debug)

if (YDOC_MESSAGE_CALLBACKS == undefined) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • this check makes main.ts unusable from command line
    • at least I believe usage from CLI was the motivation for these YDOC_ (environment) variables
  • why don't we define:
function initializeYdocServer(YDOC_HOST, YDOC_PORT, YDOC_LS_DEBUG, YDOC_MESSAGE_CALLBACKS) {
  • and then just call this method with proper arguments from Java?

@enso-bot enso-bot bot mentioned this pull request Dec 30, 2025
4 tasks
@4e6 4e6 force-pushed the wip/db/14142-ydoc-websocket branch from 27df1af to b5f7639 Compare January 1, 2026 20:50
@4e6 4e6 force-pushed the wip/db/14142-ydoc-websocket branch from 149e994 to 9a35c00 Compare January 12, 2026 09:09
}
}
public static void main(String[] args) {
// main method declaration is required to build the native library
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • GraalVM native-image doesn't need main method
  • probably the invocation logic in NativeImage.scala is wrong
  • args ++= Seq("-jar", pathToJAR.toString)
  • when there is no mainClass then we shouldn't pass in any option
  • especially when shared=true

assert impl != null;
var arr = ProxyArray.fromArray(hostname, "" + port);
impl.invokeMember("main", arr);
impl.invokeMember("launch", hostname, port + "", jsonChannelCallbacks, binaryChannelCallbacks);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • the launch method is starting new Thread to form a JavaScript event loop executor
  • due to restrictions spelled out at Allow multiple threads to access a Channel simultaneously #14264
    • "only master can initialize connection on a new thread"
    • "then slave can reply on such a thread or any other thread which was initiated previously by the master"
  • thus we must start the executor here and pass it to the other JVM
  • btw. previously it wasn't a problem as there were no callbacks from JavaScript to Java

@4e6 4e6 force-pushed the wip/db/14142-ydoc-websocket branch from 89fd5f5 to 5a30daa Compare January 21, 2026 14:05
mergify bot pushed a commit that referenced this pull request Jan 22, 2026
)

- @4e6 has troubles to obtain exceptions from _dual JVM mode_
- in his #14438
- this PR intends to make stacktraces of exceptions more useful
@4e6 4e6 force-pushed the wip/db/14142-ydoc-websocket branch from dead671 to 75734a9 Compare January 22, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI: No changelog needed Do not require a changelog entry for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avoid networking layer of Akka by ydoc-server talking directly to the engine+ls

4 participants