Broadcasting
Formidable Events Broadcaster is a user-friendly solution for implementing real-time event broadcasting between your server and frontend applications. By leveraging server-sent events (SSE), broadcasting enables seamless communication and instant updates whenever important events occur.
With Formidable Events Broadcaster, you can easily push notifications, update live data, and trigger actions on the frontend in response to server-side events. The beauty of broadcasting lies in its simplicity and efficiency, eliminating the need for complex setups like AJAX polling or WebSocket configurations.
Let's consider a simple example. Imagine you're building a live chat application. With broadcasting, new chat messages can be instantly sent from the server to all connected clients. As soon as a user sends a message, the server broadcasts it to all other participants, updating their chat interface in real-time.
Getting Started
Formidable Events Broadcaster doesn't come pre-installed with Formidable, so you'll need to install and configure it yourself. Not to worry, though, it's a breeze to set up.
Prerequisites
- The Formidable Framework >= 0.21.0
- Redis
Installation
Install the package using your preferred package manager:
- npm
- pnpm
- yarn
- bun
npm install @formidablejs/broadcaster@0.1.1
pnpm install @formidablejs/broadcaster@0.1.1
yarn add @formidablejs/broadcaster@0.1.1
bun add @formidablejs/broadcaster@0.1.1
Publish
Once installed, we can publish the vendor files:
node craftsman package:publish --package=@formidablejs/broadcaster --tag=vendor
This will publish the following files:
├── app
│ └── Resolvers
│ └── BroadcastServiceResolver.{ts,imba}
├── config
│ └── broadcaster.{ts,imba}
└── routes
└── channels.{ts,imba}
Next, you will need to register the broadcasting config file in the config/index.imba
or config/index.ts
file:
- Imba
- TypeScript
...
import broadcasting from './broadcasting'
...
export class Config < ConfigRepository
# All of the configuration items.
#
# @type {object}
get registered
return {
...
broadcasting
...
}
...
import broadcasting from './broadcasting'
...
export class Config extends ConfigRepository
{
/**
* All of the configuration items.
*/
get registered(): object
{
return {
...
broadcasting,
...
}
}
}
And finally, register the BroadcastServiceResolver
in the bootstrap/resolvers.imba
or bootstrap/resolvers.ts
file:
- Imba
- TypeScript
...
import BroadcastServiceResolver from '../app/Resolvers/BroadcastServiceResolver'
...
export default [
...
BroadcastServiceResolver
...
]
...
import BroadcastServiceResolver from '../app/Resolvers/BroadcastServiceResolver'
...
export default [
...
BroadcastServiceResolver,
...
]
Configuration
The broadcasting configuration file is located at config/broadcasting.{ts,imba}
. This file allows you to configure the redis connection, the channels prefix and middleware. In most cases, you will not need to modify this file. Broadcasting will work out of the box with the default configuration. However, you may need to modify the configuration if you want to use a different redis connection, prefix or middleware.
Prefix
The prefix
option allows you to configure the prefix for all channels path:
- Imba
- TypeScript
export default {
...
prefix: '_broadcast'
...
}
export default {
...
prefix: '_broadcast',
...
}
Changing the prefix
option in your config, will require you to also change it in the bootstrap file located at resources/js/bootstrap.ts
or resources/frontend/bootstrap.imba
:
- Imba
- TypeScript
globalThis.BroadcastConfig = {
prefix: '_broadcast'
}
window.BroadcastConfig = {
prefix: '_broadcast',
}
Middleware
The middleware
option allows you to configure the middleware that will be applied to all channels:
- Imba
- TypeScript
export default {
...
middleware: ['csrf:allow-get']
...
}
export default {
...
middleware: ['csrf:allow-get'],
...
}
Redis
The redis
object allows you to configure the redis connection name and expiration information:
- Imba
- TypeScript
export default {
...
redis: {
connection: 'default'
expiration: {
mode: 'PX'
ttl: 300
}
}
...
}
export default {
...
redis: {
connection: 'default',
expiration: {
mode: 'PX',
ttl: 300,
},
},
...
}
Cache Configuration
Finally, you may cache all of your broadcasting configuration into a single file using the config:cache
Artisan command. This will combine all of your broadcasting configuration options into a single file which will be loaded quickly by the framework. Caching your configuration provides a significant performance boost when configuring the broadcasting service for the first time:
node craftsman config:cache
Whenever you make changes to the broadcasting configuration, you should run the config:cache
command. This will clear the configuration cache so that fresh configuration values will be loaded on the next request.
Defining Broadcasts
Broadcasts are channels that broadcast messages to other clients. For example, a chat application may broadcast messages to a conversation channel. All clients listening on that channel will receive the message. Broadcasts may be defined using the channel
method on the Broadcast
class. The channel
method accepts two arguments: the channel name and the data that should be broadcast to the channel:
- Imba
- TypeScript
import { Broadcast } from '@formidablejs/broadcaster'
Broadcast.channel('chat')
import { Broadcast } from '@formidablejs/broadcaster'
Broadcast.channel('chat')
Authorizing Channels
Before broadcasting to a channel, you should authorize that the currently authenticated user can actually listen on the channel. For example, if you are broadcasting to a private chat channel, you should verify that the authenticated user is actually authorized to listen on that channel. You may do this by checking if a User
property is valid on the data payload:
- Imba
- TypeScript
import { Broadcast } from '@formidablejs/broadcaster'
Broadcast.channel('chat', do(message) message.user !== null)
import { Broadcast } from '@formidablejs/broadcaster'
Broadcast.channel('chat', message => message.user !== null)
If the channel
method returns false
, the user will be denied access to the channel. If the channel
method returns true
, the user will be authorized to listen on the channel.
Parameterized Channels
Sometimes you may need to broadcast to a channel that requires parameters. For example, you may need to broadcast to a specific user's chat channel. You may accomplish this by passing your channel parameters as channel parameters to the channel
method:
- Imba
- TypeScript
import { Broadcast } from '@formidablejs/broadcaster'
import { ConversationRepository } from '../app/Repositories/ConversationRepository'
Broadcast.channel('chat/:chat_id/:conversation_id', do({ user, params })
ConversationRepository.canAccess(user, params.chat_id, params.conversation_id)
)
import { Broadcast } from '@formidablejs/broadcaster'
import { ConversationRepository } from '../app/Repositories/ConversationRepository'
Broadcast.channel('chat/:chat_id/:conversation_id', ({ user, params }) => {
return ConversationRepository.canAccess(user, params.chat_id, params.conversation_id)
})
Broadcasting To Channels
To Broadcast to a channel, you can use the Channel
class:
- Imba
- TypeScript
import { Channel } from '@formidablejs/broadcaster'
Channel.publish('message').on('channel-name')
import { Channel } from '@formidablejs/broadcaster'
Channel.publish('message').on('channel-name')
Listening For Broadcasts
To listen for broadcasts, you may use the subscribe
helper function from the @formidablejs/broadcaster
package. The subscribe
function accepts two arguments: the channel name and a options object. The options object may contain the following properties:
Property | Type | Description |
---|---|---|
onMessage | Function | The callback that will be called when a message is received. |
onError | Function | The callback that will be called when an error occurs. |
onReady | Function | The callback that will be called when the connection is ready. |
The subscribe
function returns a EventSource
instance. You may use this instance to close the connection or to check the connection state.
Subscribing To A Channel
As mentioned above, the subscribe
function accepts two arguments: the channel name and a options object. To subscribe to a channel, you may use the following syntax:
- Imba
- Vue
- React
- Svelte
import { subscribe } from '@formidablejs/broadcaster/src/client'
export tag Chat
messages\string[] = []
def mount
subscribe('chat', {
onMessage: do(message)
messages.push(message)
imba.commit!
})
def render
<self>
for message in messages
<div> message
<script lang="ts" setup>
import { subscribe } from '@formidablejs/broadcaster/src/client'
import { onMounted, ref } from 'vue'
const messages = ref<string[]>([]);
onMounted(() => {
subscribe('chat', {
onMessage: (message: string) => messages.value.push(message),
})
})
</script>
<template>
<div>
<div v-for="(message, i) in messages" :key="i">{{ message }}</div>
</div>
</template>
import { subscribe } from '@formidablejs/broadcaster/src/client'
import { useEffect, useState } from 'react'
export default function Chat() {
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
subscribe("chat", {
onMessage: (message: string) => setMessages((messages) => [...messages, message]),
})
}, [])
return (
<div>
{messages.map((message, i) => (
<div key={i}>{message}</div>
))}
</div>
)
}
<script>
import { subscribe } from '@formidablejs/broadcaster/src/client'
import { onMount } from 'svelte';
let messages = []
onMount(() => {
subscribe('chat', {
onMessage: (message) => messages = [...messages, message],
})
})
</script>
{#each messages as message}
<div>{message}</div>
{/each}
The subscribe
function will return a EventSource
instance. You may use this instance to close the connection or to check the connection state.
That's it! You're now listening for broadcasts on the chat
channel. Any messages broadcast to the chat
channel will be received by the onMessage
callback.
Subscribing To A Parameterized Channel
Subscribing to a parameterized channel is similar to subscribing to a regular channel. The only difference is that you need to pass the channel parameters in the channel name. For example, if you have a channel named chat/:chat_id/:conversation_id
, you may subscribe to it using the following syntax:
- Imba
- Vue
- React
- Svelte
import { subscribe } from '@formidablejs/broadcaster/src/client'
export tag Chat
messages\string[] = []
chatId = 1
conversationId = 1
def mount
subscribe("chat/{chatId}/{conversationId}", {
onMessage: do(message)
messages.push(message)
imba.commit!
})
def render
<self>
for message in messages
<div> message
<script lang="ts" setup>
import { subscribe } from '@formidablejs/broadcaster/src/client'
import { onMounted, ref } from 'vue'
const messages = ref<string[]>([]);
const chatId = 1;
const conversationId = 1;
onMounted(() => {
subscribe(`chat/${chatId}/${conversationId}`, {
onMessage: (message: string) => messages.value.push(message),
})
})
</script>
<template>
<div>
<div v-for="(message, i) in messages" :key="i">{{ message }}</div>
</div>
</template>
import { subscribe } from '@formidablejs/broadcaster/src/client'
import { useEffect, useState } from 'react'
export default function Chat() {
const [messages, setMessages] = useState<string[]>([]);
const chatId = 1;
const conversationId = 1;
useEffect(() => {
subscribe(`chat/${chatId}/${conversationId}`, {
onMessage: (message: string) => setMessages((messages) => [...messages, message]),
})
}, [])
return (
<div>
{messages.map((message, i) => (
<div key={i}>{message}</div>
))}
</div>
)
}
<script>
import { subscribe } from '@formidablejs/broadcaster/src/client'
import { onMount } from 'svelte';
let messages = []
const chatId = 1;
const conversationId = 1;
onMount(() => {
subscribe(`chat/${chatId}/${conversationId}`, {
onMessage: (message) => messages = [...messages, message],
})
})
</script>
{#each messages as message}
<div>{message}</div>
{/each}
Now, you're listening for broadcasts on the chat/1/1
channel. Any messages broadcast to the chat/1/1
channel will be received by the onMessage
callback.