FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Plugin api: WebSocket upgrade routing for realtime apps (api.ws.route) · Issue #588 · JavaScriptSolidServer/JavaScriptSolidServer · GitHub

Plugin api: WebSocket upgrade routing for realtime apps (api.ws.route) #588

Description

Third seam from the #206 plugin-zero exercise. appPaths (#582, shipped 0.0.213) and getAgent (#584, shipped 0.0.214) were forced by Tideholm; this one comes from the first realtime consumer, bridge — an online card game now composed with JSS and live at https://nostr.social/bridge/. Full findings log: jss-plugin/NOTES.md.

What works today (no JSS changes)

The HTTP half of a WS app is just another app: appPaths + the scoped pass-through parser + reply.hijack(). Auth composes too — the client authenticates a plain HTTP request (/bridge/auth/nip98), getAgent verifies it (NIP-98 and pod Bearer both work), and the app mints a one-use short-TTL ticket presented in the first socket message. Credentials never cross the upgrade.

What has no seam

Fastify routes never see HTTP upgrades — they're a bare 'upgrade' event on the node server. So a realtime app has no sanctioned mount point; today the composer reaches around the api:

await fastify.listen({ port });
game.attachUpgrade(fastify.server);   // routes <prefix>/ws, after listen

Two sharp edges found doing it:

  1. The listener foot-gun. While a node http server has no 'upgrade' listener, it destroys stray upgrade attempts itself. The moment anyone attaches one, every unclaimed upgrade is their problem — an unhandled socket neither errors nor closes, it just hangs open forever (found empirically: a stray ws://host/elsewhere connect simply never resolved). Each plugin working around this independently means each assumes it owns the only listener — two realtime plugins on one host would fight or leak sockets.
  2. @fastify/websocket (used by the tunnel) also owns upgrade handling, so a plugin attaching its own listener coexists by luck, not contract (WebSocket upgrades fail intermittently under pm2: ERR_HTTP_SOCKET_ASSIGNED (@pm2/io httpMetrics vs @fastify/websocket) #545 is adjacent).

Proposal

A tiny loader-owned dispatcher (~15 lines):

api.ws.route(pathPrefix, (req, socket, head) => { ... })

The loader attaches the single 'upgrade' listener, dispatches by longest matching prefix, and destroys unclaimed sockets. Plugins bring their own ws/WebSocketServer({ noServer: true }) — JSS takes no dependency and does no protocol work; it only routes and enforces the destroy-unclaimed invariant.

Until the #206 loader exists, even documenting the fastify.server + attach-after-listen pattern (with edge 1 spelled out) on the plugins docs page would save the next app the debugging.

Activity

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

    pluginCould be implemented as a plugin (#206); core/plugin line defined in #564

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL