honojs / hono

Web framework built on Web Standards
https://hono.dev
MIT License
18.93k stars 538 forks source link

feat(WebSocket): Add Pub/Sub feature #3466

Open hayatosc opened 19 hours ago

hayatosc commented 19 hours ago

relate to #3230

Desctiption

This PR will allow us to use Publish/Subscribe messaging model on WebSocket like Bun can do natively.

Demo

Bun

Bun has own Pub/Sub API.

import { Hono } from 'hono'
import { createBunWebSocket } from 'hono/bun'
import type { ServerWebSocket } from 'bun'

const app = new Hono()
const { upgradeWebSocket, websocket } =
  createBunWebSocket<ServerWebSocket>()

app.get(
  '/ws',
  upgradeWebSocket((c) => {
    return {
      onMessage(event, ws) {
        ws.subscribe('chatroom')
        server.publish('Hello!')
      }
    }
  })
)

const server = Bun.serve({
  fetch: app.fetch,
  websocket,
})

Other runtimes

Other runtimes such as Deno will get minimal PubSub class.

import { Hono } from 'hono'
import { upgradeWebSocket } from 'hono/deno'
import { WSPubSub } from 'hono/websocket'

const app = new Hono()
const pubsub = new WSPubSub()

app.get(
  '/ws',
  upgradeWebSocket(pubsub, (c) => {
    return {
      onMessage(event, ws) {
        ws.subscribe('chatroom')
        pubsub.publish('Hello!')
      }
    }
  })
)

I don't know this method of implementation will accept all people. So if you have the opinion, please comment.

The author should do the following, if applicable

hayatosc commented 19 hours ago

How do you think, @yusukebe @nakasyou ?

I think the implementation of Other runtime may have a more good solution.

nakasyou commented 18 hours ago

It looks very good to me. I like it.

I have an idea. How about like that?:

const chatroom1 = new PubSub()
app.get(
  '/ws',
  upgradeWebSocket((c) => {
    return {
      onMessage(event, ws) {
        chatroom1.publish('Hello World')
      },
      pubSub: chatroom1
    }
  })
)

I think it's more extendable.

And in my opinion, I think we have to avoid extending WSContext and WSEvents for each runtime. I have no idea to implement it, but I think it should be made clear that the API is runtime-specific.

hayatosc commented 18 hours ago

@nakasyou

It looks very good to me. I like it.

I have an idea. How about like that?:

const chatroom1 = new PubSub()
app.get(
  '/ws',
  upgradeWebSocket((c) => {
    return {
      onMessage(event, ws) {
        chatroom1.publish('Hello World')
      },
      pubSub: chatroom1
    }
  })
)

I think it's more extendable.

And in my opinion, I thinkwe have to avoid extending WSContext and WSEvents for each runtime. I have no idea to implement it, but I think it should be made clear that the API is runtime-specific.

Hmm. Your opinion was not bad for me.

However, most of actual use cases need multiple topics, so there is a risk that the completed code will become complicated.

app.get(
  '/ws/:id',
  upgradeWebSocket((c) => {
    return {
      onMessage(event, ws) {
        const { id } = c.req.param()
        pubsub.subscribe(`chatroom-${id}`) // we want to do this
      }
    }
  })
)

Individually, also in the context of code compatibility, I feel we should follow Bun's Pub/Sub API as possible.

nakasyou commented 8 hours ago

@hayatosc

However, most of actual use cases need multiple topics, so there is a risk that the completed code will become complicated.

I agree with you. So, what do you think about that?

// Bun
const {
  upgradeWebSocket, 
  websocket,
  pubSub // Provides pubSub instance for Bun
} = createBunWebSocket<ServerWebSocket>()
app.get(
  '/ws/:id',
  upgradeWebSocket((c) => {
    const { id } = c.req.param()
    return {
      onOpen(_evt, ws) {
        ws.subscribe(`chatroom-${id}`) // Use received `pubSub`
      },
      onMessage(event, ws) {
        pubsub.publish(`chatroom-${id}`, event.data)
      },
      pubSub
    }
  })
)
// Other runtimes
const pubSub = new PubSub()
app.get(
  '/ws/:id',
  upgradeWebSocket((c) => {
    const { id } = c.req.param()
    return {
      onOpen(_evt, ws) {
        ws.subscribe(`chatroom-${id}`) // Use received `pubSub`
      },
      onMessage(event, ws) {
        pubsub.publish(`chatroom-${id}`, event.data)
      },
      pubSub
    }
  })
)

In this idea, users can switch runtime easier. And it doesn't extends WSContext and WSEvents for only Bun.