Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
6e027a4
add: ydoc-api
4e6 Dec 1, 2025
913ca09
add: ydoc-channel
4e6 Dec 2, 2025
7ab9d00
update: simplify JsonRpcServer
4e6 Dec 7, 2025
7511423
add: YdocJsonRpcServer
4e6 Dec 7, 2025
f26a88a
add: YjsWSTransport
4e6 Dec 7, 2025
b1a33b5
misc: format
4e6 Dec 8, 2025
65ec4cb
add: JsonRpcServer callbacks
4e6 Dec 8, 2025
461a208
add: callbacks test
4e6 Dec 9, 2025
c984a99
refactor: yjs transport
4e6 Dec 9, 2025
a662c50
DRAFT: yjs transport
4e6 Dec 10, 2025
f6f8dc0
DRAFT: connect gui rpc
4e6 Dec 13, 2025
61ee2df
DRAFT: yjs callbacks
4e6 Dec 13, 2025
7e852f7
misc: fixes
4e6 Dec 14, 2025
d03ed5a
Test simulating Ydoc's YjsChannel communication
JaroslavTulach Dec 19, 2025
63310b0
Test that yields: No persistance for com.oracle.truffle.api.strings.T…
JaroslavTulach Dec 19, 2025
3fc58df
Need TruffleString Persistance to interact with JavaScript
JaroslavTulach Dec 19, 2025
fa50740
DRAFT: yjs transport tests
4e6 Dec 16, 2025
62482f8
DRAFT: debug
4e6 Dec 16, 2025
2ad5870
DRAFT: backend transport
4e6 Dec 19, 2025
6a7f735
update: working yjs transport
4e6 Dec 22, 2025
5135936
feat: pass executor
4e6 Dec 25, 2025
79746e6
update: cleanup internal YjsChannel array
4e6 Dec 25, 2025
3ba1286
test: fix callbacks test
4e6 Dec 25, 2025
8c861f6
feat: syncronized callbacks
4e6 Dec 26, 2025
f8c9234
revert: pass executor
4e6 Dec 26, 2025
a0f331f
misc: format
4e6 Dec 29, 2025
1cc25f6
feat: YjsChannel WebSocket events
4e6 Dec 29, 2025
d0ef5a8
feat: YjsDataChannel
4e6 Dec 29, 2025
caaff5e
feat: binary channel callbacks
4e6 Dec 30, 2025
b6bfc9e
update: yjs handle ArrayBuffer messages
4e6 Jan 1, 2026
d4a2329
update: yjs callbacks handle Uint8Arrays
4e6 Jan 1, 2026
ae7aedc
update: receive ArrayBuffer
4e6 Jan 5, 2026
7ceb98c
update: receive ByteBuffer
4e6 Jan 7, 2026
a319ad5
refactor: YjsChannelCallbacks
4e6 Jan 8, 2026
5fa9a1d
refactor: yjs binary channel
4e6 Jan 8, 2026
b3fa26c
fix: native image building
4e6 Jan 8, 2026
fc07e4e
fix: reflection config
4e6 Jan 12, 2026
8a71fd2
fix: pnpm build
4e6 Jan 12, 2026
29b8814
add: YdocScheduledExecutionService
4e6 Jan 19, 2026
922dcba
DEBUG: channel.subscribe
4e6 Jan 19, 2026
bd9e5d9
Log the process and its argument to be executed
JaroslavTulach Jan 20, 2026
c4474c8
DEBUG: reflection config
4e6 Jan 20, 2026
d4120e8
YjsChannelSynchronized.subscribe has to be mentioned in reflection co…
JaroslavTulach Jan 21, 2026
11e0b2d
Applying javafmtAll
JaroslavTulach Jan 21, 2026
c3877c4
Applying javafmtAll
JaroslavTulach Jan 21, 2026
382d892
Need ability to proxy to Consumer
JaroslavTulach Jan 21, 2026
dc726f7
Update: run ydoc as daemon
4e6 Jan 21, 2026
e5b19f7
Update: pnpm i
4e6 Jan 21, 2026
9e360e8
misc: format
4e6 Jan 21, 2026
2db1a70
misc: pnpm lint
4e6 Jan 21, 2026
035928b
misc: cleanup YjsChannel
4e6 Jan 22, 2026
06dfe34
update: YdocTest
4e6 Jan 22, 2026
75734a9
fix: ls tests
4e6 Jan 22, 2026
f42ecbf
fix: format
4e6 Jan 22, 2026
e30b40a
fix: typecheck
4e6 Jan 22, 2026
954a3cf
misc: cleanup
4e6 Jan 26, 2026
acf3afe
fix: bazel build
4e6 Jan 26, 2026
6e106b0
update: lintAll on CI
4e6 Jan 26, 2026
c021f3c
misc: tests
4e6 Jan 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bazelignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ app/lezer-markdown/node_modules
app/project-manager-shim/node_modules
app/table-expression/node_modules
app/rust-ffi/node_modules
app/ydoc-channel/node_modules
app/ydoc-server/node_modules
app/ydoc-server-nodejs/node_modules
app/ydoc-server-polyglot/node_modules
Expand Down
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ ENGINE_DIST_SOURCES = STDLIB_SOURCES + SBT_PROJECT_FILES + [
"lib/java/poi-wrapper/src/**",
"lib/java/runtime-utils/src/**",
"lib/java/scala-libs-wrapper/src/**",
"lib/java/ydoc-api/src/**",
"lib/java/ydoc-polyfill/src/**",
"lib/java/ydoc-server/src/**",
"lib/java/ydoc-server-registration/src/**",
Expand Down
1 change: 1 addition & 0 deletions app/gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"y-protocols": "^1.0.6",
"y-textarea": "^1.0.2",
"y-websocket": "^1.5.4",
"ydoc-channel": "workspace:*",
"ydoc-shared": "workspace:*",
"yjs": "^13.6.21",
"zod": "catalog:",
Expand Down
2 changes: 2 additions & 0 deletions app/gui/src/project-view/util/crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ interface SubdocsEvent {
export type ProviderParams = {
/** URL for the project's language server RPC connection. */
ls: string
/** URL for the project's data connection. */
data: string
}

/** TODO: Add docs */
Expand Down
22 changes: 7 additions & 15 deletions app/gui/src/project-view/util/net.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import { onScopeDispose } from 'vue'
import { YjsChannel } from 'ydoc-channel'
import { AbortScope } from 'ydoc-shared/util/net'
import {
ReconnectingWebSocket,
ReconnectingWebSocketTransport,
} from 'ydoc-shared/util/net/ReconnectingWSTransport'
import { YjsTransport } from 'ydoc-shared/util/net/YjsTransport'
import * as Y from 'yjs'

export { AbortScope }

const WS_OPTIONS = {
// We do not want to enqueue any messages, because after reconnecting we have to initProtocol again.
maxEnqueuedMessages: 0,
}

/** TODO: Add docs */
export function createRpcTransport(url: string): ReconnectingWebSocketTransport {
return new ReconnectingWebSocketTransport(url, WS_OPTIONS)
export function createRpcTransport(indexDoc: Y.Doc, url: string): YjsTransport {
return new YjsTransport(indexDoc, url)
}

/** TODO: Add docs */
export function createDataWebsocket(url: string, binaryType: 'arraybuffer' | 'blob'): WebSocket {
const websocket = new ReconnectingWebSocket(url, undefined, WS_OPTIONS)
websocket.binaryType = binaryType
return websocket as WebSocket
export function createDataSocket(indexDoc: Y.Doc, url: string): YjsChannel {
return new YjsChannel(indexDoc, url)
}

export interface WebSocketHandler {
Expand Down
12 changes: 7 additions & 5 deletions app/gui/src/project-view/util/net/dataServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Err, Ok, type Result } from 'enso-common/src/utilities/data/result'
import { ObservableV2 } from 'lib0/observable'
import type { YjsChannel } from 'ydoc-channel'
import {
Builder,
ByteBuffer,
Expand Down Expand Up @@ -60,20 +61,22 @@ export class DataServer extends ObservableV2<DataServerEvents> {
/** `websocket.binaryType` should be `ArrayBuffer`. */
constructor(
public clientId: string,
public websocket: WebSocket,
public websocket: YjsChannel,
abort: AbortScope,
) {
super()
abort.handleDispose(this)

websocket.addEventListener('message', ({ data: rawPayload }) => {
if (!(rawPayload instanceof ArrayBuffer)) {
if (!ArrayBuffer.isView(rawPayload)) {
console.warn('Data Server: Data type was invalid:', rawPayload)
// Ignore all non-binary messages. If the messages are `Blob`s instead, this is a
// misconfiguration and should also be ignored.
return
}
const binaryMessage = OutboundMessage.getRootAsOutboundMessage(new ByteBuffer(rawPayload))
const binaryMessage = OutboundMessage.getRootAsOutboundMessage(
new ByteBuffer(rawPayload.buffer),
)
const payloadType = binaryMessage.payloadType()
const payload = binaryMessage.payload(new PAYLOAD_CONSTRUCTOR[payloadType]())
if (!payload) return
Expand Down Expand Up @@ -101,8 +104,7 @@ export class DataServer extends ObservableV2<DataServerEvents> {
this.scheduleInitializationAfterConnect()
})

if (websocket.readyState === WebSocket.OPEN) this.initialized = this.initialize()
else this.initialized = this.scheduleInitializationAfterConnect()
this.initialized = this.initialize()
}

/** TODO: Add docs */
Expand Down
46 changes: 26 additions & 20 deletions app/gui/src/providers/openedProjects/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { nextEvent } from '@/util/data/observable'
import type { Opt } from '@/util/data/opt'
import { ReactiveMapping } from '@/util/database/reactiveDb'
import type { MethodPointer } from '@/util/methodPointer'
import { createDataWebsocket, createRpcTransport, useAbortScope } from '@/util/net'
import { createDataSocket, createRpcTransport, useAbortScope } from '@/util/net'
import { DataServer } from '@/util/net/dataServer'
import { ProjectPath } from '@/util/projectPath'
import { tryQualifiedName, type QualifiedName } from '@/util/qualifiedName'
Expand Down Expand Up @@ -74,15 +74,17 @@ export function createProjectStore(

const doc = new Y.Doc()
const awareness = new Awareness(doc)

const ydocUrl = resolveYDocUrl(props.engine.rpcUrl, props.engine.ydocUrl)
const guiRpcId = `gui-rpc-${crypto.randomUUID()}`
const clientId = crypto.randomUUID() as Uuid
const lsRpcConnection = createLsRpcConnection(clientId, props.engine.rpcUrl, abort)
const lsRpcConnection = createLsRpcConnection(clientId, doc, guiRpcId, abort)
const projectRootId = lsRpcConnection.contentRoots.then(
(roots) => roots.find((root) => root.type === 'Project')?.id,
)
onScopeDispose(() => lsRpcConnection.release())

const dataConnection = initializeDataConnection(clientId, props.engine.dataUrl, abort)
const guiDataId = `gui-data-${crypto.randomUUID()}`
const dataConnection = initializeDataConnection(clientId, doc, guiDataId, abort)
const rpcUrl = new URL(props.engine.rpcUrl)
const isOnLocalBackend =
rpcUrl.protocol === 'mock:' ||
Expand All @@ -91,23 +93,12 @@ export function createProjectStore(
rpcUrl.hostname === '[::1]' ||
rpcUrl.hostname === '0:0:0:0:0:0:0:1'

const moduleProjectPath = computed((): Result<ProjectPath> | undefined => {
const filePath = observedFileName.value
if (filePath == null) return undefined
const withoutFileExt = filePath.replace(/\.enso$/, '')
const withDotSeparators = withoutFileExt.replace(/\//g, '.')
const qn = tryQualifiedName(withDotSeparators)
if (!qn.ok) return qn
return Ok(ProjectPath.create(undefined, qn.value))
})

const ydocUrl = resolveYDocUrl(props.engine.rpcUrl, props.engine.ydocUrl)
let yDocsProvider: ReturnType<typeof attachProvider> | undefined
watchEffect((onCleanup) => {
yDocsProvider = attachProvider(
ydocUrl.href,
'index',
{ ls: props.engine.rpcUrl },
{ ls: guiRpcId, data: guiDataId },
doc,
awareness.internal,
)
Expand All @@ -117,6 +108,16 @@ export function createProjectStore(
})
})

const moduleProjectPath = computed((): Result<ProjectPath> | undefined => {
const filePath = observedFileName.value
if (filePath == null) return undefined
const withoutFileExt = filePath.replace(/\.enso$/, '')
const withDotSeparators = withoutFileExt.replace(/\//g, '.')
const qn = tryQualifiedName(withDotSeparators)
if (!qn.ok) return qn
return Ok(ProjectPath.create(undefined, qn.value))
})

const projectModel = new DistributedProject(doc)

const entryPoint = computed<MethodPointer>(() => {
Expand Down Expand Up @@ -443,8 +444,13 @@ function resolveYDocUrl(rpcUrl: string, url: string): URL {
return resolved
}

function createLsRpcConnection(clientId: Uuid, url: string, abort: AbortScope): LanguageServer {
const transport = createRpcTransport(url)
function createLsRpcConnection(
clientId: Uuid,
doc: Y.Doc,
url: string,
abort: AbortScope,
): LanguageServer {
const transport = createRpcTransport(doc, url)
const connection = new LanguageServer(clientId, transport)
abort.onAbort(() => {
connection.stopReconnecting()
Expand All @@ -453,8 +459,8 @@ function createLsRpcConnection(clientId: Uuid, url: string, abort: AbortScope):
return connection
}

function initializeDataConnection(clientId: Uuid, url: string, abort: AbortScope) {
const client = createDataWebsocket(url, 'arraybuffer')
function initializeDataConnection(clientId: Uuid, doc: Y.Doc, url: string, abort: AbortScope) {
const client = createDataSocket(doc, url)
const connection = new DataServer(clientId, client, abort)
onScopeDispose(() => connection.dispose())
return connection
Expand Down
10 changes: 8 additions & 2 deletions app/project-manager-shim/src/projectService/ensoRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,14 @@ export class EnsoRunner implements Runner {
args: readonly string[],
spawnCallback: (cmd: string, cmdArgs: readonly string[]) => T,
) {
const cmd = this.ensoPath.endsWith('.bat') ? 'cmd.exe' : this.ensoPath
const cmdArgs = this.ensoPath.endsWith('.bat') ? ['/c', this.ensoPath, ...args] : args
const [cmd, cmdArgs] =
this.ensoPath.endsWith('.bat') ?
['cmd.exe', ['/c', this.ensoPath, ...args]]
: [this.ensoPath, args]
const isDevMode = process.env.NODE_ENV === 'development'
if (isDevMode) {
console.log('runProcess', cmd, 'with', cmdArgs)
}
return spawnCallback(cmd, cmdArgs)
}

Expand Down
34 changes: 34 additions & 0 deletions app/ydoc-channel/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
load("@aspect_rules_js//npm:defs.bzl", "npm_package")
load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
load("@npm//:defs.bzl", "npm_link_all_packages", "npm_link_targets")

npm_link_all_packages(name = "node_modules")

ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = ["//:tsconfig"],
)

ts_project(
name = "tsc",
srcs = glob(["src/**/*.ts"]),
composite = True,
out_dir = "dist",
root_dir = "src",
tsconfig = ":tsconfig",
validate = select({
"@platforms//os:windows": False,
"//conditions:default": True,
}),
deps = npm_link_targets(),
)

npm_package(
name = "pkg",
srcs = [
"package.json",
":tsc",
],
visibility = ["//visibility:public"],
)
28 changes: 28 additions & 0 deletions app/ydoc-channel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "ydoc-channel",
"version": "0.1.0",
"description": "Y.js-based bidirectional communication channel",
"type": "module",
"exports": {
".": {
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"main": "src/index.ts",
"scripts": {
"test:unit": "vitest run",
"compile": "tsc",
"lint": "eslint . --cache --max-warnings=0"
},
"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
"dependencies": {
"lib0": "^0.2.99",
"yjs": "^13.6.19"
}
}
Loading
Loading