@jengo/broadcasting
v1.0.0
Published
Universal real-time event broadcasting client for Jengo and CodeIgniter 4 supporting Server-Sent Events (SSE) and WebSockets.
Maintainers
Readme
@jengo/broadcasting
Universal real-time event broadcasting client for Jengo and CodeIgniter 4 applications.
Pairs directly with the jengo/broadcasting PHP backend package. Supports Server-Sent Events (SSE) and WebSockets (Soketi, Pusher, Reverb, Jengo WS) with automatic reconnection, channel authorization handshakes, and first-party React and Vue integrations.
Installation
# npm
npm install @jengo/broadcasting
# pnpm
pnpm add @jengo/broadcasting
# yarn
yarn add @jengo/broadcastingQuick Start: Server-Sent Events (SSE)
Server-Sent Events allow real-time streaming over standard HTTP connections with zero extra services or WebSocket servers:
import Broadcaster from '@jengo/broadcasting';
const broadcaster = new Broadcaster({
broadcaster: 'sse',
endpoint: '/broadcasting/sse',
authEndpoint: '/broadcasting/auth',
});
// Subscribe to a public channel
broadcaster.channel('orders')
.listen('OrderCreated', (event) => {
console.log('New Order:', event);
});Quick Start: WebSockets (Soketi / Pusher / Jengo WS)
import Broadcaster from '@jengo/broadcasting';
const broadcaster = new Broadcaster({
broadcaster: 'ws',
key: 'jengo-app-key',
wsHost: window.location.hostname,
wsPort: 6001,
forceTLS: false,
authEndpoint: '/broadcasting/auth',
});
// Subscribe to a public channel
broadcaster.channel('orders')
.listen('OrderCreated', (event) => {
console.log('New Order:', event);
});Channels & Subscriptions
1. Public Channels
Public channels do not require authentication:
const channel = broadcaster.channel('news');
channel.listen('HeadlineUpdated', (data) => {
console.log(data.headline);
});
// Stop listening to a specific event
channel.stopListening('HeadlineUpdated');
// Leave channel
broadcaster.leave('news');2. Private Channels
Private channels automatically invoke /broadcasting/auth with CodeIgniter 4 CSRF verification:
const privateChannel = broadcaster.private(`chat.${roomId}`);
privateChannel.listen('NewMessage', (message) => {
console.log('Private message received:', message);
});
// Client-to-client whisper (typing indicator)
privateChannel.whisper('typing', { userId: 42 });
privateChannel.listenForWhisper('typing', (data) => {
console.log('User is typing:', data.userId);
});3. Presence Channels
Presence channels allow tracking who is currently active in a channel:
const presence = broadcaster.join(`room.${roomId}`);
presence
.here((members) => {
console.log('Currently in room:', members);
})
.joining((newMember) => {
console.log('Joined:', newMember);
})
.leaving((leftMember) => {
console.log('Left:', leftMember);
})
.listen('RoomAnnouncement', (event) => {
console.log('Announcement:', event);
});React Integration
Import directly from @jengo/broadcasting/react:
import React, { useState } from 'react';
import { BroadcastingProvider, useChannel, usePresence } from '@jengo/broadcasting/react';
import Broadcaster from '@jengo/broadcasting';
const broadcaster = new Broadcaster({
broadcaster: 'sse',
endpoint: '/broadcasting/sse',
});
export function App() {
return (
<BroadcastingProvider client={broadcaster}>
<ChatRoom roomId="general" />
</BroadcastingProvider>
);
}
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<any[]>([]);
// Automatically binds and unbinds on mount/unmount
useChannel(`chat.${roomId}`, 'NewMessage', (msg) => {
setMessages((prev) => [...prev, msg]);
});
// Reactive presence member list
const { members } = usePresence(`room.${roomId}`);
return (
<div>
<h3>Online Users ({members.length})</h3>
<ul>
{members.map((m) => (
<li key={m.id}>{m.info?.name ?? m.id}</li>
))}
</ul>
</div>
);
}Vue 3 Integration
Import directly from @jengo/broadcasting/vue:
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import Broadcaster from '@jengo/broadcasting';
import { BroadcastingPlugin } from '@jengo/broadcasting/vue';
const broadcaster = new Broadcaster({
broadcaster: 'sse',
endpoint: '/broadcasting/sse',
});
const app = createApp(App);
app.use(BroadcastingPlugin, broadcaster);
app.mount('#app');<!-- ChatRoom.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { useChannel, usePresence } from '@jengo/broadcasting/vue';
const messages = ref<any[]>([]);
useChannel('orders', 'OrderCreated', (order) => {
messages.value.push(order);
});
const { members } = usePresence('room.lobby');
</script>
<template>
<div>
<h3>Members: {{ members.length }}</h3>
<ul>
<li v-for="member in members" :key="member.id">{{ member.info?.name }}</li>
</ul>
</div>
</template>Svelte Integration
Import directly from @jengo/broadcasting/svelte:
Method A: Reactive Svelte Stores ($store)
Use createChannelStore to bind broadcasted events directly to Svelte readable stores with automatic subscription lifecycle management:
<script lang="ts">
import { Broadcaster } from '@jengo/broadcasting';
import { createChannelStore } from '@jengo/broadcasting/svelte';
const broadcaster = new Broadcaster({
broadcaster: 'sse',
endpoint: '/broadcasting/sse',
});
const latestOrder = createChannelStore('orders', 'OrderCreated', null, broadcaster);
</script>
{#if $latestOrder}
<p>New order arrived: {$latestOrder.id} (${$latestOrder.total})</p>
{/if}Method B: Context & Lifecycle Hooks
Set the client once in your root layout or parent component:
<!-- +layout.svelte -->
<script lang="ts">
import { Broadcaster } from '@jengo/broadcasting';
import { setBroadcaster } from '@jengo/broadcasting/svelte';
const broadcaster = new Broadcaster({
broadcaster: 'sse',
endpoint: '/broadcasting/sse',
});
setBroadcaster(broadcaster);
</script>
<slot />Use lifecycle-aware hooks in child components:
<!-- ChatRoom.svelte -->
<script lang="ts">
import { useChannel, usePresence } from '@jengo/broadcasting/svelte';
let messages = [];
// Automatically detaches listener on component unmount
useChannel('chat.lobby', 'NewMessage', (msg) => {
messages = [...messages, msg];
});
// Returns a Svelte store for reactive member list and presence channel handle
const { members } = usePresence('room.lobby');
</script>
<div>
<h3>Online Users ({$members.length})</h3>
<ul>
{#each $members as member (member.id)}
<li>{member.info?.name ?? member.id}</li>
{/each}
</ul>
</div>CodeIgniter 4 CSRF & Auth Configuration
@jengo/broadcasting automatically resolves CodeIgniter 4's CSRF token from:
<meta name="csrf-token" content="...">or<meta name="X-CSRF-TOKEN" content="...">- Browser cookies (
csrf_cookie_nameorcsrf_test_name) - Global variables
window.jengoCsrforwindow.csrfToken
You can also pass custom authorization headers or tokens explicitly:
const broadcaster = new Broadcaster({
broadcaster: 'sse',
bearerToken: () => localStorage.getItem('auth_token'),
auth: {
headers: {
'X-Custom-Header': 'value',
},
},
});Event Deduplication
To prevent duplicate event rendering caused by network retries or stream reconnections, @jengo/broadcasting includes a built-in event deduplicator. Duplicate event payloads with identical event IDs received within the sliding deduplication window (default: 10 seconds) are automatically ignored.
const broadcaster = new Broadcaster({
broadcaster: 'sse',
deduplicationWindowMs: 15000, // 15 seconds window
});License
Released under the MIT License.
