UNPKG

@laravel/echo-vue

Version:

Vue hooks for seamless integration with Laravel Echo.

315 lines (267 loc) 7.31 kB
import { type BroadcastDriver } from "laravel-echo"; import { onMounted, onUnmounted, ref, watch } from "vue"; import { echo } from "../config"; import type { BroadcastNotification, Channel, ChannelData, ChannelReturnType, Connection, ModelEvents, ModelPayload, } from "../types"; import { toArray } from "../util"; const channels: Record<string, ChannelData<BroadcastDriver>> = {}; const resolveChannelSubscription = <T extends BroadcastDriver>( channel: Channel, ): Connection<T> => { if (channels[channel.id]) { channels[channel.id].count += 1; return channels[channel.id].connection; } const channelSubscription = subscribeToChannel<T>(channel); channels[channel.id] = { count: 1, connection: channelSubscription, }; return channelSubscription; }; const subscribeToChannel = <T extends BroadcastDriver>( channel: Channel, ): Connection<T> => { const instance = echo<T>(); if (channel.visibility === "presence") { return instance.join(channel.name); } if (channel.visibility === "private") { return instance.private(channel.name); } return instance.channel(channel.name); }; const leaveChannel = (channel: Channel, leaveAll: boolean = false): void => { if (!channels[channel.id]) { return; } channels[channel.id].count -= 1; if (channels[channel.id].count > 0) { return; } delete channels[channel.id]; if (leaveAll) { echo().leave(channel.name); } else { echo().leaveChannel(channel.id); } }; export const useEcho = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, TVisibility extends Channel["visibility"] = "private", >( channelName: string, event: string | string[] = [], callback: (payload: TPayload) => void = () => {}, dependencies: any[] = [], visibility: TVisibility = "private" as TVisibility, ) => { const eventCallback = ref(callback); const listening = ref(false); watch( () => callback, (newCallback) => { eventCallback.value = newCallback; }, ); const channel: Channel = { name: channelName, id: ["private", "presence"].includes(visibility) ? `${visibility}-${channelName}` : channelName, visibility, }; const subscription: Connection<TDriver> = resolveChannelSubscription<TDriver>(channel); const events = Array.isArray(event) ? event : [event]; const setupSubscription = () => { listen(); }; const listen = () => { if (listening.value) { return; } events.forEach((e) => { subscription.listen(e, eventCallback.value); }); listening.value = true; }; const stopListening = () => { if (!listening.value) { return; } events.forEach((e) => { subscription.stopListening(e, eventCallback.value); }); listening.value = false; }; const tearDown = (leaveAll: boolean = false) => { stopListening(); leaveChannel(channel, leaveAll); }; onMounted(() => { setupSubscription(); }); onUnmounted(() => { tearDown(); }); if (dependencies.length > 0) { watch( // eslint-disable-next-line @typescript-eslint/no-unsafe-return () => dependencies, () => { tearDown(); setupSubscription(); }, { deep: true }, ); } return { /** * Leave the channel */ leaveChannel: tearDown, /** * Leave the channel and also its associated private and presence channels */ leave: () => tearDown(true), /** * Stop listening for event(s) without leaving the channel */ stopListening, /** * Listen for event(s) */ listen, /** * Channel instance */ channel: () => subscription as ChannelReturnType<TDriver, TVisibility>, }; }; export const useEchoNotification = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, >( channelName: string, callback: (payload: BroadcastNotification<TPayload>) => void = () => {}, event: string | string[] = [], dependencies: any[] = [], ) => { const result = useEcho<BroadcastNotification<TPayload>, TDriver, "private">( channelName, [], callback, dependencies, "private", ); const events = toArray(event) .map((e) => { if (e.includes(".")) { return [e, e.replace(/\./g, "\\")]; } return [e, e.replace(/\\/g, ".")]; }) .flat(); const listening = ref(false); const initialized = ref(false); const cb = (notification: BroadcastNotification<TPayload>) => { if (!listening.value) { return; } if (events.length === 0 || events.includes(notification.type)) { callback(notification); } }; const listen = () => { if (listening.value) { return; } if (!initialized.value) { result.channel().notification(cb); } listening.value = true; initialized.value = true; }; const stopListening = () => { if (!listening.value) { return; } listening.value = false; }; onMounted(() => { listen(); }); return { ...result, /** * Stop listening for notification events */ stopListening, /** * Listen for notification events */ listen, }; }; export const useEchoPresence = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, >( channelName: string, event: string | string[] = [], callback: (payload: TPayload) => void = () => {}, dependencies: any[] = [], ) => { return useEcho<TPayload, TDriver, "presence">( channelName, event, callback, dependencies, "presence", ); }; export const useEchoPublic = < TPayload, TDriver extends BroadcastDriver = BroadcastDriver, >( channelName: string, event: string | string[] = [], callback: (payload: TPayload) => void = () => {}, dependencies: any[] = [], ) => { return useEcho<TPayload, TDriver, "public">( channelName, event, callback, dependencies, "public", ); }; export const useEchoModel = < TPayload, TModel extends string, TDriver extends BroadcastDriver = BroadcastDriver, >( model: TModel, identifier: string | number, event: ModelEvents<TModel> | ModelEvents<TModel>[] = [], callback: (payload: ModelPayload<TPayload>) => void = () => {}, dependencies: any[] = [], ) => { return useEcho<ModelPayload<TPayload>, TDriver, "private">( `${model}.${identifier}`, toArray(event).map((e) => (e.startsWith(".") ? e : `.${e}`)), callback, dependencies, "private", ); };