🔌 wo-adaptor-jetty

A Jetty-based WOAdaptor. Based on Jetty's own servlet-free APIs making it's relatively simple and lightweigt.

Usage

Releases are deployed to the WOCommunity maven repository, so if your environment is set up for WO development just add the dependency to your pom. The adaptor selects itself at startup, so no launch argument is needed; pass -WOAdaptor explicitly only when you want some other adaptor. Requires JDK 25.

<dependency>
	<groupId>is.rebbi</groupId>
	<artifactId>wo-adaptor-jetty</artifactId>
	<version>0.12.2</version>
</dependency>

Why?

  • WebSockets.
  • SSE (Server-sent events).
  • I like having control over the HTTP-serving part of my apps.
  • The main adaptor for ng-objects is Jetty-based so a Jetty-based WOAdaptor allows WO and ng to be served from the same app/Jetty server instance.

Does it work?

Works great. Since November 2025 I use it in all of my applications, some of which handle quite a bit of traffic, large file uploads, multipart requests etc.

Performance?

Running ab locally on my MacBook M4 to serve a simple ~230kb resource returns the following results, so performance is pretty great. Disabling KeepAlive (removing the -k parameter) slows us down by roughly half. Which is probably a more realistic benchmark. Not that common for legit clients to make 15.000 requests/sec to your app.

# ab -k -n 10000 -c 16 http://127.0.0.1:1200/Apps/WebObjects/Hugi.woa/res/app/ZillaSlab-Light.ttf

Concurrency Level:      16
Time taken for tests:   0.692 seconds
Complete requests:      10000
Failed requests:        0
Keep-Alive requests:    10000
Total transferred:      2399870000 bytes
HTML transferred:       2398360000 bytes
Requests per second:    14460.19 [#/sec] (mean)
Time per request:       1.106 [ms] (mean)
Time per request:       0.069 [ms] (mean, across all concurrent requests)
Transfer rate:          3388922.70 [Kbytes/sec] received

Tests

mvn verify                 # the test suite
mvn verify -Pintegration   # also the slower tests that launch a separate JVM against the packaged framework

The tests start the adaptor's default server on a free port in front of a real WOApplication, and talk raw HTTP to it. Each regression test names the fix it guards. The integration tests cover the adaptor selecting itself at startup, which happens while bundles load and so can only be observed in a freshly launched application.

Experimental: server push

Server-sent events and WebSockets. Both work and are in use, but their API may still change before it settles, so expect to adjust on upgrade.

Server push lives in a separate module, wo-adaptor-jetty-push, released together with the adaptor and used at the same version. Server-sent events arrive with 1.0.0; 0.9.0 shipped the module as wo-adaptor-jetty-websocket, with WebSockets only.

<dependency>
	<groupId>is.rebbi</groupId>
	<artifactId>wo-adaptor-jetty-push</artifactId>
	<version>0.12.2</version>
</dependency>

Server-sent events need nothing beyond the module: return an SSEStream's response from any action and keep sending to it.

private static final SSEHub TICKER = new SSEHub();

public WOActionResults eventsAction() {
	return TICKER.open().response();          // WO returns at once; the response stays open
}

// later, from anywhere:
TICKER.broadcast( "tick", Instant.now().toString() );

SSEStream queues events (send never blocks), writes a keep-alive comment every 30 seconds while quiet, and closes itself when the client goes away, at which point onClose listeners run and an SSEHub drops it. The adaptor streams the response with chunked transfer encoding, so this works only with wo-adaptor-jetty, not WO's classic adaptor. If you use JettyMaxConcurrentRequests, exclude your event paths with JettyQoSExcludedPaths (comma-separated Jetty path specs such as /sse/*), since an open response holds its permit for as long as it lives.

WebSockets are enabled by the module's presence on the classpath: the adaptor discovers it and adds WebSocket upgrades to its default server. Register endpoints from your Application class with WOWebSocketRegistry.register( "/ws/chat", ChatHandler.class ). If you build your own Jetty server through JettyServerProvider, wrap your handler with WOJettyWebSocketSupport.createWebSocketHandler( server, handler ) yourself, since the automatic discovery only applies to the default server.

Changelog

See CHANGES.md.