Skip to content

Improve hx-ws messages and reconnects - #3910

Open
scriptogre wants to merge 28 commits into
bigskysoftware:four-devfrom
scriptogre:4.0/improve-ws-ext
Open

Improve hx-ws messages and reconnects#3910
scriptogre wants to merge 28 commits into
bigskysoftware:four-devfrom
scriptogre:4.0/improve-ws-ext

Conversation

@scriptogre

@scriptogre scriptogre commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Redesign hx-ws around element-owned connections and one lifecycle for incoming and outgoing messages.

Messages stay ordered while sockets open or reconnect, like in htmx 2.0. Only configured close codes retry. Sender correlation and URL pooling are removed.

Connections

<div hx-ws:connect="/actions">
  <button hx-ws:send>Save</button>
  <button hx-ws:send>Delete</button>
</div>
  • hx-ws:send uses the nearest hx-ws:connect, including one on the same element.
  • Separate owners get separate connections, even for the same URL.
  • Incoming events and swaps use the owner.
  • Outgoing events use the sender.

An hx-ws:send value no longer opens a connection and is ignored, matching htmx 2.0's ws-send marker. Put hx-ws:connect on the same element to make the sender own its connection.

Messages

Both directions use:

event.detail = {
  connection,
  message,
  waitUntil,
  cancelled
}
// Incoming
message = { data, text(), json() }

// Outgoing
message = { headers, values, data }

Before hooks can:

  • Delay work with waitUntil(promise).
  • Cancel with event.preventDefault() or detail.cancelled = true.
  • Replace incoming or outgoing data through message.data.

Format

Outgoing values move from body to the top level:

// Before
{ "headers": { /* ... */ }, "body": { "message": "Hello" } }

// After
{ "headers": { /* ... */ }, "message": "Hello" }

Events

Before After
htmx:before:ws:connection htmx:ws:before:connection
htmx:after:ws:connection htmx:ws:after:connection
htmx:before:ws:request htmx:ws:before:message:outgoing
htmx:after:ws:request htmx:ws:after:message:outgoing
htmx:before:ws:message htmx:ws:before:message:incoming
htmx:after:ws:message htmx:ws:after:message:incoming

htmx:ws:error identifies an existing connection, or the requested url when no connection exists.

Order

  • Outgoing messages queue while opening or reconnecting.
  • Queued messages send in trigger order.
  • Incoming messages finish in arrival order.
  • ws.maxOutgoingMessagesQueueSize bounds the queue.

This restores htmx 2's queue behavior.

Reconnect

Reconnect requires:

  • ws.reconnect enabled.
  • A close code in ws.reconnectCodes.
  • A connected owner.

Code 1000 stops by default. A later trigger can open a new connection.

Config comes from global htmx.config.ws and local hx-config values.

Correlation

Removed:

  • HX-Request-ID routing.
  • request_id routing.
  • Sender maps and correlation TTLs.

Incoming messages use the connection owner.

@scriptogre scriptogre changed the title Improve WebSocket messages and reconnects Improve hx-ws messages and reconnects Aug 10, 2026
@stukennedy

Copy link
Copy Markdown

Good work on this. The element-owned connections and lifecycle events fix the orphan/pending-cleanup issues from the 4.0/ws-alignment review properly instead of patching around them, and waitUntil() is a nice addition.

A few thoughts on message handling. These are direction questions rather than implementation ones:

1. Should incoming fragments be addressed by ID instead of resolved by content?

Right now messages with target/content/swap fields trigger target resolution plus swap logic, with the swap:none fallback when they're absent. An alternative worth considering: if the payload carries an element ID, morph or replace that element by ID and skip target resolution entirely. That makes the routing model uniform. Messages address elements, elements own connections. It pairs naturally with the new ownership model, and it lets servers send idempotent patches without caring what was previously in the DOM. Probably not for this PR, but if the wire format is changing anyway (HX-Request-ID removal), this is the cheapest time to reserve a shape for it.

2. Versioning the outgoing wire format

With the correlation system gone, servers speaking the old protocol will silently degrade. A type field or version header on outbound messages would let backends detect and adapt, and gives room for future message kinds without magic-field sniffing. Even a small closed vocabulary, fragment / event, makes both client dispatch and the server-side story easier to document.

3. Connection-per-element trade-off

Element ownership fixes the orphan bug, but a page with several hx-ws:connect elements now holds several sockets to the same URL. Worth a docs note on the expected pattern (one connection element per page, listeners attach to it) so people don't accidentally fan out sockets. And if shared connections ever come back, element-ID-addressed routing is what makes sharing safe again. Another argument for reserving that shape.

The implementation itself is solid. These are mostly about locking in the wire format before it calcifies.

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.

2 participants