Skip to content

package: wheels-websockets — opt-in realtime WS transport (RustCFML native, Lucee emulated) #3292

Description

@bpamiri

What

New first-party package wheels-websockets: an opt-in realtime WebSocket transport for Wheels channels. Core stays SSE-only (the AdonisJS/Transmit position, unchanged); this package adds WS as an additive transport for the same publish()/subscribeToChannel() semantics where the engine can serve it.

Decision record: #3154 (GO as package, 2026-07-06). Parent roadmap: #2962. Demand probe (now prioritizing engine backends + features): #3286.

Engine strategy

Engine Backend Status
RustCFML Native (wsPublish BIF + channel CFCs, shipped v0.299–v0.303) Day one — bridge seam proven live 2026-07-06: HTTP request → wsPublish() → connected WS client, on stock v0.423
Lucee 6/7 Emulation over the official lucee/extension-websocket (active, updated 2026-06) — fallback candidate: isapir/lucee-websocket (JSR-356, updated 2026-07) Day one — API spike first (extension surface → emulation shape)
Adobe / BoxLang None (cfwebsocket deprecated / absent) Demand-gated via #3286; package degrades gracefully — SSE channels keep working

Programming model — adopt the RustCFML surface

Rather than inventing a third CFML realtime API, the package adopts the channel-CFC shape RustCFML shipped (one CFC per channel, convention lifecycle onConnect/onMessage/onDisconnect/onError, connect reject gate, rooms, io() emitter, ack-by-return):

  • On RustCFML: channel CFCs are served natively by the engine; the package contributes only the channels bridge, config, and the JS client.
  • On Lucee: the package hosts the same CFC contract over the WS extension — instantiates the app's channel CFCs and provides socket/io() shims.
  • Everywhere: publish() on the Wheels channel bus fans out to connected WS clients when a backend is live.

Architecture

  • Channel-bus hook (zero core changes): Global.cfc::$getChannelEngine() lazily creates application.wheels.channelEngine; the package (loaded at app start) pre-installs a decorator that forwards every publish() to the wrapped engine AND the WS transport. Same for the database adapter singleton.
  • Engine detection at package load: feature-detect (wsPublish BIF → RustCFML; WS extension present → Lucee; else inactive with a one-time wheels.log notice). Per-package error isolation means a broken backend can never take the app down.
  • JS client: WheelsRealtime — attempts WS, falls back to the existing WheelsSSE transparently, one subscription API.
  • Package manifest: provides.mixins: global for the app-facing helpers; wheelsVersion >= 4.0.

Phasing

  • P1 — RustCFML backend + channels bridge: decorator hook, engine detection, JS client, docs, specs; verify against a Wheels app served by RustCFML (ladder rig)
  • P2 — Lucee emulation: spike lucee/extension-websocket's API; implement the channel-CFC host + socket/io() shims; CommandBox/undertow compatibility check
  • P3 — registry publish: standalone repo wheels-dev/wheels-websockets, two-PR registry flow, wheels packages add websockets
  • P4 — demand-gated: Adobe/BoxLang backends if Do you need WebSockets in Wheels? Real-world demand check for channels #3286 shows demand; requires.engines manifest field proposal if gating should become declarative
  • Upstream ask (RustCFML): configurable channel-CFC directory (Wheels apps would prefer app/websockets/ over webroot public/websockets/)

Evidence

  • RustCFML spike (2026-07-06): WS handshake 101 → welcome frame; then plain HTTP .cfm calling wsPublish(channel="/chat", event="bridged", ...) delivered the frame to the connected client. The bridge is a thin adapter, not an integration project.
  • Full surface reference: docs/websockets.md in the RustCFML repo (rooms, presence, acks, lastEventId resumability, multi-node fan-out via memberlist, raw-WS + socket.io wires, socket.io-lucee compat).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions