Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run ydoc-server with GraalVM #9528

Merged
merged 61 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
456c59e
DRAFT: js-websocket
4e6 Mar 21, 2024
675bf37
DRAFT: webpack
4e6 Mar 21, 2024
dcdc4f1
update: configure webpack
4e6 Mar 25, 2024
7635e02
DRAFT: ffiJvm
4e6 Mar 27, 2024
fffa04d
DRAFT: vite bundle
4e6 Mar 28, 2024
2d645b1
feat: remove webpack bundle
4e6 Apr 2, 2024
920d39d
refactor: polyglot-ydoc-server
4e6 Apr 2, 2024
34e90ec
feat: encoding polyfill
4e6 Apr 2, 2024
30abe2d
feat: abort controller
4e6 Apr 9, 2024
788a9df
DRAFT: working language server connection
4e6 Apr 11, 2024
6d13cb9
fix: workaround ws dependency
4e6 Apr 11, 2024
cbf9ff1
update: simplify Main
4e6 Apr 11, 2024
64a4aef
update: crypto
4e6 Apr 11, 2024
b86f2eb
refactor: polyfill components
4e6 Apr 11, 2024
3f8b094
refactor: ydoc-server
4e6 Apr 11, 2024
b486e86
misc: cleanup websocket
4e6 Apr 11, 2024
de4ab9f
test: platform
4e6 Apr 12, 2024
0aa4287
refactor: web socket class
4e6 Apr 15, 2024
a276155
update: ffi interface
4e6 Apr 15, 2024
14b8470
feat: byte buffer interop
4e6 Apr 15, 2024
6936b97
feat: WebSocket.ping
4e6 Apr 15, 2024
89b7082
feat: url path
4e6 Apr 15, 2024
d309f9d
feat: connect to dashboard
4e6 Apr 16, 2024
ef185c5
feat: build uses java ffi
4e6 Apr 16, 2024
6aa97df
feat: vite build before run
4e6 Apr 16, 2024
c8d3b5f
feat: run ydoc dev server
4e6 Apr 16, 2024
c133dd3
revert: ffi interface
4e6 Apr 16, 2024
2affe8f
feat: java parser ffi
4e6 Apr 17, 2024
9e19852
fix: ffi parse_code
4e6 Apr 18, 2024
d5e50cc
misc: fmt
4e6 Apr 18, 2024
4858351
fix: typedarray interop
4e6 Apr 19, 2024
2a0b0ef
doc: ffi
4e6 Apr 19, 2024
6658eb5
DEBUG: vite dev
4e6 Apr 22, 2024
6ce1ddb
misc: nodejs environment
4e6 Apr 22, 2024
a73c4a5
misc: doc web polyfill
4e6 Apr 22, 2024
6f689f0
misc: typo
4e6 Apr 22, 2024
9c05dbc
feat: util api
4e6 Apr 22, 2024
5a11c21
feat: logging
4e6 Apr 22, 2024
c6c67dc
fix: event target events
4e6 Apr 23, 2024
a3e195f
feat: context builder
4e6 Apr 23, 2024
f22db61
feat: configure debug logging
4e6 Apr 23, 2024
ab24af3
misc: fmt
4e6 Apr 23, 2024
1884a9e
feat: workaround ws dependency
4e6 Apr 23, 2024
9d54d78
misc: cleanup vite build
4e6 Apr 23, 2024
7548ef0
misc: ffiPolyglot
4e6 Apr 23, 2024
7c8fec7
misc: typescript fmt
4e6 Apr 23, 2024
fbbb6e0
misc: helidon issue fixed in 4.0.8
4e6 Apr 23, 2024
6345a3e
feat: cloneEvent test
4e6 Apr 23, 2024
997ea10
DRAFT: profiling
4e6 Apr 24, 2024
8320ecc
sampler can be null when debugging
JaroslavTulach Apr 29, 2024
e208a5e
Fix profiling-utils/compile
Akirathan Apr 30, 2024
4617cd8
feat: ydocUrl
4e6 Apr 30, 2024
edf79dc
refactor: resolveLsUrl
4e6 Apr 30, 2024
7d6d6a7
feat: sampling
4e6 May 1, 2024
2bc1983
misc: cleanup
4e6 May 1, 2024
a316b88
misc: lint
4e6 May 1, 2024
8400376
misc: npm typecheck
4e6 May 1, 2024
a662c60
fix: npm run lint
4e6 May 1, 2024
4f17e3b
fix: lint strict-boolean-expressions
4e6 May 1, 2024
629d6b9
fix: prettier
4e6 May 1, 2024
f508ffa
Enable serialization of JMX beans
JaroslavTulach May 2, 2024
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 app/gui2/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="vite/client" />

declare const PROJECT_MANAGER_URL: string
declare const YDOC_SERVER_URL: string
declare const RUNNING_VITEST: boolean
declare const IS_CLOUD_BUILD: boolean

Expand Down
1 change: 1 addition & 0 deletions app/gui2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"story:build": "histoire build",
"story:preview": "histoire preview",
"build-only": "vite build",
"build-ydoc-server-polyglot": "vite build --config vite.ydoc-server-polyglot.config.ts",
"compile-server": "tsc -p tsconfig.server.json",
"typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint .",
Expand Down
6 changes: 6 additions & 0 deletions app/gui2/shared/ast/ffi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* @file Provides the Rust ffi interface. The interface should be kept in sync with polyglot ffi inteface {@link module:ffiPolyglot}.
*
* @module ffi
*/

import { createXXHash128 } from 'hash-wasm'
import type { IDataType } from 'hash-wasm/dist/lib/util'
import init, { is_ident_or_operator, parse, parse_doc_to_json } from '../../rust-ffi/pkg/rust_ffi'
Expand Down
21 changes: 21 additions & 0 deletions app/gui2/shared/ast/ffiPolyglot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @file This file is used as ffi {@link module:ffi} interface for building the polyglot ydoc server.
* All the exported methods are provided by the ydoc server implementation.
* The interface should be kept in sync with Rust ffi inteface {@link module:ffi}.
*
* @module ffiPolyglot
*/

import type { IDataType } from 'hash-wasm/dist/lib/util'

declare global {
function parse_tree(code: string): Uint8Array
function parse_doc_to_json(docs: string): string
function is_ident_or_operator(code: string): number
function xxHash128(input: IDataType): string
}

export async function initializeFFI(path?: string | undefined) {}

/* eslint-disable-next-line camelcase */
export const { is_ident_or_operator, parse_doc_to_json, parse_tree, xxHash128 } = globalThis
5 changes: 5 additions & 0 deletions app/gui2/src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
"value": "",
"primary": false
},
"ydocUrl": {
"description": "The address of the Ydoc Server endpoint.",
"value": "",
"primary": false
},
"namespace": {
"description": "The namespace of the opened project. It can be used when connecting to an existing Language Server process.",
"value": "local",
Expand Down
2 changes: 2 additions & 0 deletions app/gui2/src/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function main() {
const supportsVibrancy = config.window.vibrancy
const shouldUseAuthentication = config.authentication.enabled
const projectManagerUrl = config.engine.projectManagerUrl || PROJECT_MANAGER_URL
const ydocUrl = config.engine.ydocUrl === '' ? YDOC_SERVER_URL : config.engine.ydocUrl
const initialProjectName = config.startup.project || null

dashboard.run({
Expand All @@ -77,6 +78,7 @@ function main() {
supportsLocalBackend: !IS_CLOUD_BUILD,
supportsDeepLinks: !isDevMode && !isOnLinux(),
projectManagerUrl,
ydocUrl,
isAuthenticationDisabled: !shouldUseAuthentication,
shouldShowDashboard: true,
initialProjectName,
Expand Down
25 changes: 17 additions & 8 deletions app/gui2/src/stores/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,29 @@ import * as Y from 'yjs'
interface LsUrls {
rpcUrl: string
dataUrl: string
ydocUrl: URL
}

function resolveLsUrl(config: GuiConfig): LsUrls {
const engine = config.engine
if (engine == null) throw new Error('Missing engine configuration')
if (engine.rpcUrl != null && engine.dataUrl != null) {
let dataUrl = engine.dataUrl
let rpcUrl = engine.rpcUrl
let ydocUrl
if (engine.ydocUrl == null || engine.ydocUrl === '') {
ydocUrl = new URL(location.origin)
ydocUrl.protocol = location.protocol.replace(/^http/, 'ws')
} else {
ydocUrl = new URL(engine.rpcUrl)
ydocUrl.port = '1234'
}
ydocUrl.pathname = '/project'

return {
rpcUrl: engine.rpcUrl,
dataUrl: engine.dataUrl,
rpcUrl,
dataUrl,
ydocUrl,
}
}
throw new Error('Incomplete engine configuration')
Expand Down Expand Up @@ -132,13 +146,8 @@ export const useProjectStore = defineStore('project', () => {

let yDocsProvider: ReturnType<typeof attachProvider> | undefined
watchEffect((onCleanup) => {
// For now, let's assume that the websocket server is running on the same host as the web server.
// Eventually, we can make this configurable, or even runtime variable.
const socketUrl = new URL(location.origin)
socketUrl.protocol = location.protocol.replace(/^http/, 'ws')
socketUrl.pathname = '/project'
yDocsProvider = attachProvider(
socketUrl.href,
lsUrls.ydocUrl.href,
'index',
{ ls: lsUrls.rpcUrl },
doc,
Expand Down
43 changes: 42 additions & 1 deletion app/gui2/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import vue from '@vitejs/plugin-vue'
import { getDefines, readEnvironmentFromFile } from 'enso-common/src/appConfig'
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import postcssNesting from 'postcss-nesting'
import tailwindcss from 'tailwindcss'
Expand All @@ -14,6 +16,9 @@ const localServerPort = 8080
const projectManagerUrl = 'ws://127.0.0.1:30535'

const IS_CLOUD_BUILD = process.env.CLOUD_BUILD === 'true'
const IS_POLYGLOT_YDOC_SERVER_DEBUG = process.env.POLYGLOT_YDOC_SERVER_DEBUG === 'true'
const IS_POLYGLOT_YDOC_SERVER =
process.env.POLYGLOT_YDOC_SERVER === 'true' || IS_POLYGLOT_YDOC_SERVER_DEBUG

await readEnvironmentFromFile()

Expand Down Expand Up @@ -59,6 +64,7 @@ export default defineConfig({
...getDefines(localServerPort),
IS_CLOUD_BUILD: JSON.stringify(IS_CLOUD_BUILD),
PROJECT_MANAGER_URL: JSON.stringify(projectManagerUrl),
YDOC_SERVER_URL: IS_POLYGLOT_YDOC_SERVER ? JSON.stringify('defined') : undefined,
RUNNING_VITEST: false,
'import.meta.vitest': false,
// Single hardcoded usage of `global` in aws-amplify.
Expand Down Expand Up @@ -94,13 +100,48 @@ export default defineConfig({
},
})

let ydocServer: ChildProcessWithoutNullStreams | null
function gatewayServer(): Plugin {
return {
name: 'gateway-server',
configureServer(server) {
if (server.httpServer == null) return

createGatewayServer(server.httpServer, undefined)
if (IS_POLYGLOT_YDOC_SERVER) {
const ydocServerJar = fileURLToPath(
new URL(
'../../lib/java/ydoc-server/target/ydoc-server-assembly-0.1.0-SNAPSHOT.jar',
JaroslavTulach marked this conversation as resolved.
Show resolved Hide resolved
import.meta.url,
),
)
const runYdocServer = () => {
let args = []
if (IS_POLYGLOT_YDOC_SERVER_DEBUG) {
args.push('-DinspectPort=34567')
}
args.push('-jar', ydocServerJar)
ydocServer = spawn('java', args)
if (IS_POLYGLOT_YDOC_SERVER_DEBUG) {
ydocServer.stdout.on('data', (data) => console.log(`ydoc: ${data}`))
}
ydocServer.stderr.on('data', (data) => console.log(`ydoc: ${data}`))
}
if (!existsSync(ydocServerJar)) {
const cwd = fileURLToPath(new URL('../..', import.meta.url))
const sbt = spawn('sbt', ['ydoc-server/assembly'], { cwd })
Copy link
Contributor

Choose a reason for hiding this comment

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

That seems like an unnecessary dependency. If someone forgot to build the jar, tough luck.

sbt.stdout.on('data', (data) => console.log(`sbt: ${data}`))
sbt.on('exit', runYdocServer)
} else {
runYdocServer()
}
} else {
createGatewayServer(server.httpServer, undefined)
}
},
buildEnd() {
if (ydocServer == null) return

ydocServer.kill('SIGTERM')
},
}
}
Expand Down
72 changes: 72 additions & 0 deletions app/gui2/vite.ydoc-server-polyglot.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/// <reference types="histoire" />

import { getDefines, readEnvironmentFromFile } from 'enso-common/src/appConfig'
import * as fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { defineConfig, type Plugin } from 'vite'

const localServerPort = 8080
const projectManagerUrl = 'ws://127.0.0.1:30535'

const IS_CLOUD_BUILD = process.env.CLOUD_BUILD === 'true'

await readEnvironmentFromFile()

// https://vitejs.dev/config/
export default defineConfig({
root: fileURLToPath(new URL('.', import.meta.url)),
cacheDir: fileURLToPath(new URL('../../node_modules/.cache/vite', import.meta.url)),
publicDir: fileURLToPath(new URL('./public', import.meta.url)),
envDir: fileURLToPath(new URL('.', import.meta.url)),
plugins: [usePolyglotFfi()],
resolve: {
alias: {
shared: fileURLToPath(new URL('./shared', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
define: {
...getDefines(localServerPort),
IS_CLOUD_BUILD: JSON.stringify(IS_CLOUD_BUILD),
PROJECT_MANAGER_URL: JSON.stringify(projectManagerUrl),
RUNNING_VITEST: false,
'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
emptyOutDir: true,
outDir: '../../lib/java/ydoc-server/target/classes/dist',
rollupOptions: {
input: {
ydocServer: fileURLToPath(new URL('ydoc-server/indexPolyglot.ts', import.meta.url)),
},
output: {
entryFileNames: `assets/[name].js`,
},
},
},
})

/**
* Use `ffiPolyglot` module as `ffi` interface during the build.
*/
function usePolyglotFfi(): Plugin {
const ffiPolyglot = fileURLToPath(new URL('./shared/ast/ffiPolyglot.ts', import.meta.url))
const ffiBackup = fileURLToPath(new URL('./shared/ast/ffiBackup.ts', import.meta.url))
const ffi = fileURLToPath(new URL('./shared/ast/ffi.ts', import.meta.url))

return {
name: 'use-polyglot-ffi',
options: () => {
fs.renameSync(ffi, ffiBackup)
fs.copyFileSync(ffiPolyglot, ffi)
},
buildEnd: () => {
fs.renameSync(ffiBackup, ffi)
},
}
}
22 changes: 22 additions & 0 deletions app/gui2/ydoc-server/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @file Utility methods for ydoc server authentication.
*/

export type ConnectionData = {
lsUrl: string
doc: string
user: string
}

const docNameRegex = /^[a-z0-9/-]+$/i

export function docName(pathname: string) {
const prefix = '/project/'
if (pathname != null && pathname.startsWith(prefix)) {
const docName = pathname.slice(prefix.length)
if (docNameRegex.test(docName)) {
return docName
}
}
return null
}
19 changes: 1 addition & 18 deletions app/gui2/ydoc-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,9 @@ import { IncomingMessage } from 'node:http'
import { parse } from 'url'
import { WebSocket, WebSocketServer } from 'ws'
import { initializeFFI } from '../shared/ast/ffi'
import { ConnectionData, docName } from './auth'
import { setupGatewayClient } from './ydoc'

type ConnectionData = {
lsUrl: string
doc: string
user: string
}

export async function createGatewayServer(httpServer: Server, rustFFIPath: string | undefined) {
await initializeFFI(rustFFIPath)
const wss = new WebSocketServer({ noServer: true })
Expand Down Expand Up @@ -71,15 +66,3 @@ function authenticate(
const data = doc != null && typeof lsUrl === 'string' ? { lsUrl, doc, user } : null
callback(null, data)
}

const docNameRegex = /^[a-z0-9/-]+$/i
function docName(pathname: string) {
const prefix = '/project/'
if (pathname != null && pathname.startsWith(prefix)) {
const docName = pathname.slice(prefix.length)
if (docNameRegex.test(docName)) {
return docName
}
}
return null
}
29 changes: 29 additions & 0 deletions app/gui2/ydoc-server/indexPolyglot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @file An entry point for polyglot Yjs gateway server.
*/

import { docName } from './auth'
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) => {
const doc = docName(url.pathname)
const ls = url.searchParams.get('ls')
if (doc != null && ls != null) {
console.log('setupGatewayClient', ls, doc)
setupGatewayClient(socket, ls, doc)
} else {
console.log('Failed to authenticate user', ls, doc)
}
}

wss.start()
2 changes: 1 addition & 1 deletion app/gui2/ydoc-server/ydoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
import { readSyncMessage, writeSyncStep1, writeUpdate } from 'y-protocols/sync'
import * as Y from 'yjs'

import WebSocket from 'isomorphic-ws'
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'
import { ObservableV2 } from 'lib0/observable'
import { WebSocket } from 'ws'
import { LanguageServerSession } from './languageServerSession'

const pingTimeout = 30000
Expand Down
1 change: 1 addition & 0 deletions app/ide-desktop/lib/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export interface AppProps {
readonly initialProjectName: string | null
readonly onAuthenticated: (accessToken: string | null) => void
readonly projectManagerUrl: string | null
readonly ydocUrl: string | null
readonly appRunner: AppRunner
}

Expand Down
1 change: 1 addition & 0 deletions app/ide-desktop/lib/dashboard/src/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ main.run({
},
/** The cloud frontend is not capable of running a Project Manager. */
projectManagerUrl: null,
ydocUrl: null,
// This cannot be `appRunner: window.enso` as `window.enso` is set to a new value
// every time a new project is opened.
appRunner: {
Expand Down
Loading
Loading