Skip to content

Add WebSocket module and streaming query evaluation to exist-core#6145

Open
joewiz wants to merge 12 commits intoeXist-db:developfrom
joewiz:feature/websocket-core
Open

Add WebSocket module and streaming query evaluation to exist-core#6145
joewiz wants to merge 12 commits intoeXist-db:developfrom
joewiz:feature/websocket-core

Conversation

@joewiz
Copy link
Member

@joewiz joewiz commented Mar 17, 2026

Summary

Adds WebSocket infrastructure to exist-core with two endpoints:

  • /ws — Channel-based pub/sub messaging (ported from monex) with _monitor query lifecycle broadcasting
  • /ws/eval — Real-time streaming XQuery evaluation with cancellation, progress reporting, and timing breakdown

This is the foundation for removing Java code from monex, enabling future LSP wire protocol support, and making eXist's interactive tools feel modern and responsive.

Based on: PR #6144 (Jetty 12 upgrade) — should merge after that PR.

/ws — Channel Pub/Sub

Ported from monex's RemoteConsoleEndpoint into a general-purpose WebSocket module.

XQuery ws:send("channel", $data)
    → WebSocketModule → WebSocketAdapter → WebSocketEndpoint → Browser

Clients connect, subscribe to channels via {"channel": "name"}, and receive JSON messages. A 500ms heartbeat ping keeps connections alive through proxies.

_monitor Channel (new)

Admin clients can subscribe to the _monitor channel to receive real-time query lifecycle events:

  • Event-driven: started, progress, completed, error, cancelled events from /ws/eval queries
  • Periodic snapshots: All running queries (including REST/XQueryServlet) via ProcessMonitor, broadcast every 1 second

XQuery Functions

Function Signature Purpose
ws:log ($items as item()*) as empty-sequence() Log to "default" channel
ws:log ($channel, $items) Log to specific channel
ws:send ($channel, $items) Send JSON to channel
ws:broadcast ($items as item()*) Send to ALL clients
ws:channel-count ($channel) as xs:integer Count subscribers

/ws/eval — Streaming Query Evaluation (new)

A WebSocket endpoint for real-time XQuery evaluation — the XQuery equivalent of a modern database's interactive query console protocol.

Protocol (JSON over WebSocket)

Client → Server:

Action Purpose
eval Execute query with optional streaming, variables, serialization, timeout
cancel Cancel a running query by ID
compile Compile-check query without execution (syntax validation)
admin-cancel DBA-only: kill any running query via ProcessMonitor

Server → Client:

Type Purpose
progress Phase updates: parsing → compiling → evaluating → serializing
result Serialized data chunks with more/done indicators and timing
error XPath error with code, message, line, column
cancelled Confirmation with items-produced count
compile Success/failure with diagnostics array

Key Features

  • Streaming: Configurable chunk size (default 1000 items). Large results stream incrementally — the client sees data as it's produced.
  • Cancellation: cancel action sets a flag on XQueryWatchDog. Works mid-evaluation and mid-serialization.
  • Timing breakdown: Parse, compile, evaluate, serialize phases measured independently.
  • Authentication: Basic auth on WebSocket handshake, falls back to guest.
  • Admin-cancel: DBA users can kill any running query (same as system:kill-running-xquery).
  • Progress reporting: Phase transitions broadcast to both the eval client and _monitor subscribers.

Example

const ws = new WebSocket('ws://localhost:8080/exist/ws/eval');

ws.send(JSON.stringify({
    action: 'eval',
    id: 'q-1',
    query: 'for $i in 1 to 1000000 return $i',
    "chunk-size": 1000,
    serialization: { method: 'adaptive' }
}));

ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    if (msg.type === 'result') appendToOutput(msg.data);
    if (msg.type === 'progress') updateStatusBar(msg.phase);
    if (!msg.more) showTiming(msg.timing);
};

// Cancel
ws.send(JSON.stringify({ action: 'cancel', id: 'q-1' }));

New Classes

Class Package Purpose
WebSocketEndpoint o.e.xquery.functions.websocket @ServerEndpoint("/ws") — channel pub/sub, heartbeat
WebSocketModule o.e.xquery.functions.websocket XQuery module (ws: namespace)
ConsoleCompatModule o.e.xquery.functions.websocket Backward-compatible console: namespace
EvalWebSocketEndpoint o.e.http.ws @ServerEndpoint("/ws/eval") — streaming eval
EvalSession o.e.http.ws Per-connection state, query cancellation
QueryExecutor o.e.http.ws XQuery execution with streaming, progress, timing
EvalProtocol o.e.http.ws JSON message parsing and generation
QueryMonitorBroadcaster o.e.http.ws Bridges eval lifecycle → _monitor channel

Backward Compatibility

The console: namespace (http://exist-db.org/xquery/console) is provided by ConsoleCompatModule, which delegates to WebSocketModule. Existing monex XQuery code using console:log() works unchanged.

Test plan

WebSocket pub/sub (3 tests):

  • Connect and receive heartbeat ping
  • Subscribe to channel and receive message
  • Channel count reflects subscribers

Eval endpoint (20 tests):

  • Simple eval (1 + 1"2")
  • Variables (external variable binding with type casting)
  • Compile error (syntax error returns error message)
  • Compile action (success and failure with diagnostics)
  • Streaming results (500 items, chunk-size 100 → 5 chunks)
  • Large result streaming (100K items, chunk-size 1000 → 100 chunks)
  • Cancellation (long query cancelled mid-flight)
  • Rapid cancel (cancel before compilation completes)
  • Max execution time (timeout enforcement via watchdog)
  • Timing (result includes parse/compile/eval/serialize breakdown)
  • Progress reporting (phase messages during execution)
  • Serialization options (adaptive method)
  • Binary result handling (xs:base64Binary)
  • Map/array serialization (adaptive output)
  • Module-load-path resolution (import module from /db)
  • Admin-cancel permission check (non-DBA gets denied)
  • Monitor channel (_monitor receives query lifecycle events)
  • Connection cleanup (disconnect mid-query releases resources)
  • Error recovery (invalid JSON doesn't kill connection)
  • Invalid message / missing query handling

Total: 23 integration tests

What This Enables

  • eXide: Stream results live, cancel runaway queries, see timing breakdown
  • Sandbox: Notebook cells with progress bars and incremental output
  • monex: Live query dashboard via _monitor channel
  • VS Code: "Run Query" with streaming results and cancel button
  • xst CLI: xst eval --stream prints results as they arrive

CI Status

Integration tests pass on macOS and ubuntu. Known CI issues:

  • ubuntu unit tests: Timeout — WebSocket integration tests add overhead to the already tight CI time limit
  • windows integration: Flaky WebSocket test failures due to platform-specific timing (passes locally)
  • Container image: Build failure (inherits from Jetty 12 base — merge Upgrade Jetty 11.0.25 to 12.0.16 (EE10) #6144 first)
  • Codacy: Pre-existing style issues — no new findings from this PR

🤖 Generated with Claude Code

joewiz and others added 6 commits March 16, 2026 23:32
Dependency version changes:
- Jetty: 11.0.25 -> 12.0.16
- Jakarta Servlet API: 5.0.0 -> 6.0.0 (EE10)
- Milton servlet: 1.8.1.3-jakarta5 -> 1.8.1.3-jakarta-ee10
  (fixes setStatus(int,String) removal in Servlet 6.0;
  temporarily uses com.evolvedbinary.thirdparty groupId —
  needs republishing under org.exist-db.thirdparty)

Jetty 12 artifact coordinate changes:
- Servlet-layer: org.eclipse.jetty -> org.eclipse.jetty.ee10
  (annotations, servlet, webapp, plus, jndi)
- WebSocket: org.eclipse.jetty.websocket -> org.eclipse.jetty.ee10.websocket
- JAAS: removed as separate dep (now in jetty-security core module)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Handler model:
- HandlerCollection -> Handler$Sequence (constructor takes Array arg)
- Server constructor arg: threadpool -> threadPool (case-sensitive)

Deployment:
- Replaced ContextProvider/scanner with inline addHandler on
  ContextHandlerCollection

Security and class paths:
- ConstraintSecurityHandler -> org.eclipse.jetty.ee10.servlet.security
- JAASLoginService -> org.eclipse.jetty.security.jaas
- DefaultServlet/IntrospectorCleaner -> ee10 packages
- ELContextCleaner removed (obsolete)

Compliance and other:
- MultiPartFormDataCompliance -> MultiPartCompliance
- UriCompliance: LEGACY -> UNSAFE (non-ASCII path support)
- SecureRequestCustomizer: constructor args -> setter-based
- GzipHandler: use addIncludedMethods
- Removed org.eclipse.jetty.util.log.Log references

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
JettyStart.java: Handler model rewrite, ResourceFactory, ee10 packages
WebAppContext.java: extends ee10 WebAppContext
HttpServletRequestWrapper.java: Servlet 6.0 additions/removals
XQueryURLRewrite.java, HttpServletResponseAdapter.java: removed
  setStatus(int, String)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- All web-app version="5.0" -> "6.0" with Servlet 6.0 schema refs
- Distribution XSDs: real Jakarta EE 10 schemas from jakarta.ee
- @ignore GetDataTest HTTP/0.9 and HTTP/1.0 tests (Jetty 12)
- DbStore2Test: Jetty 12 ResourceHandler/Handler.Sequence API

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
New LockTest with 3 tests:
- lockAndUnlockXmlDocument: LOCK returns token, UNLOCK succeeds
- lockAndUnlockBinDocument: same for binary documents
- relockReturnsFreshToken: re-LOCK by same user replaces the lock

Ensures LOCK support (Milton 1.x LockableResource) is preserved
across the Jetty 12 upgrade.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
New package: org.exist.xquery.functions.websocket

WebSocket endpoint (@serverendpoint("/ws")):
- Channel-based pub/sub: clients send {"channel": "name"} to subscribe
- sendAll(channel, data) broadcasts to subscribed clients
- 500ms heartbeat ping keeps connections alive through proxies
- Session management via ConcurrentHashMap

XQuery module (namespace: http://exist-db.org/xquery/websocket, prefix: ws):
- ws:log($items) / ws:log($channel, $items) — log to channel
- ws:send($channel, $items) — send JSON to channel
- ws:broadcast($items) — send to all clients
- ws:channel-count($channel) — count subscribers

Backward-compatible console module (http://exist-db.org/xquery/console):
- console:log() and console:send() delegate to WebSocketModule
- Existing monex XQuery code works unchanged

Ported from monex's RemoteConsoleEndpoint/RemoteConsoleAdapter/ConsoleModule.

Dependencies:
- jakarta.websocket-client-api 2.1.0 (provided scope)
- jakarta.websocket-api 2.1.0 (provided scope)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@joewiz joewiz requested a review from a team as a code owner March 17, 2026 04:40
Jetty 12 integration:
- JettyStart.configureWebSocket() finds the ServletContextHandler
  in the server's handler tree and registers the /ws endpoint via
  JakartaWebSocketServletContainerInitializer.configure()
- WebSocketEndpoint.initialize() sets up the adapter and heartbeat
  scheduler during server startup
- Added jetty-ee10-websocket-jakarta-server dependency to exist-core

Integration tests (WebSocketEndpointTest):
- connectAndReceiveHeartbeat: client connects, receives ping
- subscribeToChannelAndReceiveMessage: client subscribes to channel,
  ws:send() delivers message to subscribed client
- channelCountReflectsSubscribers: ws:channel-count() returns
  correct subscriber count

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@joewiz joewiz force-pushed the feature/websocket-core branch from 2d3f524 to 56f81a4 Compare March 17, 2026 05:42
joewiz added a commit to joewiz/monex that referenced this pull request Mar 17, 2026
BREAKING CHANGE: Requires eXist-db 7.0+ with WebSocket support.
Build system migrated from Apache Maven to Node.js/Gulp, following
the pattern established in eXist-db/semver.xq#69.

Java removal (requires eXist-db/exist#6145 WebSocket module):
- Removed all Java source (6 files, ~630 LOC)
- WebSocket/Console functions now provided by exist-core
- JMXToken reimplemented as pure XQuery monex:jmx-token()
- JS clients updated: /rconsole → /ws
- Cypress tests updated to match

De-mavenization (modeled on eXist-db/semver.xq#69):
- Replaced pom.xml + xar-assembly.xml with package.json + gulpfile.js
- XAR built via gulp: template substitution → copy → zip
- expath-pkg.xml.tmpl and repo.xml.tmpl with @variable@ placeholders
- build.xml Ant wrapper for backward compatibility
- New GitHub Actions CI workflow (Node.js-based)
- npm run build / npm run deploy / npm test

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
joewiz added a commit to joewiz/monex that referenced this pull request Mar 17, 2026
BREAKING CHANGE: Requires eXist-db 7.0+ with WebSocket support.
Build system migrated from Apache Maven to Node.js/Gulp, following
the pattern established in eXist-db/semver.xq#69.

Java removal (requires eXist-db/exist#6145 WebSocket module):
- Removed all Java source (6 files, ~630 LOC)
- WebSocket/Console functions now provided by exist-core
- JMXToken reimplemented as pure XQuery monex:jmx-token()
- JS clients updated: /rconsole → /ws
- Cypress tests updated to match

De-mavenization (modeled on eXist-db/semver.xq#69):
- Replaced pom.xml + xar-assembly.xml with package.json + gulpfile.js
- XAR built via gulp: template substitution → copy → zip
- expath-pkg.xml.tmpl and repo.xml.tmpl with @variable@ placeholders
- build.xml Ant wrapper for backward compatibility
- New GitHub Actions CI workflow (Node.js-based)
- npm run build / npm run deploy / npm test

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
joewiz added a commit to joewiz/monex that referenced this pull request Mar 17, 2026
BREAKING CHANGE: Requires eXist-db 7.0+ with WebSocket support.
Build system migrated from Apache Maven to Node.js/Gulp, following
the pattern established in eXist-db/semver.xq#69.

Java removal (requires eXist-db/exist#6145 WebSocket module):
- Removed all Java source (6 files, ~630 LOC)
- WebSocket/Console functions now provided by exist-core
- JMXToken reimplemented as pure XQuery monex:jmx-token()
- JS clients updated: /rconsole → /ws
- Cypress tests updated to match

De-mavenization (modeled on eXist-db/semver.xq#69):
- Replaced pom.xml + xar-assembly.xml with package.json + gulpfile.js
- XAR built via gulp: template substitution → copy → zip
- expath-pkg.xml.tmpl and repo.xml.tmpl with @variable@ placeholders
- build.xml Ant wrapper for backward compatibility
- New GitHub Actions CI workflow (Node.js-based)
- npm run build / npm run deploy / npm test

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
joewiz and others added 3 commits March 21, 2026 18:53
console:log is now a valid built-in function via ConsoleCompatModule
on the WebSocket branch. Use nonexistent:foo instead to test
unknown function error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…tion

Add a new WebSocket endpoint at /ws/eval that enables real-time,
streaming XQuery evaluation with progress reporting, cancellation,
and timing breakdown. This is the XQuery equivalent of a modern
database's interactive query console protocol.

Protocol supports:
- eval: Execute queries with streaming results in configurable chunks
- cancel: Cancel running queries via XQueryWatchDog
- compile: Compile-check queries without execution

Server responds with typed JSON messages:
- progress: Phase updates (parsing/compiling/evaluating/serializing)
- result: Serialized data chunks with more/done indicators
- error: XPath errors with code, message, line, column
- cancelled: Confirmation with items-produced count
- compile: Success/failure with diagnostics

Authentication via Basic auth on WebSocket handshake, falling back
to guest. Queries execute on worker threads with BrokerPool integration.

New classes in org.exist.http.ws:
- EvalWebSocketEndpoint: @serverendpoint with auth configurator
- EvalSession: Per-connection state and query cancellation
- QueryExecutor: XQuery execution with streaming and progress
- EvalProtocol: JSON message parsing and generation

13 integration tests covering: simple eval, variables, streaming
chunks, cancellation, compile check, progress, timing, serialization
options, timeout, error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Bridge /ws/eval query lifecycle events to the /ws pub/sub channel system,
enabling admin monitoring clients to receive real-time query progress.

- QueryMonitorBroadcaster: broadcasts started/progress/completed/error/
  cancelled events to _monitor channel; periodic 1s snapshot of ALL
  running queries from ProcessMonitor (covers REST, URL rewrite, etc.)
- EvalWebSocketEndpoint: admin-cancel action for DBA users to kill any
  running query cross-session via ProcessMonitor
- WebSocketEndpoint: sendAll made public; snapshot scheduler added
- EvalProtocol: ACTION_ADMIN_CANCEL constant

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
joewiz added a commit to joewiz/xst that referenced this pull request Mar 22, 2026
Include required 'id' field in all messages, handle chunked 'result'
messages with 'more' boolean, read timing from final result chunk,
and handle 'cancelled' response type for SIGINT cancellation.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
joewiz added a commit to joewiz/xst that referenced this pull request Mar 22, 2026
Add streaming query execution via WebSocket to the existing exec command.
--stream connects to the /exist/ws/eval endpoint and prints results as
they arrive, with Ctrl+C cancellation support. --timing reports execution
timing in both HTTP and streaming modes.

Depends on the server-side WebSocket eval endpoint (eXist-db/exist#6145).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add 10 new integration tests (23 total) covering:

- Concurrent queries: two eval actions on the same connection
- Large result streaming: 100K items with chunk-size verification
- Binary result handling: xs:base64Binary serialization
- Map/array result serialization: adaptive output method
- Module-load-path resolution: import module stored in /db
- Admin-cancel permission check: non-DBA gets permission denied
- Monitor channel: _monitor subscription receives query lifecycle events
- Connection cleanup: disconnect mid-query releases server resources
- Error recovery: invalid JSON doesn't kill the connection
- Rapid cancel: cancel immediately after eval before compilation

Also fix long-running query pattern in timeout tests: use
string($i) to force watchdog checks instead of lazy RangeSequence.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@joewiz joewiz changed the title Add WebSocket module to exist-core Add WebSocket module and streaming query evaluation to exist-core Mar 22, 2026
CI runners are 3-5x slower than local machines. Increase await()
timeouts from 10s to 30s in cancellation, maxExecutionTime, and
rapidCancel tests. Also increase max-execution-time from 500ms to
2000ms to ensure the watchdog has time to activate on slow runners.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant