Installatie
Realtime werkt zonder npm-package: je kopieert drie bestanden naar je Next.js-project en zet vier omgevingsvariabelen. Daarna gebruik je de useChannel-hook (browser) en de pusher-instantie (server).
1. Client-class
Kopieer naar lib/go-pusher-client.ts:
tstype EventCallback = (data: any) => void; export class GoPusherClient { private ws: WebSocket | null = null; private listeners = new Map<string, Map<string, Set<EventCallback>>>(); private socketId: string | null = null; private pingInterval: ReturnType<typeof setInterval> | null = null; private reconnectTimer: ReturnType<typeof setTimeout> | null = null; private subscribedChannels = new Set<string>(); private host: string; constructor( private clientKey: string, host?: string, ) { this.host = host ?? window.location.host; } /** Verbind met de realtime server */ connect(): Promise<void> { return new Promise((resolve, reject) => { const proto = location.protocol === "https:" ? "wss" : "ws"; this.ws = new WebSocket(`${proto}://${this.host}/ws/app/${this.clientKey}`); this.ws.onopen = () => { this.pingInterval = setInterval(() => { this.send("pusher:ping", {}); }, 30000); }; this.ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.event === "pusher:connection_established") { const d = typeof msg.data === "string" ? JSON.parse(msg.data) : msg.data; this.socketId = d.socket_id; resolve(); return; } this.dispatch(msg.channel, msg.event, msg.data); }; this.ws.onerror = () => reject(new Error("Verbinding mislukt")); this.ws.onclose = () => { this.cleanup(); this.reconnectTimer = setTimeout(() => { this.connect().then(() => { this.subscribedChannels.forEach((ch) => this.subscribe(ch)); }); }, 3000); }; }); } /** Abonneer op een publiek kanaal */ subscribe(channel: string): this { this.subscribedChannels.add(channel); this.send("pusher:subscribe", { channel }); return this; } /** Abonneer op een privé- of presence-kanaal */ async subscribePrivate( channel: string, authEndpoint = "/api/pusher/auth", channelData?: { user_id: string; user_info: Record<string, any> }, ): Promise<this> { const body: Record<string, string> = { socket_id: this.socketId!, channel_name: channel, }; if (channelData) body.channel_data = JSON.stringify(channelData); const res = await fetch(authEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const { auth, channel_data } = await res.json(); this.subscribedChannels.add(channel); this.send("pusher:subscribe", { channel, auth, channel_data: channel_data ?? body.channel_data, }); return this; } /** Luister naar een event op een kanaal */ on(channel: string, event: string, cb: EventCallback): this { if (!this.listeners.has(channel)) this.listeners.set(channel, new Map()); const ch = this.listeners.get(channel)!; if (!ch.has(event)) ch.set(event, new Set()); ch.get(event)!.add(cb); return this; } /** Stop met luisteren */ off(channel: string, event: string, cb: EventCallback): void { this.listeners.get(channel)?.get(event)?.delete(cb); } /** Afmelden van een kanaal */ unsubscribe(channel: string): void { this.subscribedChannels.delete(channel); this.listeners.delete(channel); this.send("pusher:unsubscribe", { channel }); } /** Socket ID (nodig voor kanaal-auth) */ getSocketId(): string | null { return this.socketId; } /** Verbreek de verbinding */ disconnect(): void { if (this.reconnectTimer) clearTimeout(this.reconnectTimer); this.cleanup(); this.ws?.close(); } private send(event: string, data: any): void { this.ws?.send(JSON.stringify({ event, data })); } private dispatch(ch: string, ev: string, data: any): void { const parsed = typeof data === "string" ? JSON.parse(data) : data; this.listeners.get(ch)?.get(ev)?.forEach((cb) => cb(parsed)); } private cleanup(): void { if (this.pingInterval) clearInterval(this.pingInterval); this.socketId = null; } }
2. Server-class
Kopieer naar lib/go-pusher-server.ts:
tsimport { createHmac } from "crypto"; export class GoPusherServer { constructor( private apiKey: string, private baseUrl: string, ) {} /** Publiceer een event naar een kanaal */ async publish(channel: string, event: string, data: unknown): Promise<void> { const res = await fetch(`${this.baseUrl}/api/events`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ channel, event, data: JSON.stringify(data) }), }); if (!res.ok) throw new Error(`Realtime publish mislukt: ${res.status}`); } /** Onderteken een kanaal-autorisatie (voor privé/presence) */ authenticate( socketId: string, channelName: string, channelData?: string, ): { auth: string; channel_data?: string } { let stringToSign = `${socketId}:${channelName}`; if (channelData) stringToSign += `:${channelData}`; const auth = createHmac("sha256", this.apiKey).update(stringToSign).digest("hex"); return { auth, ...(channelData && { channel_data: channelData }) }; } } // Eén gedeelde instantie, gevoed door je omgevingsvariabelen: export const pusher = new GoPusherServer( process.env.GOPUSHER_API_KEY!, process.env.GOPUSHER_URL!, );
3. React-hook
Kopieer naar hooks/use-channel.ts:
ts"use client"; import { useEffect, useRef } from "react"; import { GoPusherClient } from "@/lib/go-pusher-client"; // Eén gedeelde verbinding per app let client: GoPusherClient | null = null; let connectPromise: Promise<void> | null = null; function getClient(clientKey: string): Promise<GoPusherClient> { if (!client) { // De host komt uit NEXT_PUBLIC_GOPUSHER_HOST (bijv. realtime.eduinsights.nl) client = new GoPusherClient(clientKey, process.env.NEXT_PUBLIC_GOPUSHER_HOST); connectPromise = client.connect(); } return connectPromise!.then(() => client!); } /** * Abonneer op een kanaal en luister naar een event. * * useChannel(process.env.NEXT_PUBLIC_GOPUSHER_KEY!, "chat", "nieuw-bericht", (data) => { * setBericht(data); * }); */ export function useChannel<T = any>( clientKey: string, channel: string, event: string, callback: (data: T) => void, ): void { const callbackRef = useRef(callback); callbackRef.current = callback; useEffect(() => { let cancelled = false; const handler = (data: T) => { if (!cancelled) callbackRef.current(data); }; getClient(clientKey).then((c) => { if (cancelled) return; c.subscribe(channel).on(channel, event, handler); }); return () => { cancelled = true; client?.off(channel, event, handler); }; }, [clientKey, channel, event]); }
4. Omgevingsvariabelen
Voeg toe aan .env.local (de waarden vind je op het tabblad Gegevens van je realtime app). Heb je de realtime app aan je Cloud-app gekoppeld? Dan staan deze er al automatisch in.
bash# Server (Server Actions / Route Handlers) GOPUSHER_API_KEY=JE_API_KEY GOPUSHER_URL=https://realtime.eduinsights.nl # Browser (client components) NEXT_PUBLIC_GOPUSHER_HOST=realtime.eduinsights.nl NEXT_PUBLIC_GOPUSHER_KEY=JE_CLIENT_KEY
Houd je API-sleutel geheim
GOPUSHER_API_KEY geeft volledige publiceer-toegang en wordt gebruikt om kanalen te ondertekenen. Gebruik hem alleen server-side, nooit met een NEXT_PUBLIC_-prefix.
Klaar? Ga verder met Client (browser).
Meer in Realtime (WebSockets)