Skip to content

Hrana WebSocket: support JSON over WebSocket (phase 1) #187

Closed

Description

@magnusp

Goal

Speak Hrana 3 JSON over WebSocket (hrana3 subprotocol) to sqld, selected by the JDBC URL. This is phase 1 of the Hrana WebSocket work tracked in #189.

What the code already does, and what has to change

The existing client is HTTP-pipeline-shaped in two places that do not carry over to WebSocket:

  1. The codec is baton-shaped. StreamRequest / StreamResponse in libsql-hrana-codec model the HTTP v3/pipeline bodies: type: "close" with no stream_id, and the HranaJsonCodec SPI is serializePipelineRequest / deserializePipelineResponse. Over WebSocket the envelope is different: a ClientMsg (hello | request) and ServerMsg (hello_ok | hello_error | response_ok | response_error), each stream request carries a client-assigned stream_id, and close becomes close_stream with open_stream added. Only the leaf structures (Stmt, StmtResult, Batch, BatchResult, Error, Value, DescribeResult) are shared.
  2. The connection is coupled to the HTTP client and its baton. LibsqlConnection depends directly on the concrete LibsqlHttpClient and derives "is a stream open" from baton == null (beginPending()). WebSocket has no baton; the client owns a stream_id. Introduce a stream-level seam (open / execute / batch / close, with error mapping) implemented by both LibsqlHttpClient (baton) and the new LibsqlWebSocketClient (stream_id), and migrate LibsqlConnection to depend on the seam.

Scope

  • libsql-hrana-codec: WS envelope models (ClientMsg / ServerMsg / request_id) and WS request/response variants (open_stream, close_stream, execute, batch, sequence, describe, store_sql, close_sql, get_autocommit — each stream op with its stream_id), plus the JSON codec for them. Reuse the leaf serialization from the existing JSON codec.
  • libsql-client: LibsqlWebSocketClient on java.net.http.WebSocket (no new dependency), and the seam interface that LibsqlHttpClient also implements.
  • libsql-jdbc: LibsqlUrlParser.acceptsUrl and parse gain ws / wss; LibsqlDriver selects the client from the scheme; LibsqlConnection migrates to the seam.
  • Auth: HTTP sends Authorization: Bearer <token>; WebSocket sends the same token as the jwt field of hello.
  • Handshake: offer hrana3 in Sec-WebSocket-Protocol; send hello first and fail the connection if the server replies hello_error or selects an unexpected subprotocol. Text frames (opcode 0x1).
  • Correlation: the client assigns a request_id, sends, and matches the response by request_id (the server may reorder responses). With a single outstanding request this is trivial, but the matching logic is a correctness requirement.
  • Busy handling: preserve the existing SQLITE_BUSY retry/backoff semantics. The HTTP-only 429/503 branch has no WebSocket equivalent; over WebSocket a busy DB arrives as a response_error with a busy code, and must retry the same way.

Out of scope

  • Cursor ops (open_cursor / close_cursor / fetch_cursor). Nothing in the driver uses them today, and the HTTP path does not either.
  • Protobuf encoding (phase 2, Hrana WebSocket: support Protobuf over WebSocket (phase 2) #188).
  • Any change to the HTTP transport's behavior.
  • Sharing one socket across JDBC connections (the protocol supports it, but it is not needed for a synchronous driver).

Source of truth

vendor/HRANA_3_SPEC.md, "Hrana over WebSocket": the subprotocol table, hello, request/response messages, ordering, and close codes.

Acceptance (outside check)

  • Unit tests: round-trip the WS envelope codec; a local fake WebSocket server asserts the hello handshake and request_id correlation. Assertions check observable JDBC results, not internal frames.
  • Integration tests: JDBC over ws:// against the pinned sqld image (libsql.image in the root pom.xml) run the same basic query, transaction, and error cases the HTTP itests already cover.
  • spotless:check and ./mvnw test pass; itests pass with the documented DOCKER_CONFIG override.

Open decisions

  1. URL scheme. Proposed: jdbc:libsql:ws:// and jdbc:libsql:wss://, mirroring http / https. Alternative: keep http URLs and select the transport with a property.
  2. Connection model. Proposed: one WebSocket per JDBC connection. Alternative: a shared socket per URL (a pool).
  3. TLS for wss. Proposed: the JDK default trust store only. Alternative: a configurable truststore.
  4. Dropped socket. Proposed: fail the JDBC connection with an SQLException. Transparent reconnect would lose open streams and transaction state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions